diff --git a/.gitignore b/.gitignore index 8bc310c..791b6fc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ build/ /.idea/ /.mvn/ /config.json +/web-server/src/main/webui/src/json-rpc.js +/.cache/ +ovpn-connector.json diff --git a/app/pom.xml b/app/pom.xml index 9b848e9..e7b356c 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -25,7 +25,6 @@ - org.eclipse.jgit @@ -40,6 +39,37 @@ 0.39.0 + + + org.eclipse.jetty + jetty-server + 12.0.12 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + prepare-package + + java + + + ru.kirillius.json.rpc.CodeGeneration.AutoGenerationUtility + + --b=${basedir} + ${project.basedir}/../webui/src/json-rpc.js + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/kirillius/pf/sdn/App.java b/app/src/main/java/ru/kirillius/pf/sdn/App.java index f52638c..865b857 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/App.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java @@ -1,48 +1,182 @@ package ru.kirillius.pf.sdn; +import lombok.Getter; import lombok.SneakyThrows; -import ru.kirillius.java.utils.events.EventListener; -import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; +import ru.kirillius.pf.sdn.External.API.Components.FRR; +import ru.kirillius.pf.sdn.External.API.Components.OVPN; +import ru.kirillius.pf.sdn.External.API.Components.TDNS; +import ru.kirillius.pf.sdn.External.API.HEInfoProvider; +import ru.kirillius.pf.sdn.core.Auth.AuthManager; +import ru.kirillius.pf.sdn.core.Auth.TokenStorage; +import ru.kirillius.pf.sdn.core.*; +import ru.kirillius.pf.sdn.core.Component; +import ru.kirillius.pf.sdn.core.Networking.ASInfoService; +import ru.kirillius.pf.sdn.core.Networking.NetworkManager; +import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager; +import ru.kirillius.pf.sdn.core.Util.Wait; +import ru.kirillius.pf.sdn.web.HTTPServer; import ru.kirillius.utils.logging.SystemLogger; +import java.io.Closeable; import java.io.File; import java.io.IOException; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; -public class App extends AppContext { +public class App implements Context, Closeable { private final static File configFile = new File("config.json"); + protected final static String CTX = App.class.getSimpleName(); + + static { + SystemLogger.initializeLogging(Level.INFO, List.of(InMemoryLogHandler.class)); + SystemLogger.setExceptionDumping(true); + } + + private final AtomicBoolean shouldRestart = new AtomicBoolean(false); + private final AtomicBoolean running = new AtomicBoolean(true); + + @Getter + private final NetworkManager networkManager; + @Getter + private Config config; + @Getter + private final AuthManager authManager; + @Getter + private final HTTPServer server; + @Getter + private final ASInfoService ASInfoService; + @Getter + private final SubscriptionManager subscriptionManager; + @Getter + private final UpdateManager updateManager; + @Getter + private final TokenStorage tokenStorage; + @Getter + private final ContextEventsHandler EventsHandler = new ContextEventsHandler(); + + private final List> loadedComponents = new ArrayList<>(); @SneakyThrows public App(File configFile) { - super(configFile); + try { + config = Config.load(configFile); + } catch (IOException e) { + config = new Config(); + try { + Config.store(config, configFile); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + authManager = new AuthManager(this); + ASInfoService = new ASInfoService(); + ASInfoService.setProvider(new HEInfoProvider(this)); + networkManager = new NetworkManager(this); + networkManager.getInputResources().add(config.getCustomResources()); + subscriptionManager = new SubscriptionManager(this); + updateManager = new UpdateManager(this); + tokenStorage = new TokenStorage(this); + subscribe(); + updateManager.start(); + initComponents(); + server = new HTTPServer(this); + if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) { + SystemLogger.error("There is no password for admin. Setting default password: admin", CTX); + getAuthManager().updatePassword("admin"); + } getSubscriptionManager().triggerUpdate(); - - - } - - static { - SystemLogger.initializeLogging(Level.INFO, Collections.emptyList()); } public static void main(String[] args) { - - try (App app = new App(configFile)) { - app.getEventsHandler().getNetworkManagerUpdateEvent().add(new EventListener() { - @Override - public void invoke(NetworkResourceBundle bundle) throws Exception { - SystemLogger.message("Network resource bundle updated.", CTX); - } - }); - while (true) { - Thread.yield(); + var restart = false; + do { + try (var app = new App(configFile)) { + Wait.when(app.running::get); + restart = app.shouldRestart.get(); + } catch (Exception e) { + SystemLogger.error("Unhandled error", CTX, e); } - } catch (IOException e) { - throw new RuntimeException(e); - } + } while (restart); } + public void triggerRestart() { + SystemLogger.message("Restarting app", CTX); + running.set(false); + shouldRestart.set(true); + } -} + public void triggerShutdown() { + SystemLogger.message("Shutting down app", CTX); + running.set(false); + } + + public Collection>> getComponentClasses() { + return List.of(FRR.class, OVPN.class, TDNS.class); + } + + public void initComponents() { + var enabledPlugins = config.getEnabledComponents(); + + (List.copyOf(loadedComponents)).forEach(plugin -> { + if (!enabledPlugins.contains(plugin.getClass())) { + SystemLogger.message("Unloading plugin: " + plugin.getClass().getSimpleName(), CTX); + try { + plugin.close(); + } catch (IOException e) { + SystemLogger.error("Error on plugin unload", CTX, e); + } finally { + loadedComponents.remove(plugin); + } + } + }); + var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList(); + enabledPlugins.forEach(pluginClass -> { + if (loadedClasses.contains(pluginClass)) { + return; + } + SystemLogger.message("Loading plugin: " + pluginClass.getSimpleName(), CTX); + var plugin = Component.loadPlugin(pluginClass, this); + loadedComponents.add(plugin); + }); + } + + private void subscribe() { + var eventsHandler = getEventsHandler(); + eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> { + var manager = getNetworkManager(); + var inputResources = getNetworkManager().getInputResources(); + inputResources.clear(); + inputResources.add(config.getCustomResources()); + inputResources.add(bundle); + manager.triggerUpdate(false); + }); + } + + @Override + public Component getPluginInstance(Class> pluginClass) { + return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null); + } + + @Override + public void close() throws IOException { + loadedComponents.forEach(plugin -> { + try { + plugin.close(); + } catch (IOException e) { + SystemLogger.error("Error closing plugin", CTX, e); + } + }); + ASInfoService.close(); + networkManager.close(); + try { + server.stop(); + } catch (Exception e) { + SystemLogger.error("Error stopping server", CTX, e); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java b/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java deleted file mode 100644 index dc0077b..0000000 --- a/app/src/main/java/ru/kirillius/pf/sdn/AppContext.java +++ /dev/null @@ -1,106 +0,0 @@ -package ru.kirillius.pf.sdn; - -import lombok.Getter; -import org.eclipse.jetty.server.Server; -import ru.kirillius.pf.sdn.External.API.HEInfoProvider; -import ru.kirillius.pf.sdn.core.Auth.AuthManager; -import ru.kirillius.pf.sdn.core.Config; -import ru.kirillius.pf.sdn.core.Context; -import ru.kirillius.pf.sdn.core.ContextEventsHandler; -import ru.kirillius.pf.sdn.core.Networking.ASInfoService; -import ru.kirillius.pf.sdn.core.Networking.NetworkManager; -import ru.kirillius.pf.sdn.core.Plugin; -import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager; -import ru.kirillius.utils.logging.SystemLogger; - -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public class AppContext implements Context, Closeable { - - protected final static String CTX = AppContext.class.getSimpleName(); - - public AppContext(File configFile) { - try { - config = Config.load(configFile); - } catch (IOException e) { - config = new Config(); - try { - Config.store(config, configFile); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - authManager = new AuthManager(this); - server = new Server(); - ASInfoService = new ASInfoService(); - ASInfoService.setProvider(new HEInfoProvider(this)); - networkManager = new NetworkManager(this); - networkManager.getInputResources().add(config.getCustomResources()); - subscriptionManager = new SubscriptionManager(this); - subscribe(); - initPlugins(); - } - - private void initPlugins() { - config.getEnabledPlugins().forEach(pluginClass -> { - var plugin = Plugin.loadPlugin(pluginClass, this); - loadedPlugins.add(plugin); - }); - } - - private final List> loadedPlugins = new ArrayList<>(); - - private void subscribe() { - var eventsHandler = getEventsHandler(); - eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> { - var manager = getNetworkManager(); - var inputResources = getNetworkManager().getInputResources(); - inputResources.clear(); - inputResources.add(config.getCustomResources()); - inputResources.add(bundle); - manager.triggerUpdate(); - }); - } - - - @Getter - private final NetworkManager networkManager; - @Getter - private Config config; - @Getter - private final AuthManager authManager; - @Getter - private final Server server; - @Getter - private final ASInfoService ASInfoService; - @Getter - private final SubscriptionManager subscriptionManager; - - @Override - public void close() throws IOException { - loadedPlugins.forEach(plugin -> { - try { - plugin.close(); - } catch (IOException e) { - SystemLogger.error("Error closing plugin", CTX, e); - } - }); - ASInfoService.close(); - networkManager.close(); - try { - server.stop(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Getter - private final ContextEventsHandler EventsHandler = new ContextEventsHandler(); - - -} diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/FRRPlugin.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java similarity index 95% rename from app/src/main/java/ru/kirillius/pf/sdn/External/API/FRRPlugin.java rename to app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java index a9f73e1..8f6e810 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/FRRPlugin.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/FRR.java @@ -1,11 +1,12 @@ -package ru.kirillius.pf.sdn.External.API; +package ru.kirillius.pf.sdn.External.API.Components; import lombok.*; import ru.kirillius.java.utils.events.EventListener; import ru.kirillius.json.JSONArrayProperty; import ru.kirillius.json.JSONProperty; import ru.kirillius.json.JSONSerializable; -import ru.kirillius.pf.sdn.core.AbstractPlugin; +import ru.kirillius.pf.sdn.External.API.ShellExecutor; +import ru.kirillius.pf.sdn.core.AbstractComponent; import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; @@ -17,13 +18,13 @@ import java.util.List; import java.util.function.Consumer; import java.util.regex.Pattern; -public final class FRRPlugin extends AbstractPlugin { +public final class FRR extends AbstractComponent { private final static String SUBNET_PATTERN = "{%subnet}"; private final static String GW_PATTERN = "{%gateway}"; - private final static String CTX = FRRPlugin.class.getSimpleName(); + private final static String CTX = FRR.class.getSimpleName(); private final EventListener subscription; - public FRRPlugin(Context context) { + public FRR(Context context) { super(context); subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets())); } diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java new file mode 100644 index 0000000..21b08ea --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java @@ -0,0 +1,72 @@ +package ru.kirillius.pf.sdn.External.API.Components; + +import lombok.Getter; +import lombok.Setter; +import org.json.JSONArray; +import org.json.JSONObject; +import ru.kirillius.java.utils.events.EventListener; +import ru.kirillius.json.JSONProperty; +import ru.kirillius.json.JSONSerializable; +import ru.kirillius.json.rpc.Annotations.JRPCMethod; +import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; +import ru.kirillius.pf.sdn.External.API.ShellExecutor; +import ru.kirillius.pf.sdn.core.AbstractComponent; +import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.core.Util.IPv4Util; +import ru.kirillius.pf.sdn.web.ProtectedMethod; +import ru.kirillius.utils.logging.SystemLogger; + +import java.io.IOException; + +public final class OVPN extends AbstractComponent { + private final static String CTX = OVPN.class.getSimpleName(); + private final EventListener subscription; + + public OVPN(Context context) { + super(context); + subscription = context.getEventsHandler().getRPCInitEvent() + .add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this)); + } + + @JRPCMethod + @ProtectedMethod + public void restartSystemService() { + try (var shell = new ShellExecutor(config.shellConfig)) { + shell.executeCommand(new String[]{config.restartCommand}); + } catch (IOException e) { + SystemLogger.error("Error when trying to restart OVPN", CTX, e); + } + } + + @JRPCMethod + @ProtectedMethod + public JSONArray getManagedRoutes() { + var array = new JSONArray(); + context.getNetworkManager().getOutputResources().getSubnets().stream().map(subnet -> { + var json = new JSONObject(); + json.put("address", subnet.getAddress()); + json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength()))); + return json; + }).forEach(array::put); + return array; + } + + + @Override + public void close() throws IOException { + context.getEventsHandler().getRPCInitEvent().remove(subscription); + } + + @JSONSerializable + public static class OVPNConfig { + @Getter + @Setter + @JSONProperty + private ShellExecutor.Config shellConfig = new ShellExecutor.Config(); + + @Getter + @Setter + @JSONProperty + private String restartCommand = "rc-service openvpn restart"; + } +} diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumPlugin.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java similarity index 84% rename from app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumPlugin.java rename to app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java index 13a268e..94eaf98 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumPlugin.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java @@ -1,11 +1,12 @@ -package ru.kirillius.pf.sdn.External.API; +package ru.kirillius.pf.sdn.External.API.Components; import lombok.*; import ru.kirillius.java.utils.events.EventListener; import ru.kirillius.json.JSONArrayProperty; import ru.kirillius.json.JSONProperty; import ru.kirillius.json.JSONSerializable; -import ru.kirillius.pf.sdn.core.AbstractPlugin; +import ru.kirillius.pf.sdn.External.API.TDNSAPI; +import ru.kirillius.pf.sdn.core.AbstractComponent; import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.utils.logging.SystemLogger; @@ -15,12 +16,12 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -public final class TechnitiumPlugin extends AbstractPlugin { +public final class TDNS extends AbstractComponent { - private final static String CTX = TechnitiumPlugin.class.getSimpleName(); + private final static String CTX = TDNS.class.getSimpleName(); private final EventListener subscription; - public TechnitiumPlugin(Context context) { + public TDNS(Context context) { super(context); subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains())); } @@ -28,10 +29,10 @@ public final class TechnitiumPlugin extends AbstractPlugin domains) { for (var instance : config.instances) { SystemLogger.message("Updating zones on DNS server " + instance.server, CTX); - try (var api = new TechnitiumDNSAPI(instance.server, instance.token)) { + try (var api = new TDNSAPI(instance.server, instance.token)) { var existingForwardZones = api.getZones().stream() - .filter(zoneResponse -> zoneResponse.getType() == TechnitiumDNSAPI.ZoneType.Forwarder) - .map(TechnitiumDNSAPI.ZoneResponse::getName) + .filter(zoneResponse -> zoneResponse.getType() == TDNSAPI.ZoneType.Forwarder) + .map(TDNSAPI.ZoneResponse::getName) .collect(Collectors.toCollection(ArrayList::new)); existingForwardZones.forEach(zoneName -> { if (!domains.contains(zoneName)) { diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumDNSAPI.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java similarity index 97% rename from app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumDNSAPI.java rename to app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java index 17e4004..58d4a72 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/TechnitiumDNSAPI.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/TDNSAPI.java @@ -16,12 +16,12 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.*; -public class TechnitiumDNSAPI implements Closeable { +public class TDNSAPI implements Closeable { private final String server; private final String authToken; private final HttpClient httpClient; - public TechnitiumDNSAPI(String server, String authToken) { + public TDNSAPI(String server, String authToken) { this.server = server; httpClient = HttpClient.newBuilder().build(); this.authToken = authToken; diff --git a/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java b/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java new file mode 100644 index 0000000..ced7a5e --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/InMemoryLogHandler.java @@ -0,0 +1,40 @@ +package ru.kirillius.pf.sdn; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +public class InMemoryLogHandler extends Handler { + private final static Queue queue = new ConcurrentLinkedQueue<>(); + + private final static DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US); + + private String format(LogRecord logRecord) { + return "[" + dateFormat.format(new Date(logRecord.getMillis())) + "][" + logRecord.getLevel().getName() + "] " + logRecord.getMessage().trim(); + } + + public static Collection getMessages() { + return Collections.unmodifiableCollection(queue); + } + + @Override + public void publish(LogRecord logRecord) { + if (queue.size() >= 1000) { + queue.poll(); + } + queue.add(format(logRecord)); + } + + @Override + public void flush() { + + } + + @Override + public void close() { + + } +} diff --git a/web-server/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java b/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java similarity index 71% rename from web-server/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java rename to app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java index 22713f5..a5b6079 100644 --- a/web-server/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java @@ -5,12 +5,14 @@ import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.jetbrains.annotations.Nullable; import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.pf.sdn.core.Context; -import ru.kirillius.utils.logging.SystemLogger; -import ru.kirillius.pf.sdn.web.RPC.AuthRPC; +import ru.kirillius.pf.sdn.web.RPC.Auth; +import ru.kirillius.pf.sdn.web.RPC.NetworkManager; import ru.kirillius.pf.sdn.web.RPC.RPC; +import ru.kirillius.pf.sdn.web.RPC.SubscriptionManager; +import ru.kirillius.pf.sdn.web.RPC.System; +import ru.kirillius.utils.logging.SystemLogger; import java.net.MalformedURLException; import java.net.URL; @@ -23,8 +25,6 @@ public class HTTPServer extends Server { private final static String LOG_CONTEXT = HTTPServer.class.getSimpleName(); private final static String TOKEN_HEADER = "X-Auth-token"; - private final JSONRPCServlet JSONRPC = new JSONRPCServlet(); - private String getResourceBase() throws MalformedURLException { var resourceFile = getClass().getClassLoader().getResource("htdocs/index.html"); return new URL(Objects.requireNonNull(resourceFile).getProtocol(), resourceFile.getHost(), resourceFile.getPath() @@ -32,26 +32,35 @@ public class HTTPServer extends Server { .toString(); } - private final static Set> RPCHandlerTypes = Set.of(AuthRPC.class); + private final static Set> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class); - public HTTPServer(Context appContext, int port, @Nullable String host) throws Exception { - + public HTTPServer(Context appContext) { + var config = appContext.getConfig(); var connector = new ServerConnector(this); - connector.setPort(port); + connector.setPort(config.getHttpPort()); + var host = config.getHost(); if (host != null && !host.equals(ANY_HOST)) { connector.setHost(host); } this.addConnector(connector); - ServletContextHandler servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + var JSONRPC = new JSONRPCServlet(); servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH); var holder = servletContext.addServlet(DefaultServlet.class, "/"); - holder.setInitParameter("resourceBase", getResourceBase()); + try { + holder.setInitParameter("resourceBase", getResourceBase()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } this.setHandler(servletContext); - start(); - + try { + start(); + } catch (Exception e) { + throw new RuntimeException("Error starting HTTPServer", e); + } JSONRPC.addRequestHandler((request, response, call) -> { var authManager = appContext.getAuthManager(); @@ -87,5 +96,13 @@ public class HTTPServer extends Server { (throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) + " has failed with error", LOG_CONTEXT, throwable.getError()); }); + + try { + appContext.getEventsHandler().getRPCInitEvent().invoke(JSONRPC); + } catch (Exception e) { + SystemLogger.error("Error on RPC init event", CTX, e); + } } + + private final static String CTX = HTTPServer.class.getSimpleName(); } diff --git a/web-server/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java b/app/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java similarity index 100% rename from web-server/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java rename to app/src/main/java/ru/kirillius/pf/sdn/web/ProtectedMethod.java diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java new file mode 100644 index 0000000..b7223ac --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/Auth.java @@ -0,0 +1,93 @@ +package ru.kirillius.pf.sdn.web.RPC; + +import org.json.JSONArray; +import ru.kirillius.json.JSONUtility; +import ru.kirillius.json.rpc.Annotations.JRPCArgument; +import ru.kirillius.json.rpc.Annotations.JRPCContext; +import ru.kirillius.json.rpc.Annotations.JRPCMethod; +import ru.kirillius.json.rpc.Servlet.CallContext; +import ru.kirillius.pf.sdn.core.Auth.AuthToken; +import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.web.ProtectedMethod; + +import java.util.Date; + +public class Auth implements RPC { + private final Context context; + + public Auth(Context context) { + this.context = context; + } + + @ProtectedMethod + @JRPCMethod + public JSONArray listTokens() { + return JSONUtility.serializeCollection(context.getTokenStorage().getTokens(), AuthToken.class, null); + } + + @ProtectedMethod + @JRPCMethod + public void removeToken(@JRPCArgument(name = "token") String token) { + context.getTokenStorage().remove(new AuthToken(token)); + } + + @ProtectedMethod + @JRPCMethod + public String createAPIToken(@JRPCArgument(name = "description") String description) { + var token = new AuthToken(); + token.setDescription(description); + context.getTokenStorage().add(token); + return token.getToken(); + } + + @ProtectedMethod + @JRPCMethod + public String createToken(@JRPCContext CallContext call) { + var UA = call.getRequest().getHeader("User-Agent"); + if (UA == null) { + UA = "Unknown user agent"; + } + var authManager = context.getAuthManager(); + var token = authManager.createToken(UA +" "+ new Date()); + authManager.setSessionToken(call.getSession(), token); + return token.getToken(); + } + + @JRPCMethod + public boolean startSessionByPassword(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) { + var authManager = context.getAuthManager(); + if (authManager.validatePassword(password)) { + authManager.setSessionAuthState(call.getSession(), true); + return true; + } + return false; + } + + @JRPCMethod + public boolean startSessionByToken(@JRPCArgument(name = "token") String token, @JRPCContext CallContext call) { + var authManager = context.getAuthManager(); + if (authManager.validateToken(token)) { + authManager.setSessionAuthState(call.getSession(), true); + return true; + } + return false; + } + + @JRPCMethod + public boolean isAuthenticated(@JRPCContext CallContext call) { + var authManager = context.getAuthManager(); + return authManager.getSessionAuthState(call.getSession()); + } + + + @JRPCMethod + public void logout(@JRPCContext CallContext call) { + var authManager = context.getAuthManager(); + authManager.setSessionAuthState(call.getSession(), false); + var token = authManager.getSessionToken(call.getSession()); + if (token != null) { + authManager.invalidateToken(token); + authManager.setSessionToken(call.getSession(), null); + } + } +} diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java new file mode 100644 index 0000000..60e6e9a --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/NetworkManager.java @@ -0,0 +1,34 @@ +package ru.kirillius.pf.sdn.web.RPC; + +import org.json.JSONObject; +import ru.kirillius.json.JSONUtility; +import ru.kirillius.json.rpc.Annotations.JRPCArgument; +import ru.kirillius.json.rpc.Annotations.JRPCMethod; +import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.web.ProtectedMethod; + +public class NetworkManager implements RPC { + private final Context context; + + public NetworkManager(Context context) { + this.context = context; + } + + @JRPCMethod + @ProtectedMethod + public boolean isUpdating() { + return context.getNetworkManager().isUpdatingNow(); + } + + @JRPCMethod + @ProtectedMethod + public void triggerUpdate(@JRPCArgument(name = "ignoreCache") boolean ignoreCache) { + context.getNetworkManager().triggerUpdate(ignoreCache); + } + + @JRPCMethod + @ProtectedMethod + public JSONObject getOutputResources() { + return JSONUtility.serializeStructure(context.getNetworkManager().getOutputResources()); + } +} diff --git a/web-server/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java similarity index 100% rename from web-server/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java rename to app/src/main/java/ru/kirillius/pf/sdn/web/RPC/RPC.java diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java new file mode 100644 index 0000000..95709a9 --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/SubscriptionManager.java @@ -0,0 +1,51 @@ +package ru.kirillius.pf.sdn.web.RPC; + +import org.json.JSONArray; +import org.json.JSONObject; +import ru.kirillius.json.JSONUtility; +import ru.kirillius.json.rpc.Annotations.JRPCArgument; +import ru.kirillius.json.rpc.Annotations.JRPCMethod; +import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; +import ru.kirillius.pf.sdn.web.ProtectedMethod; + +import java.util.stream.Collectors; + +public class SubscriptionManager implements RPC { + private final Context context; + + public SubscriptionManager(Context context) { + this.context = context; + } + + @JRPCMethod + @ProtectedMethod + public boolean isUpdating() { + return context.getSubscriptionManager().isUpdatingNow(); + } + + @JRPCMethod + @ProtectedMethod + public void triggerUpdate() { + context.getSubscriptionManager().triggerUpdate(); + } + + @JRPCMethod + @ProtectedMethod + public JSONArray getSubscribedResources() { + return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null); + } + + @JRPCMethod + @ProtectedMethod + public JSONObject getAvailableResources() { + return JSONUtility.serializeMap(context.getSubscriptionManager().getAvailableResources(), String.class, NetworkResourceBundle.class, null, null); + } + + @JRPCMethod + @ProtectedMethod + public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) { + context.getConfig().setSubscribedResources(JSONUtility.deserializeCollection(subscribedResources, String.class, null).collect(Collectors.toList())); + triggerUpdate(); + } +} diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java new file mode 100644 index 0000000..d46544d --- /dev/null +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java @@ -0,0 +1,80 @@ +package ru.kirillius.pf.sdn.web.RPC; + +import org.json.JSONArray; +import org.json.JSONObject; +import ru.kirillius.json.JSONUtility; +import ru.kirillius.json.rpc.Annotations.JRPCArgument; +import ru.kirillius.json.rpc.Annotations.JRPCMethod; +import ru.kirillius.pf.sdn.core.Component; +import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.web.ProtectedMethod; + +import java.util.stream.Collectors; + +public class System implements RPC { + private final Context context; + + public System(Context context) { + this.context = context; + } + + @ProtectedMethod + @JRPCMethod + public void restart() { + context.triggerRestart(); + } + + @ProtectedMethod + @JRPCMethod + public void shutdown() { + context.triggerShutdown(); + } + + @ProtectedMethod + @JRPCMethod + public JSONObject getConfig() { + return JSONUtility.serializeStructure(context.getConfig()); + } + + @ProtectedMethod + @JRPCMethod + public boolean isConfigChanged() { + return context.getConfig().isModified(); + } + + @ProtectedMethod + @JRPCMethod + public boolean hasUpdates() { + return true; + } + + + @ProtectedMethod + @JRPCMethod + public JSONArray getEnabledComponents() { + return JSONUtility.serializeCollection(context.getConfig().getEnabledComponents().stream().map(Class::getName).toList(), String.class, null); + } + + + @SuppressWarnings("unchecked") + @ProtectedMethod + @JRPCMethod + public void setEnabledComponents(@JRPCArgument(name = "components") JSONArray components) { + context.getConfig().setEnabledComponents(JSONUtility.deserializeCollection(components, String.class, null).map(s -> { + try { + return (Class>) Class.forName(s); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to load Component class " + s, e); + } + }).collect(Collectors.toList())); + context.initComponents(); + } + + @ProtectedMethod + @JRPCMethod + public JSONArray getAvailableComponents() { + return JSONUtility.serializeCollection(context.getComponentClasses().stream().map(Class::getName).toList(), String.class, null); + } + + +} diff --git a/app/src/main/resources/htdocs/index.html b/app/src/main/resources/htdocs/index.html new file mode 100644 index 0000000..4a4ea78 --- /dev/null +++ b/app/src/main/resources/htdocs/index.html @@ -0,0 +1,10 @@ + + + + + Title + + +test ok + + \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 08fbe3a..3a9be39 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -11,5 +11,24 @@ core + + + ru.kirillius.utils + common-logging + 1.3.0.0 + + + + org.slf4j + slf4j-api + 2.0.9 + + + org.slf4j + slf4j-jdk14 + 2.0.9 + + + \ No newline at end of file diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractPlugin.java b/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java similarity index 70% rename from core/src/main/java/ru/kirillius/pf/sdn/core/AbstractPlugin.java rename to core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java index ab6baca..4bea1a1 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractPlugin.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/AbstractComponent.java @@ -1,11 +1,11 @@ package ru.kirillius.pf.sdn.core; -public abstract class AbstractPlugin implements Plugin { +public abstract class AbstractComponent implements Component { protected final CT config; protected final Context context; @SuppressWarnings({"unchecked", "rawtypes"}) - public AbstractPlugin(Context context) { + public AbstractComponent(Context context) { config = (CT) context.getConfig().getPluginsConfig().getConfig((Class) getClass()); this.context = context; } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java index b3e38ed..da7610d 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthManager.java @@ -3,13 +3,17 @@ package ru.kirillius.pf.sdn.core.Auth; import jakarta.servlet.http.HttpSession; import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Util.HashUtil; +import ru.kirillius.utils.logging.SystemLogger; +import java.io.IOException; import java.util.Objects; +import java.util.UUID; public class AuthManager { - private final Context context; public final static String SESSION_AUTH_KEY = "auth"; public final static String SESSION_TOKEN = "token"; + private final static String CTX = AuthManager.class.getSimpleName(); + private final Context context; public AuthManager(Context context) { this.context = context; @@ -22,22 +26,27 @@ public class AuthManager { public void updatePassword(String pass) { var config = context.getConfig(); + config.setPasswordSalt(UUID.randomUUID().toString()); config.setPasswordHash( HashUtil.hash(pass, config.getPasswordSalt()) ); } public AuthToken createToken(String description) { - var config = context.getConfig(); var token = new AuthToken(); - token.setDescripton(description); - config.getTokens().add(token); - config.update(); + token.setDescription(description); + var tokenStorage = context.getTokenStorage(); + tokenStorage.add(token); + try { + tokenStorage.store(); + } catch (IOException e) { + SystemLogger.error("Failed to save token storage", CTX, e); + } return token; } public boolean validateToken(AuthToken token) { - return context.getConfig().getTokens().contains(token); + return context.getTokenStorage().contains(token); } public void setSessionAuthState(HttpSession session, boolean state) { @@ -57,9 +66,16 @@ public class AuthManager { } public void invalidateToken(AuthToken token) { - var config = context.getConfig(); - config.getTokens().remove(token); - config.update(); + var tokenStorage = context.getTokenStorage(); + if (tokenStorage.contains(token)) { + tokenStorage.remove(token); + try { + tokenStorage.store(); + } catch (IOException e) { + SystemLogger.error("Failed to save token storage", CTX, e); + } + } + } public boolean validateToken(String token) { diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java index 6b992a6..e1bcebb 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/AuthToken.java @@ -19,7 +19,7 @@ public class AuthToken { @Setter @Getter @JSONProperty - private String descripton = "untitled"; + private String description = "untitled"; @Override public boolean equals(Object o) { diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenStorage.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenStorage.java new file mode 100644 index 0000000..1332341 --- /dev/null +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Auth/TokenStorage.java @@ -0,0 +1,64 @@ +package ru.kirillius.pf.sdn.core.Auth; + +import org.json.JSONArray; +import org.json.JSONTokener; +import ru.kirillius.json.JSONUtility; +import ru.kirillius.pf.sdn.core.Context; + +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + + +public class TokenStorage { + private final File file; + + public TokenStorage(Context context) { + file = new File(context.getConfig().getCacheDirectory(), "tokens.json"); + if (file.exists()) { + try (var stream = new FileInputStream(file)) { + var json = new JSONArray(new JSONTokener(stream)); + tokens = JSONUtility.deserializeCollection(json, AuthToken.class, null).collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException("Failed to read token storage", e); + } + } else { + tokens = new ArrayList<>(); + } + } + + private final List tokens; + + public Collection getTokens() { + synchronized (tokens) { + return Collections.unmodifiableList(tokens); + } + } + + public void add(AuthToken... what) { + synchronized (tokens) { + Collections.addAll(tokens, what); + } + } + + public boolean contains(AuthToken what) { + synchronized (tokens) { + return tokens.contains(what); + } + } + + public void remove(AuthToken... what) { + synchronized (tokens) { + Arrays.stream(what).forEach(tokens::remove); + } + } + + public synchronized void store() throws IOException { + try (var fileInputStream = new FileOutputStream(file)) { + try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) { + writer.write(JSONUtility.serializeCollection(tokens, AuthToken.class, null).toString()); + writer.flush(); + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Plugin.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Component.java similarity index 68% rename from core/src/main/java/ru/kirillius/pf/sdn/core/Plugin.java rename to core/src/main/java/ru/kirillius/pf/sdn/core/Component.java index 24b1451..8e5eb41 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Plugin.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Component.java @@ -5,16 +5,16 @@ import lombok.SneakyThrows; import java.io.Closeable; import java.lang.reflect.ParameterizedType; -public interface Plugin extends Closeable { +public interface Component extends Closeable { @SuppressWarnings("unchecked") - static Class getConfigClass(Class> pluginClass) { + static Class getConfigClass(Class> pluginClass) { var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass(); var typeArguments = genericSuperclass.getActualTypeArguments(); return (Class) typeArguments[0]; } @SneakyThrows - static > T loadPlugin(Class pluginClass, Context context) { + static > T loadPlugin(Class pluginClass, Context context) { return pluginClass.getConstructor(Context.class).newInstance(context); } } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java index 23f1419..7ff875d 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java @@ -5,8 +5,10 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.json.JSONObject; import org.json.JSONTokener; -import ru.kirillius.json.*; -import ru.kirillius.pf.sdn.core.Auth.AuthToken; +import ru.kirillius.json.JSONArrayProperty; +import ru.kirillius.json.JSONProperty; +import ru.kirillius.json.JSONSerializable; +import ru.kirillius.json.JSONUtility; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig; @@ -19,6 +21,21 @@ import java.util.UUID; @NoArgsConstructor @JSONSerializable public class Config { + @Getter + @Setter + @JSONProperty + private int updateSubscriptionsInterval = 6; + + @Getter + @Setter + @JSONProperty + private boolean cachingAS = true; + + @Getter + @Setter + @JSONProperty + private int updateASInterval = 12; + @Getter private File loadedConfigFile = null; @Setter @@ -31,11 +48,6 @@ public class Config { @JSONProperty private File cacheDirectory = new File("./.cache"); - @Setter - @Getter - @JSONArrayProperty(type = AuthToken.class) - private List tokens = Collections.emptyList(); - @Setter @Getter @JSONArrayProperty(type = RepositoryConfig.class) @@ -55,7 +67,7 @@ public class Config { @Setter @Getter @JSONArrayProperty(type = Class.class, required = false) - private List>> enabledPlugins = new ArrayList<>(); + private List>> enabledComponents = new ArrayList<>(); @Setter @Getter @@ -92,14 +104,6 @@ public class Config { @JSONProperty private NetworkResourceBundle filteredResources = new NetworkResourceBundle(); - public void update() { - try { - store(this, loadedConfigFile); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - public static void store(Config config, File file) throws IOException { try (var fileInputStream = new FileOutputStream(file)) { try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) { @@ -122,9 +126,14 @@ public class Config { var json = new JSONObject(new JSONTokener(stream)); var config = deserialize(json); config.loadedConfigFile = file; + config.initialJSON = json; return config; } } + private JSONObject initialJSON = new JSONObject(); + public boolean isModified() { + return !initialJSON.equals(serialize(this)); + } } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java index 1a56852..5bb0cf0 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java @@ -1,21 +1,37 @@ package ru.kirillius.pf.sdn.core; -import org.eclipse.jetty.server.Server; import ru.kirillius.pf.sdn.core.Auth.AuthManager; +import ru.kirillius.pf.sdn.core.Auth.TokenStorage; import ru.kirillius.pf.sdn.core.Networking.ASInfoService; import ru.kirillius.pf.sdn.core.Networking.NetworkManager; import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager; +import java.util.Collection; + public interface Context { Config getConfig(); AuthManager getAuthManager(); - Server getServer(); - ASInfoService getASInfoService(); NetworkManager getNetworkManager(); + ContextEventsHandler getEventsHandler(); + SubscriptionManager getSubscriptionManager(); + + UpdateManager getUpdateManager(); + + Component getPluginInstance(Class> pluginClass); + + void triggerRestart(); + + void triggerShutdown(); + + TokenStorage getTokenStorage(); + + void initComponents(); + + Collection>> getComponentClasses(); } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java index 3d5f917..9bd0586 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java @@ -3,6 +3,7 @@ package ru.kirillius.pf.sdn.core; import lombok.Getter; import ru.kirillius.java.utils.events.ConcurrentEventHandler; import ru.kirillius.java.utils.events.EventHandler; +import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; public final class ContextEventsHandler { @@ -10,4 +11,6 @@ public final class ContextEventsHandler { private final EventHandler networkManagerUpdateEvent = new ConcurrentEventHandler<>(); @Getter private final EventHandler subscriptionsUpdateEvent = new ConcurrentEventHandler<>(); + @Getter + private final EventHandler RPCInitEvent = new ConcurrentEventHandler<>(); } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java index fef2417..0f25405 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkManager.java @@ -1,28 +1,42 @@ package ru.kirillius.pf.sdn.core.Networking; import lombok.Getter; +import org.json.JSONObject; +import org.json.JSONTokener; +import ru.kirillius.json.JSONUtility; import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Util.IPv4Util; import ru.kirillius.utils.logging.SystemLogger; -import java.io.Closeable; -import java.io.IOException; +import java.io.*; import java.util.*; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public class NetworkManager implements Closeable { private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final static String CTX = NetworkManager.class.getSimpleName(); private final Context context; + private final File cacheFile; public NetworkManager(Context context) { this.context = context; + cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json"); + if (cacheFile.exists() && context.getConfig().isCachingAS()) { + SystemLogger.message("Loading as cache file", CTX); + try (var is = new FileInputStream(cacheFile)) { + var json = new JSONObject(new JSONTokener(is)); + json.keySet().forEach(key -> { + var as = Integer.parseInt(key); + prefixCache.put(as, JSONUtility.deserializeCollection(json.getJSONArray(key), IPv4Subnet.class, null).toList()); + }); + } catch (Exception e) { + SystemLogger.error("Failed to load as cache file " + cacheFile.getPath(), CTX, e); + } + } } private final AtomicReference> updateProcess = new AtomicReference<>(); - private final AtomicBoolean cacheInvalid = new AtomicBoolean(true); @Getter private final NetworkResourceBundle inputResources = new NetworkResourceBundle(); @Getter @@ -35,7 +49,7 @@ public class NetworkManager implements Closeable { private final Map> prefixCache = new ConcurrentHashMap<>(); - public void triggerUpdate() { + public void triggerUpdate(boolean ignoreCache) { if (isUpdatingNow()) { return; } @@ -48,8 +62,23 @@ public class NetworkManager implements Closeable { var asn = new ArrayList<>(inputResources.getASN()); asn.removeAll(filteredResources.getASN()); - if (cacheInvalid.get()) { - fetchPrefixes(asn); + var asnToFetch = new ArrayList<>(asn); + if (!ignoreCache) { + asnToFetch.removeAll(prefixCache.keySet()); + } + + fetchPrefixes(asnToFetch); + + if (config.isCachingAS()) { + try (var os = new FileOutputStream(cacheFile)) { + var json = new JSONObject(); + prefixCache.forEach((key, asnList) -> { + json.put(String.valueOf(key), JSONUtility.serializeCollection(asnList, IPv4Subnet.class, null)); + }); + os.write(json.toString().getBytes()); + } catch (IOException e) { + SystemLogger.error("Unable to write file " + cacheFile.getPath(), CTX, e); + } } var subnets = new HashSet<>(inputResources.getSubnets()); @@ -100,14 +129,10 @@ public class NetworkManager implements Closeable { } - public void invalidateCache() { - cacheInvalid.set(true); - } - private void fetchPrefixes(List systems) { + var service = context.getASInfoService(); systems.forEach(as -> { SystemLogger.message("Fetching AS" + as + " prefixes...", CTX); - var service = context.getASInfoService(); var future = service.getPrefixes(as); while (!future.isDone() && !future.isCancelled()) { diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java b/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java index d2e410b..b8e7db1 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/PluginConfigStorage.java @@ -36,7 +36,7 @@ public class PluginConfigStorage { try { var pluginClass = loader.loadClass(key); var value = json.getJSONObject(key); - var configClass = Plugin.getConfigClass((Class) pluginClass); + var configClass = Component.getConfigClass((Class) pluginClass); storage.configs.put((Class)pluginClass, JSONUtility.deserializeStructure(value, configClass)); } catch (ClassNotFoundException e) { throw new RuntimeException(e); @@ -46,13 +46,13 @@ public class PluginConfigStorage { } } - private final Map>, Object> configs = new ConcurrentHashMap<>(); + private final Map>, Object> configs = new ConcurrentHashMap<>(); @SuppressWarnings("unchecked") @SneakyThrows - public CT getConfig(Class> pluginClass) { + public CT getConfig(Class> pluginClass) { if (!configs.containsKey(pluginClass)) { - var configClass = Plugin.getConfigClass(pluginClass); + var configClass = Component.getConfigClass(pluginClass); var instance = configClass.getConstructor().newInstance(); configs.put(pluginClass, instance); } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java index b6f00de..ec76156 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Subscription/SubscriptionManager.java @@ -3,9 +3,11 @@ package ru.kirillius.pf.sdn.core.Subscription; import lombok.Getter; import ru.kirillius.pf.sdn.core.Context; import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; +import ru.kirillius.utils.logging.SystemLogger; import java.io.Closeable; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -34,12 +36,17 @@ public class SubscriptionManager implements Closeable { return future != null && !future.isDone() && !future.isCancelled(); } + @Getter + private final Map availableResources = new ConcurrentHashMap<>(); + public synchronized void triggerUpdate() { if (isUpdatingNow()) { return; } updateProcess.set(executor.submit(() -> { + var available = new HashMap(); + var bundle = new NetworkResourceBundle(); var config = context.getConfig(); @@ -61,20 +68,24 @@ public class SubscriptionManager implements Closeable { if (subscribedResources.contains(resourceName)) { bundle.add(resources.get(key)); } + available.put(resourceName, resources.get(key)); }); } - + availableResources.clear(); + availableResources.putAll(available); outputResources.clear(); outputResources.add(bundle); try { context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources); } catch (Exception e) { - throw new RuntimeException(e); //FIXME to LOG + SystemLogger.error("Error on update event", CTX, e); } })); } + private final static String CTX = SubscriptionManager.class.getSimpleName(); + @Override public void close() throws IOException { diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/UpdateManager.java b/core/src/main/java/ru/kirillius/pf/sdn/core/UpdateManager.java new file mode 100644 index 0000000..ad2cc53 --- /dev/null +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/UpdateManager.java @@ -0,0 +1,60 @@ +package ru.kirillius.pf.sdn.core; + +import lombok.SneakyThrows; +import ru.kirillius.pf.sdn.core.Util.Wait; +import ru.kirillius.utils.logging.SystemLogger; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Duration; + +public class UpdateManager implements Closeable { + private final Thread updateThread; + + public UpdateManager(Context context) { + this.context = context; + updateThread = new Thread(new ThreadWorker()); + } + + public void start() { + updateThread.start(); + } + + private final Context context; + + @Override + public void close() throws IOException { + updateThread.interrupt(); + } + + private final static String CTX = UpdateManager.class.getSimpleName(); + + private class ThreadWorker implements Runnable { + + @SneakyThrows + @Override + public void run() { + var uptime = 0L; + var config = context.getConfig(); + while (!Thread.currentThread().isInterrupted()) { + Thread.sleep(Duration.ofMinutes(1)); + uptime++; + + + if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval()*60L) == 0) { + SystemLogger.message("Updating subscriptions", CTX); + var subscriptionManager = context.getSubscriptionManager(); + subscriptionManager.triggerUpdate(); + Wait.until(subscriptionManager::isUpdatingNow); + Wait.when(subscriptionManager::isUpdatingNow); + } + + if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) { + SystemLogger.message("Updating cached AS", CTX); + var networkManager = context.getNetworkManager(); + networkManager.triggerUpdate(true); + } + } + } + } +} diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java index f34113a..37418b6 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/IPv4Util.java @@ -57,6 +57,14 @@ public class IPv4Util { return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF); } + public static String maskToString(long maskLong) { + return String.format("%d.%d.%d.%d", + (maskLong >> 24) & 0xff, + (maskLong >> 16) & 0xff, + (maskLong >> 8) & 0xff, + maskLong & 0xff); + } + public interface SummarisationResult { List getResult(); @@ -97,7 +105,7 @@ public class IPv4Util { } if (parent.overlaps(subnet)) { overlapped.add(subnet); - if(source.contains(subnet)) { + if (source.contains(subnet)) { mergedSubnets.add(subnet); } } @@ -135,10 +143,10 @@ public class IPv4Util { result.remove(subnet); result.remove(next); result.add(largerSubnet); - if(source.contains(subnet)) { + if (source.contains(subnet)) { mergedSubnets.add(subnet); } - if(source.contains(next)) { + if (source.contains(next)) { mergedSubnets.add(next); } i++; @@ -215,7 +223,7 @@ public class IPv4Util { if (100.0 * used.get() / candidate.count() >= usePercentage) { //подсеть подходит под критерий overlapped.forEach(subnet -> { - if(source.contains(subnet)) { + if (source.contains(subnet)) { mergedSubnets.add(subnet); } }); diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java new file mode 100644 index 0000000..414faec --- /dev/null +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/Wait.java @@ -0,0 +1,32 @@ +package ru.kirillius.pf.sdn.core.Util; + +import java.time.Duration; +import java.util.function.Supplier; + +public final class Wait { + private Wait() { + throw new AssertionError(); + } + + public static void until(Supplier condition) throws InterruptedException { + if (condition == null) { + return; + } + + while (!condition.get() && !Thread.currentThread().isInterrupted()) { + Thread.sleep(Duration.ofSeconds(1)); + } + } + + public static void when(Supplier condition) throws InterruptedException { + if (condition == null) { + return; + } + + while (condition.get() && !Thread.currentThread().isInterrupted()) { + Thread.sleep(Duration.ofSeconds(1)); + } + } + + +} diff --git a/ovpn-connector/pom.xml b/ovpn-connector/pom.xml new file mode 100644 index 0000000..3a04069 --- /dev/null +++ b/ovpn-connector/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + ru.kirillius + pf-sdn + 0.1.0.0 + + + ovpn-connector + + + 24 + 24 + UTF-8 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 1.4 + + + package + + shade + + + + + + + *:* + + module-info.class + JDOMAbout*class + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + ru.kirillius.pf.sdn.External.API.ovpn.connector.App + + + + true + shaded + + + + + + + + \ No newline at end of file diff --git a/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java new file mode 100644 index 0000000..6d7af1d --- /dev/null +++ b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java @@ -0,0 +1,90 @@ +package ru.kirillius.pf.sdn.External.API.ovpn.connector; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import ru.kirillius.json.JSONUtility; + +import java.io.*; +import java.net.ConnectException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +public class App { + private static Config loadConfig() { + var configFile = new File("ovpn-connector.json"); + if (!configFile.exists()) { + throw new RuntimeException("Config file " + configFile.getAbsolutePath() + " does not exist"); + } + try (var stream = new FileInputStream(configFile)) { + return JSONUtility.deserializeStructure(new JSONObject(new JSONTokener(stream)), Config.class); + } catch (IOException e) { + throw new RuntimeException("Failed to load config file", e); + } + } + + private static void pushRoutes(JSONArray routes, File pushFile) { + try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pushFile)))) { + for (var i = 0; i < routes.length(); i++) { + var entry = routes.getJSONObject(i); + var route = "push \"route " + entry.getString("address") + " " + entry.getString("mask") + "\"\n"; + writer.write(route); + } + writer.flush(); + } catch (IOException ex) { + throw new RuntimeException("Failed to write PUSH file", ex); + } + } + + public static void main(String[] args) { + try { + var config = loadConfig(); + + if (args.length < 1) { + throw new RuntimeException("OVPN Push file is not specified in ARGS"); + } + var pushFile = new File(args[0]); + var routes = sendRequest(config); + pushRoutes(routes, pushFile); + System.exit(0); + } catch (Exception e) { + e.printStackTrace(System.err); + System.exit(1); + } + } + + private static JSONArray sendRequest(Config config) throws IOException, InterruptedException { + var json = new JSONObject(); + json.put("jsonrpc", "2.0"); + json.put("method", "ru.kirillius.pf.sdn.External.API.OVPNPlugin::getManagedRoutes"); + json.put("params", new JSONObject()); + json.put("id", 1); + + + try (var httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build()) { + // Создаем HTTP запрос + var request = HttpRequest.newBuilder() + .uri(URI.create(config.getHost() + "/jsonrpc/rpc")) + .header("Content-Type", "application/json") + .header("X-Auth-token", config.getToken()) + .POST(HttpRequest.BodyPublishers.ofString(json.toString())) + .timeout(Duration.ofSeconds(30)) + .build(); + + // Отправляем запрос и получаем ответ + var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + // Проверяем статус ответа + if (response.statusCode() != 200) { + throw new RuntimeException("HTTP error code: " + response.statusCode() + " body: " + response.body()); + } + + return new JSONArray(new JSONObject(response.body()).getJSONArray("result")); + } catch (ConnectException e) { + throw new RuntimeException("Unable to connect to remote server", e); + } + } +} diff --git a/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java new file mode 100644 index 0000000..1ed55ed --- /dev/null +++ b/ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java @@ -0,0 +1,16 @@ +package ru.kirillius.pf.sdn.External.API.ovpn.connector; + +import lombok.Getter; +import lombok.Setter; +import ru.kirillius.json.JSONProperty; +import ru.kirillius.json.JSONSerializable; + +@JSONSerializable +@Getter +@Setter +public class Config { + @JSONProperty + private String token = "NO_TOKEN_IS_SET"; + @JSONProperty + private String host = "http://127.0.0.1:8080"; +} diff --git a/pom.xml b/pom.xml index 80a6856..6874599 100644 --- a/pom.xml +++ b/pom.xml @@ -9,10 +9,11 @@ 0.1.0.0 pom - web-client core - web-server + app + ovpn-connector + @@ -21,6 +22,30 @@ UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.projectlombok + lombok + 1.18.40 + + + + + + + + + @@ -53,46 +78,6 @@ test - - org.javatuples - javatuples - 1.2 - - - com.cronutils - cron-utils - 9.2.0 - - - - - ru.kirillius - json-rpc-servlet - 2.1.4.0 - - - ru.kirillius.utils - common-logging - 1.3.0.0 - - - - org.slf4j - slf4j-api - 2.0.9 - - - org.slf4j - slf4j-jdk14 - 2.0.9 - - - - - org.eclipse.jetty - jetty-server - 12.0.12 - @@ -102,6 +87,13 @@ provided + + + ru.kirillius + json-rpc-servlet + 2.1.5.0 + + \ No newline at end of file diff --git a/web-client/pom.xml b/web-client/pom.xml deleted file mode 100644 index 3da5878..0000000 --- a/web-client/pom.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - 4.0.0 - - ru.kirillius - pf-sdn - 0.1.0.0 - - - web-client - - - - \ No newline at end of file diff --git a/web-server/pom.xml b/web-server/pom.xml deleted file mode 100644 index 66ec4c9..0000000 --- a/web-server/pom.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - 4.0.0 - - ru.kirillius - pf-sdn - 0.1.0.0 - - - web-server - - - ru.kirillius - core - 0.1.0.0 - compile - - - - - \ No newline at end of file diff --git a/web-server/src/main/java/ru/kirillius/pf/sdn/web/RPC/AuthRPC.java b/web-server/src/main/java/ru/kirillius/pf/sdn/web/RPC/AuthRPC.java deleted file mode 100644 index 49e81be..0000000 --- a/web-server/src/main/java/ru/kirillius/pf/sdn/web/RPC/AuthRPC.java +++ /dev/null @@ -1,52 +0,0 @@ -package ru.kirillius.pf.sdn.web.RPC; - -import ru.kirillius.json.rpc.Annotations.JRPCArgument; -import ru.kirillius.json.rpc.Annotations.JRPCContext; -import ru.kirillius.json.rpc.Annotations.JRPCMethod; -import ru.kirillius.json.rpc.Servlet.CallContext; -import ru.kirillius.pf.sdn.core.Auth.AuthToken; -import ru.kirillius.pf.sdn.core.Context; -import ru.kirillius.pf.sdn.web.ProtectedMethod; - -public class AuthRPC implements RPC { - private final Context context; - - public AuthRPC(Context context) { - this.context = context; - } - - @ProtectedMethod - @JRPCMethod - public AuthToken getAuthToken(@JRPCContext CallContext call) { - return context.getAuthManager().getSessionToken(call.getSession()); - } - - @ProtectedMethod - @JRPCMethod - public AuthToken rememberCurrentUser(@JRPCContext CallContext call) { - var UA = call.getRequest().getHeader("User-Agent"); - if (UA == null) { - UA = "Unknown user agent"; - } - var authManager = context.getAuthManager(); - var token = authManager.createToken(UA); - authManager.setSessionToken(call.getSession(), token); - return token; - } - - @JRPCMethod - public boolean auth(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) { - var authManager = context.getAuthManager(); - if (authManager.validatePassword(password)) { - authManager.setSessionAuthState(call.getSession(), true); - } - return false; - } - - - @JRPCMethod - public void logout(@JRPCContext CallContext call) { - var authManager = context.getAuthManager(); - authManager.setSessionAuthState(call.getSession(), false); - } -} diff --git a/webui/.gitignore b/webui/.gitignore new file mode 100644 index 0000000..8ee54e8 --- /dev/null +++ b/webui/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/webui/README.md b/webui/README.md new file mode 100644 index 0000000..d3feed8 --- /dev/null +++ b/webui/README.md @@ -0,0 +1,38 @@ +# webui-vue + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` diff --git a/webui/index.html b/webui/index.html new file mode 100644 index 0000000..19729ae --- /dev/null +++ b/webui/index.html @@ -0,0 +1,18 @@ + + + + + + + SDN Control + + +
+
+ Загрузка... +
+
+ + + + \ No newline at end of file diff --git a/webui/jsconfig.json b/webui/jsconfig.json new file mode 100644 index 0000000..5a1f2d2 --- /dev/null +++ b/webui/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/webui/package-lock.json b/webui/package-lock.json new file mode 100644 index 0000000..1c75aca --- /dev/null +++ b/webui/package-lock.json @@ -0,0 +1,3424 @@ +{ + "name": "webui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webui", + "version": "0.0.0", + "dependencies": { + "jquery": "^3.7.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "sass": "^1.93.2", + "vite": "^7.1.7", + "vite-plugin-vue-devtools": "^8.0.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", + "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", + "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/shared": "3.5.22", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz", + "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz", + "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/compiler-core": "3.5.22", + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.19", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz", + "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.2.tgz", + "integrity": "sha512-V7eKTTHoS6KfK8PSGMLZMhGv/9yNDrmv6Qc3r71QILulnzPnqK2frsTyx3e2MrhdUZnENPEm6hcb4z0GZOqNhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.2", + "@vue/devtools-shared": "^8.0.2", + "mitt": "^3.0.1", + "nanoid": "^5.1.5", + "pathe": "^2.0.3", + "vite-hot-client": "^2.1.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.2.tgz", + "integrity": "sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.2", + "birpc": "^2.5.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.2.tgz", + "integrity": "sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz", + "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz", + "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz", + "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/runtime-core": "3.5.22", + "@vue/shared": "3.5.22", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz", + "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "vue": "3.5.22" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz", + "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", + "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/birpc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz", + "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.227", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", + "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sass": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.0.tgz", + "integrity": "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-utils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.2.tgz", + "integrity": "sha512-1069qvMBcyAu3yXQlvYrkwoyLOk0lSSR/gTKy/vy+Det7TXnouGei6ZcKwr5TIe938v/14oLlp0ow6FSJkkORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.0.2", + "@vue/devtools-kit": "^8.0.2", + "@vue/devtools-shared": "^8.0.2", + "execa": "^9.6.0", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^5.3.2" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz", + "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vue": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", + "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-sfc": "3.5.22", + "@vue/runtime-dom": "3.5.22", + "@vue/server-renderer": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/webui/package.json b/webui/package.json new file mode 100644 index 0000000..9f50c12 --- /dev/null +++ b/webui/package.json @@ -0,0 +1,23 @@ +{ + "name": "webui", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "jquery": "^3.7.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "sass": "^1.93.2", + "vite": "^7.1.7", + "vite-plugin-vue-devtools": "^8.0.2" + } +} diff --git a/webui/public/favicon.ico b/webui/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/webui/public/favicon.ico differ diff --git a/webui/src/json-rpc.js b/webui/src/json-rpc.js new file mode 100644 index 0000000..f0cae5a --- /dev/null +++ b/webui/src/json-rpc.js @@ -0,0 +1,167 @@ +export const JSONRPC = { + url: "/jsonrpc/rpc", + __id: 1, + /** + * + * @param method + * @param params + * @returns Object + * @private + */ + __invoke: async function (method, params) { + const request = await JSONRPC.__performRequest(method, params); + + if (!request.success) { + console.error(request.result); + throw new Error("Failed to invoke method " + method + " with params " + JSON.stringify(params)); + } + + return request.result; + }, + __performRequest: async function (method, params) { + const __this = this; + const resp = await fetch( + __this.url, + { + method: "POST", + mode: "cors", + cache: "no-cache", + credentials: "include", + headers: { + "Content-Type": "application/json" + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: JSON.stringify({ + jsonrpc: '2.0', + method: method, + params: params, + id: __this.__id++ + }) + } + ); + const success = resp.status === 200; + const result = (success ? (await resp.json()).result : { + "error": true, + "code": resp.status, + "status": resp.statusText, + "body": await resp.text() + }); + + return { + "result": result, + "success": success + }; + } +}; + +export const TYPES = {}; +JSONRPC.SubscriptionManager = {}; + +JSONRPC.SubscriptionManager.getSubscribedResources=async function(){ + return await JSONRPC.__invoke("web.RPC.SubscriptionManager::getSubscribedResources", {}); +}; + +JSONRPC.SubscriptionManager.getAvailableResources=async function(){ + return await JSONRPC.__invoke("web.RPC.SubscriptionManager::getAvailableResources", {}); +}; + +JSONRPC.SubscriptionManager.setSubscribedResources=async function(resources){ + return await JSONRPC.__invoke("web.RPC.SubscriptionManager::setSubscribedResources", {"resources":resources}); +}; + +JSONRPC.SubscriptionManager.isUpdating=async function(){ + return await JSONRPC.__invoke("web.RPC.SubscriptionManager::isUpdating", {}); +}; + +JSONRPC.SubscriptionManager.triggerUpdate=async function(){ + return await JSONRPC.__invoke("web.RPC.SubscriptionManager::triggerUpdate", {}); +}; +JSONRPC.Auth = {}; + +JSONRPC.Auth.createToken=async function(){ + return await JSONRPC.__invoke("web.RPC.Auth::createToken", {}); +}; + +JSONRPC.Auth.listTokens=async function(){ + return await JSONRPC.__invoke("web.RPC.Auth::listTokens", {}); +}; + +JSONRPC.Auth.removeToken=async function(token){ + return await JSONRPC.__invoke("web.RPC.Auth::removeToken", {"token":token}); +}; + +JSONRPC.Auth.createAPIToken=async function(description){ + return await JSONRPC.__invoke("web.RPC.Auth::createAPIToken", {"description":description}); +}; + +JSONRPC.Auth.startSessionByPassword=async function(password){ + return await JSONRPC.__invoke("web.RPC.Auth::startSessionByPassword", {"password":password}); +}; + +JSONRPC.Auth.startSessionByToken=async function(token){ + return await JSONRPC.__invoke("web.RPC.Auth::startSessionByToken", {"token":token}); +}; + +JSONRPC.Auth.isAuthenticated=async function(){ + return await JSONRPC.__invoke("web.RPC.Auth::isAuthenticated", {}); +}; + +JSONRPC.Auth.logout=async function(){ + return await JSONRPC.__invoke("web.RPC.Auth::logout", {}); +}; +JSONRPC.System = {}; + +JSONRPC.System.shutdown=async function(){ + return await JSONRPC.__invoke("web.RPC.System::shutdown", {}); +}; + +JSONRPC.System.getEnabledComponents=async function(){ + return await JSONRPC.__invoke("web.RPC.System::getEnabledComponents", {}); +}; + +JSONRPC.System.setEnabledComponents=async function(components){ + return await JSONRPC.__invoke("web.RPC.System::setEnabledComponents", {"components":components}); +}; + +JSONRPC.System.isConfigChanged=async function(){ + return await JSONRPC.__invoke("web.RPC.System::isConfigChanged", {}); +}; + +JSONRPC.System.hasUpdates=async function(){ + return await JSONRPC.__invoke("web.RPC.System::hasUpdates", {}); +}; + +JSONRPC.System.getAvailableComponents=async function(){ + return await JSONRPC.__invoke("web.RPC.System::getAvailableComponents", {}); +}; + +JSONRPC.System.getConfig=async function(){ + return await JSONRPC.__invoke("web.RPC.System::getConfig", {}); +}; + +JSONRPC.System.restart=async function(){ + return await JSONRPC.__invoke("web.RPC.System::restart", {}); +}; +JSONRPC.NetworkManager = {}; + +JSONRPC.NetworkManager.getOutputResources=async function(){ + return await JSONRPC.__invoke("web.RPC.NetworkManager::getOutputResources", {}); +}; + +JSONRPC.NetworkManager.isUpdating=async function(){ + return await JSONRPC.__invoke("web.RPC.NetworkManager::isUpdating", {}); +}; + +JSONRPC.NetworkManager.triggerUpdate=async function(ignoreCache){ + return await JSONRPC.__invoke("web.RPC.NetworkManager::triggerUpdate", {"ignoreCache":ignoreCache}); +}; +JSONRPC.OVPN = {}; + +JSONRPC.OVPN.restartSystemService=async function(){ + return await JSONRPC.__invoke("ru.kirillius.pf.sdn.External.API.Components.OVPN::restartSystemService", {}); +}; + +JSONRPC.OVPN.getManagedRoutes=async function(){ + return await JSONRPC.__invoke("ru.kirillius.pf.sdn.External.API.Components.OVPN::getManagedRoutes", {}); +}; diff --git a/webui/src/main.js b/webui/src/main.js new file mode 100644 index 0000000..4dbf994 --- /dev/null +++ b/webui/src/main.js @@ -0,0 +1,20 @@ +// src/main.js + +import './styles/app.css'; +import $ from 'jquery'; +window.$ = window.jQuery = $; + +// 1. Импорт JSONRPC по алиасу (предполагаем, что алиас настроен в vite.config.js) +import { JSONRPC } from '@/json-rpc.js'; + +// 🔥 Указываем URL сервера, как ты и сделал +JSONRPC.url = "http://localhost:8080" + JSONRPC.url; + +// 2. Импорт главного модуля приложения +import { initApp } from './modules/app.js'; + +// Запуск приложения после загрузки DOM +$(document).ready(() => { + $('#loader').remove(); + initApp(); +}); \ No newline at end of file diff --git a/webui/src/modules/app.js b/webui/src/modules/app.js new file mode 100644 index 0000000..4a2eebe --- /dev/null +++ b/webui/src/modules/app.js @@ -0,0 +1,23 @@ +import { renderLoginForm, renderDashboard } from './ui.js'; +import { checkAuthOnStartup } from './auth.js'; // 🔥 Теперь импортируем здесь + +// Глобальный статус приложения +export let isAuthenticated = false; + +// Главная функция инициализации +export async function initApp() { + // 1. Проверяем авторизацию + const isInitialAuth = await checkAuthOnStartup(); + setAuthenticated(isInitialAuth); // Устанавливаем состояние и рендерим +} + +// Функция для смены состояния (например, после успешного логина/выхода) +export function setAuthenticated(status) { + isAuthenticated = status; + + if (status) { + renderDashboard(); + } else { + renderLoginForm(); + } +} \ No newline at end of file diff --git a/webui/src/modules/auth.js b/webui/src/modules/auth.js new file mode 100644 index 0000000..02f1d48 --- /dev/null +++ b/webui/src/modules/auth.js @@ -0,0 +1,95 @@ +import { getAuthToken, setAuthToken, removeAuthToken } from '../utils/cookies.js'; + +// ⚠️ ОБНОВИ ПУТЬ! Этот импорт должен указывать на твой модуль для работы с RPC. +// Предполагается, что JSONRPC.Auth содержит методы: +// - isAuthenticated() +// - startSessionByToken(token) +// - startSessionByPassword(password) +// - createToken() +// - logout() +import { JSONRPC } from '@/json-rpc.js'; + + +/** + * @private + * Проверяет, активна ли сессия (через API) или сохранен ли токен в cookies. + * @returns {Promise} + */ +export async function checkAuthOnStartup() { + try { + // 1. Проверяем активную сессию (самый надежный способ) + const isAuthenticated = await JSONRPC.Auth.isAuthenticated(); + if (isAuthenticated) { + return true; + } + + // 2. Сессии нет, ищем токен в Cookie + const token = getAuthToken(); + if (token) { + console.log("Токен найден, пытаемся стартовать сессию..."); + // 3. Пытаемся стартовать сессию по токену + const success = await JSONRPC.Auth.startSessionByToken(token); + if (success) { + console.log("Сессия успешно восстановлена по токену."); + return true; + } else { + // Токен недействителен или просрочен + console.warn("Токен недействителен, удаляем его."); + removeAuthToken(); + return false; + } + } + + // 4. Нет ни сессии, ни токена + return false; + + } catch (err) { + console.error('Ошибка при стартовой проверке аутентификации:', err); + // В случае ошибки сети/бэкенда, предполагаем, что не авторизованы + return false; + } +} + +/** + * @public + * Обрабатывает вход по паролю. + * @param {string} password - Пароль пользователя. + * @param {boolean} rememberMe - Флаг, указывающий, нужно ли сохранить токен. + * @returns {Promise} - true, если вход успешен. + */ +export async function handleLogin(password, rememberMe) { + // 1. Пытаемся стартовать сессию по паролю + const success = await JSONRPC.Auth.startSessionByPassword(password); + + if (success) { + if (rememberMe) { + // 2. Успех: если "запомнить меня", создаем и сохраняем токен + const token = await JSONRPC.Auth.createToken(); + setAuthToken(token); + console.log("Токен создан и сохранен."); + } + return true; + } + + return false; +} + + +/** + * @public + * Обрабатывает выход из системы. + */ +export async function handleLogout() { + try { + // 1. Завершаем сессию на стороне сервера + await JSONRPC.Auth.logout(); + } catch(e) { + console.warn("Ошибка при завершении сессии на сервере (возможно, уже была неактивна).", e); + } + + // 2. Удаляем токен из Cookie + removeAuthToken(); + console.log("Выход выполнен, токен удален."); + + // Обработчик в ui.js сменит состояние приложения +} \ No newline at end of file diff --git a/webui/src/modules/router.js b/webui/src/modules/router.js new file mode 100644 index 0000000..28e6569 --- /dev/null +++ b/webui/src/modules/router.js @@ -0,0 +1,104 @@ +// src/modules/router.js + +import $ from 'jquery'; +// 🔥 Импортируем только объект StatisticsPage +import { StatisticsPage } from '../pages/Statistics.js'; +import { Subscriptions } from '../pages/Subscriptions.js'; +import { Components } from '../pages/Components.js'; +import { APITokens } from '../pages/APITokens.js'; + +// Переменная для отслеживания текущего активного хеша (для корректного unmount) +let currentRouteHash = ''; + + +// 1. Определение меню и путей +const menuItems = [ + { label: 'Статистика', path: 'stats' }, + { label: 'Подписки', path: 'subscriptions' }, + { label: 'Настройки', path: 'settings' }, + { label: 'Компоненты', path: 'components' }, + { label: 'API', path: 'api' }, +]; + +// 2. Определение страниц +const routes = { + '#stats': { + render: StatisticsPage.render, + mount: StatisticsPage.mount, + // 🔥 Используем unmount из объекта страницы + unmount: StatisticsPage.unmount + }, + '#subscriptions': { + render: Subscriptions.render, + mount: Subscriptions.mount, + unmount: Subscriptions.unmount + }, + '#settings': { + render: () => '

Системные Настройки

Конфигурация сети, пользователя и безопасности.

', + mount: () => {}, + unmount: () => {} + }, + '#components': { + render: Components.render, + mount: Components.mount, + unmount: Components.unmount + }, + '#api': { + render: APITokens.render, + mount: APITokens.mount, + unmount: APITokens.unmount + } +}; + +// 3. Функция рендеринга страницы +export function renderPage(hash) { + const $contentArea = $('#content-area'); + const key = hash.startsWith('#') ? hash : '#' + hash; + + // 🔥 НОВАЯ ПРОВЕРКА: Если страница уже открыта, просто обновляем меню и выходим + if (currentRouteHash === key) { + $('.menu-item').removeClass('active'); + $(`.menu-item[data-path="${key.substring(1)}"]`).addClass('active'); + return; + } + + // Определяем, какой хеш был активным до этого. Используем 'stats' как дефолт + const previousKey = currentRouteHash || '#stats'; + + // Шаг 1: Если мы меняем страницу, и предыдущая страница имеет unmount, вызываем его + if (previousKey !== key && routes[previousKey] && routes[previousKey].unmount) { + routes[previousKey].unmount(); + } + + if (routes[key]) { + // Рендерим HTML + $contentArea.html(routes[key].render()); + + // Вызываем функцию монтирования + routes[key].mount(); + + // Обновляем активный пункт меню + $('.menu-item').removeClass('active'); + $(`.menu-item[data-path="${key.substring(1)}"]`).addClass('active'); + + // Обновляем hash в адресной строке + if (window.location.hash !== key) { + history.pushState(null, null, key); + } + + // Шаг 2: Успешно обновили страницу, сохраняем новый хеш + currentRouteHash = key; + + } else { + // Если путь не найден, перенаправляем на "Статистику" + window.location.hash = 'stats'; + } +} + +// 4. Обработчик изменения хеша в браузере (для навигации по истории) +$(window).on('hashchange', function() { + renderPage(window.location.hash); +}); + +// Экспорт menuItems для построения сайдбара в ui.js +export { menuItems }; \ No newline at end of file diff --git a/webui/src/modules/ui.js b/webui/src/modules/ui.js new file mode 100644 index 0000000..879c7de --- /dev/null +++ b/webui/src/modules/ui.js @@ -0,0 +1,103 @@ +import $ from 'jquery'; +import { handleLogin, handleLogout } from './auth.js'; +import { setAuthenticated } from './app.js'; +import { renderPage, menuItems } from './router.js'; + +// Функция рендеринга формы авторизации +export function renderLoginForm() { + const html = ` +
+
+

Авторизация

+
+
+ + +
+ +
+ + +
+ + + +
+
+
+ `; + + $('#app').html(html); + + // Прикрепление событий jQuery + $('#login-form').on('submit', async function(e) { + e.preventDefault(); + + const $button = $('#login-button'); + const $error = $('#login-error'); + const password = $('#password-input').val(); + const rememberMe = $('#remember-me').prop('checked'); + + $error.hide().text(''); + $button.prop('disabled', true).text('Вход...'); + + try { + const success = await handleLogin(password, rememberMe); + if (success) { + setAuthenticated(true); + } else { + $error.text('Ошибка входа: Неверный пароль.').show(); + } + } catch (err) { + $error.text('Произошла ошибка при попытке входа.').show(); + } finally { + $button.prop('disabled', false).text('Войти'); + } + }); +} + + +// Функция рендеринга рабочего стола (Dashboard) +export function renderDashboard() { + // Используем menuItems из router.js для построения сайдбара + const sidebarHtml = menuItems.map(item => { + return `${item.label}`; + }).join(''); + + const html = ` +
+ + +
+
+
+ `; + + $('#app').html(html); + + // Прикрепляем события для выхода + $('#logout-btn').on('click', async function(e) { + e.preventDefault(); + await handleLogout(); + setAuthenticated(false); + }); + + // Запускаем роутер с текущим хешем (или 'stats' по умолчанию) + renderPage(window.location.hash || '#stats'); + + // Прикрепляем события для меню (роутер) + $('#main-nav').on('click', '.menu-item', function(e) { + // Предотвращаем стандартный переход, чтобы обработать его через JS + e.preventDefault(); + + const path = $(this).data('path'); + if (path) { + window.location.hash = path; + renderPage(path); + } + }); +} \ No newline at end of file diff --git a/webui/src/pages/APITokens.js b/webui/src/pages/APITokens.js new file mode 100644 index 0000000..f83613b --- /dev/null +++ b/webui/src/pages/APITokens.js @@ -0,0 +1,5 @@ +// src/pages/APITokens.js + +import { APITokensPage } from '../ui/api_tokens.js'; + +export const APITokens = APITokensPage; \ No newline at end of file diff --git a/webui/src/pages/Components.js b/webui/src/pages/Components.js new file mode 100644 index 0000000..5eba55e --- /dev/null +++ b/webui/src/pages/Components.js @@ -0,0 +1,5 @@ +// src/pages/Components.js + +import { ComponentsPage } from '../ui/components.js'; + +export const Components = ComponentsPage; \ No newline at end of file diff --git a/webui/src/pages/Statistics.js b/webui/src/pages/Statistics.js new file mode 100644 index 0000000..cb14b39 --- /dev/null +++ b/webui/src/pages/Statistics.js @@ -0,0 +1,22 @@ +// src/pages/Statistics.js + +import { renderStatisticsPage, stopPolling } from '../ui/statistics.js'; + +export const StatisticsPage = { + render: () => { + return ` +

Статистика

+
+ Загрузка данных... +
+ `; + }, + mount: () => { + // Вызывает renderStatisticsPage, который запускает опрос + renderStatisticsPage(); + }, + // 🔥 ВСТРОЕННАЯ ФУНКЦИЯ UNMOUNT: Роутер использует StatisticsPage.unmount + unmount: stopPolling +}; + +// 🔥 УДАЛЕН ЭКСПОРТ { stopPolling }; из этого файла. \ No newline at end of file diff --git a/webui/src/pages/Subscriptions.js b/webui/src/pages/Subscriptions.js new file mode 100644 index 0000000..7fda284 --- /dev/null +++ b/webui/src/pages/Subscriptions.js @@ -0,0 +1,6 @@ +// src/pages/Subscriptions.js + +import { SubscriptionsPage } from '../ui/subscriptions.js'; + +// Просто реэкспортируем объект для роутера +export const Subscriptions = SubscriptionsPage; \ No newline at end of file diff --git a/webui/src/styles/app.css b/webui/src/styles/app.css new file mode 100644 index 0000000..51b6562 --- /dev/null +++ b/webui/src/styles/app.css @@ -0,0 +1,411 @@ +/* --- Сброс и Тёмные Переменные --- */ +:root { + /* Основная цветовая палитра */ + --color-primary: #3b82f6; /* Синий (Акцент) */ + --color-primary-dark: #2563eb; + --color-text: #e5e7eb; /* Светлый текст */ + --color-text-secondary: #9ca3af; /* Второстепенный текст */ + --color-bg-main: #111827; /* Очень тёмный фон */ + --color-bg-card: #1f2937; /* Фон карточек и сайдбара */ + --color-border: #374151; /* Границы */ + --color-error: #ef4444; /* Красный */ + + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --border-radius: 6px; +} + +/* Применяем переменные к тёмной теме */ +html.dark-theme, body { + background-color: var(--color-bg-main); + color: var(--color-text); + font-family: var(--font-family); + margin: 0; + padding: 0; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +/* --- Общие Классы UI и Формы (для Авторизации) --- */ + +/* Контейнеры */ +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} +.full-screen { + min-height: 100vh; +} +.card { + background-color: var(--color-bg-card); + border-radius: var(--border-radius); + padding: 30px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +/* Форма и Поля */ +.form-group { + margin-bottom: 20px; +} +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} +.form-control { + width: 100%; + padding: 12px 10px; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + background-color: var(--color-bg-main); + color: var(--color-text); + transition: border-color 0.2s, box-shadow 0.2s; + font-size: 16px; +} +.form-control:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary); +} + +/* Кнопка */ +.btn-primary { + display: block; + width: 100%; + padding: 12px; + background-color: var(--color-primary); + color: white; + border: none; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: background-color 0.2s; +} +.btn-primary:hover:not(:disabled) { + background-color: var(--color-primary-dark); +} +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.error-message { + color: var(--color-error); + margin-top: 15px; + text-align: center; +} +.success-message { + color: #10b981; /* Зеленый */ + margin-top: 15px; + text-align: center; +} + + +/* --- Стили Dashboard (Сайдбар и Общая Структура) --- */ +.sidebar { + width: 250px; + background-color: var(--color-bg-card); + border-right: 1px solid var(--color-border); + flex-shrink: 0; + padding: 20px 0; + display: flex; /* Для прибивания кнопки выхода вниз */ + flex-direction: column; +} +.sidebar-title { + color: var(--color-primary); + text-align: center; + margin-bottom: 30px; + font-size: 20px; +} + +/* Стили меню */ +.menu-item { + display: block; + padding: 12px 20px; + color: var(--color-text-secondary); + text-decoration: none; + transition: background-color 0.2s, color 0.2s; + font-size: 15px; +} +.menu-item:hover { + background-color: #2e3a4e; /* Чуть светлее, чем сайдбар */ + color: var(--color-text); +} +.menu-item.active { + background-color: var(--color-primary); + color: white; +} +.logout-btn { + margin-top: auto; /* Прижимает к низу */ + border-top: 1px solid var(--color-border); +} + +.content-area { + flex-grow: 1; + padding: 30px; + overflow-y: auto; +} +.page-title { + color: var(--color-text); + border-bottom: 1px solid var(--color-border); + padding-bottom: 15px; + margin-top: 0; + margin-bottom: 30px; +} + + +/* --- Стили для страницы Статистики --- */ + +.stats-grid { + display: grid; + gap: 20px; + margin-bottom: 30px; +} + +/* Сетка для статусов (3 колонки) */ +.status-grid { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.stat-card { + background-color: var(--color-bg-card); + border-radius: var(--border-radius); + padding: 20px; + border: 1px solid var(--color-border); + transition: box-shadow 0.3s; +} + +.stat-card:hover { + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.stat-card h3 { + margin-top: 0; + font-size: 18px; + color: var(--color-text); + border-bottom: 1px solid var(--color-border); + padding-bottom: 10px; + margin-bottom: 15px; +} + +.status-line { + font-size: 14px; + margin-bottom: 15px; +} + +.text-yellow-500 { + color: #f59e0b; /* Желтый для "обновляется" */ + font-weight: 600; +} + +.text-green-500 { + color: #10b981; /* Зеленый для "активен" */ + font-weight: 600; +} + +/* Кнопка вторичная (для обновления) */ +.btn-secondary { + padding: 8px 15px; + background-color: transparent; + color: var(--color-primary); + border: 1px solid var(--color-primary); + border-radius: var(--border-radius); + cursor: pointer; + transition: background-color 0.2s; +} + +.btn-secondary:hover:not(:disabled) { + background-color: rgba(59, 130, 246, 0.1); +} + +.btn-secondary:disabled { + opacity: 0.7; + cursor: not-allowed; + color: var(--color-text-secondary); + border-color: var(--color-border); +} + +/* --- Стили для страницы Статистики (Дополнение: Ресурсы) --- */ + +.resource-group { + border-top: 1px solid var(--color-border); + padding-top: 15px; + margin-top: 20px; +} + +.resource-group h4 { + margin: 0 0 10px 0; + font-size: 14px; + color: var(--color-text-secondary); +} + +.resource-row { + display: flex; + gap: 15px; + justify-content: space-between; +} + +.resource-badge { + text-align: center; + padding: 10px; + border-radius: var(--border-radius); + background-color: var(--color-bg-main); /* Более темный фон для выделения */ + flex-grow: 1; + min-width: 0; +} + +/* Класс для размера чисел в блоках (50% от исходного размера) */ +.resource-count { + display: block; + font-weight: 700; + color: var(--color-primary); + margin-bottom: 3px; + font-size: 20px; /* Базовый размер для подписок */ +} + +/* Корректировка для уменьшения размера */ +.resource-count.small-count { + font-size: 16px; +} + + +.resource-title { + display: block; + font-size: 12px; + color: var(--color-text-secondary); +} + + +/* --- Стили для страницы Подписок --- */ + +.subscription-list { + list-style: none; + padding: 0; + margin: 20px 0; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + background-color: var(--color-bg-card); +} + +.subscription-item { + padding: 15px; + border-bottom: 1px solid var(--color-border); +} + +.subscription-item:last-child { + border-bottom: none; +} + +.subscription-item label { + display: flex; + align-items: flex-start; /* Выравниваем элементы по верху */ + cursor: pointer; + font-size: 16px; +} + +.subscription-checkbox { + margin-right: 15px; + flex-shrink: 0; + width: 20px; + height: 20px; + margin-top: 2px; +} + +.resource-info { + display: flex; + flex-direction: column; /* Обертка для двух строк */ + flex-grow: 1; +} + +.resource-main-line { + /* Первая строка: Описание (Ключ) */ + display: flex; + margin-bottom: 3px; +} + +.resource-description { + font-weight: 600; + color: var(--color-text); + margin-right: 8px; +} + +/* 🔥 ОБНОВЛЕНО: Сделаем resource-key (используется для shortName) крупнее */ +.resource-key { + font-weight: 600; /* Жирный шрифт */ + color: var(--color-text); /* Основной цвет текста */ + font-size: 16px; /* Крупнее, чтобы лучше читалось */ + margin-right: 15px; +} + +.resource-details { + /* Вторая строка: Детали / или в случае компонентов - полное имя */ + font-size: 13px; + color: var(--color-text-secondary); + margin-left: 0; +} + + +/* --- Стили для страницы API Токенов --- */ + +.token-list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + background-color: var(--color-bg-card); +} + +.token-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px; + border-bottom: 1px solid var(--color-border); +} + +.token-item:last-child { + border-bottom: none; +} + +.token-description { + font-weight: 600; + color: var(--color-text); + flex-grow: 1; +} + +.token-details { + display: flex; + align-items: center; + gap: 20px; + flex-shrink: 0; +} + +.token-value { + font-family: monospace; + font-size: 14px; + color: var(--color-text-secondary); +} + +/* Кнопка-иконка для удаления */ +.btn-icon { + background: none; + border: none; + cursor: pointer; + color: var(--color-error); + padding: 5px; + border-radius: 4px; + transition: background-color 0.2s; +} + +.btn-icon:hover { + background-color: rgba(239, 68, 68, 0.1); /* Легкий красный фон при наведении */ +} + +.delete-icon { + font-weight: 600; + font-size: 18px; + line-height: 1; +} diff --git a/webui/src/ui/api_tokens.js b/webui/src/ui/api_tokens.js new file mode 100644 index 0000000..12ceca8 --- /dev/null +++ b/webui/src/ui/api_tokens.js @@ -0,0 +1,205 @@ +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; + +// --- Глобальное состояние --- +let tokens = []; + +/** + * @private + * Загружает текущий список токенов. + * @returns {Promise} + */ +async function loadTokens() { + try { + // Запрос списка токенов + tokens = await JSONRPC.Auth.listTokens(); + tokens = tokens || []; + return true; + } catch (e) { + console.error("Ошибка при загрузке токенов:", e); + tokens = []; + return false; + } +} + +/** + * @private + * Рендерит список токенов и кнопки управления. + */ +function renderTokenList() { + const $listContainer = $('#api-token-container'); + + // Кнопка "Создать токен" + const createButtonHtml = ` + + + `; + + if (tokens.length === 0) { + $listContainer.html(createButtonHtml + '

Активных API токенов не найдено.

'); + attachEventHandlers(); + return; + } + + let listHtml = tokens.map(item => { + // Мы НЕ показываем полный токен, показываем только его начало/конец для идентификации + const displayToken = item.token ? + `${item.token.substring(0, 8)}...${item.token.substring(item.token.length - 4)}` : + 'Неизвестный токен'; + + return ` +
  • +
    ${item.description}
    +
    + Ключ: ${displayToken} + +
    +
  • + `; + }).join(''); + + const html = ` + ${createButtonHtml} +
      ${listHtml}
    + `; + + $listContainer.html(html); + attachEventHandlers(); +} + +/** + * @private + * Отображает модальное окно для создания токена. + */ +function showCreateTokenModal() { + // Используем простое модальное окно jQuery для минимализма + const description = prompt("Введите описание для нового API токена (например, 'Токен для Telegram бота'):"); + + if (description === null) { + return; // Пользователь нажал Отмена + } + + if (description.trim() === "") { + alert("Описание токена не может быть пустым."); + return; + } + + createToken(description.trim()); +} + +/** + * @private + * Логика создания токена и обновления UI. + * @param {string} description - Описание токена. + */ +async function createToken(description) { + const $btn = $('#create-token-btn'); + const $message = $('#token-status-message'); + + $message.hide().removeClass('error-message').addClass('success-message'); + $btn.prop('disabled', true).text('Создание...'); + + try { + const newTokenValue = await JSONRPC.Auth.createAPIToken(description); + + if (!newTokenValue) { + throw new Error("Сервер не вернул токен."); + } + + // Показываем токен пользователю ОДИН раз + prompt("ВАЖНО: Ваш новый токен API. Сохраните его, он больше не будет показан!", newTokenValue); + + // Перезагружаем список + await loadTokens(); + renderTokenList(); + + $message.text(`Токен "${description}" успешно создан.`).show(); + + } catch (e) { + console.error('Ошибка создания токена:', e); + $message.removeClass('success-message').addClass('error-message').text('Ошибка при создании токена.').show(); + } finally { + $btn.prop('disabled', false).text('Создать токен API'); + setTimeout(() => $message.fadeOut(), 8000); + } +} + +/** + * @private + * Логика удаления токена. + * @param {string} token - Токен для удаления. + */ +async function deleteToken(token) { + if (!confirm("Вы уверены, что хотите удалить этот API токен?")) { + return; + } + + const $item = $(`li.token-item[data-full-token="${token}"]`); + $item.addClass('deleting').css('opacity', 0.5); // Визуальная обратная связь + + try { + await JSONRPC.Auth.removeToken(token); + + // Удаляем из локального состояния + tokens = tokens.filter(item => item.token !== token); + + // Перерисовываем список + renderTokenList(); + + const $message = $('#token-status-message'); + $message.text('Токен успешно удален.').addClass('success-message').show(); + + } catch (e) { + console.error('Ошибка удаления токена:', e); + $item.removeClass('deleting').css('opacity', 1); // Восстанавливаем, если ошибка + const $message = $('#token-status-message'); + $message.removeClass('success-message').addClass('error-message').text('Ошибка при удалении токена.').show(); + } +} + + +/** + * @private + * Прикрепление обработчиков событий. + */ +function attachEventHandlers() { + $('#create-token-btn').off('click').on('click', showCreateTokenModal); + + // Делегирование для кнопок удаления (они перерисовываются) + $('#api-token-container').off('click', '.delete-token-btn').on('click', '.delete-token-btn', function() { + const tokenToDelete = $(this).data('token'); + if (tokenToDelete) { + deleteToken(tokenToDelete); + } + }); +} + + +// --- Функции для роутера --- + +export const APITokensPage = { + render: () => { + return ` +

    API Токены

    +
    +

    Загрузка токенов...

    +
    + `; + }, + mount: async () => { + const success = await loadTokens(); + if (success) { + renderTokenList(); + } else { + $('#api-token-container').html('

    Не удалось загрузить токены. Проверьте соединение с сервером.

    '); + } + }, + unmount: () => { + tokens = []; + $('#api-token-container').off(); // Удаляем все обработчики + } +}; \ No newline at end of file diff --git a/webui/src/ui/components.js b/webui/src/ui/components.js new file mode 100644 index 0000000..365b319 --- /dev/null +++ b/webui/src/ui/components.js @@ -0,0 +1,134 @@ +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; + +// --- Глобальное состояние для страницы --- +let availableComponents = []; +let enabledComponents = []; + +/** + * @private + * Извлекает короткое имя компонента, беря часть строки после последней точки. + * @param {string} fullName - Полное имя компонента (напр., ru.kirillius.pf.sdn.API). + * @returns {string} Короткое имя (напр., API). + */ +function getShortName(fullName) { + // Находим позицию последней точки + const lastDotIndex = fullName.lastIndexOf('.'); + + // Если точка найдена, возвращаем подстроку после неё. + // Если точки нет (или она в конце), возвращаем полное имя. + if (lastDotIndex > -1 && lastDotIndex < fullName.length - 1) { + return fullName.substring(lastDotIndex + 1); + } + return fullName; +} + + +/** Загружает все необходимые данные для страницы (без изменений) */ +async function loadComponentData() { + try { + enabledComponents = await JSONRPC.System.getEnabledComponents(); + availableComponents = await JSONRPC.System.getAvailableComponents(); + availableComponents.sort(); + + } catch (e) { + console.error("Ошибка при загрузке данных компонентов:", e); + availableComponents = []; + enabledComponents = []; + return false; + } + return true; +} + +/** Рендеринг HTML списка компонентов */ +function renderComponentList() { + const $listContainer = $('#component-list-container'); + + if (availableComponents.length === 0) { + $listContainer.html('

    Нет доступных системных компонентов.

    '); + return; + } + + let listHtml = availableComponents.map(fullName => { + // 🔥 Применяем новую функцию для отображения + const shortName = getShortName(fullName); + + // Проверяем, включен ли компонент + const isChecked = enabledComponents.includes(fullName); // Проверка по полному имени! + + return ` +
  • + +
  • + `; + }).join(''); + + const html = ` +
      ${listHtml}
    + + + `; + + $listContainer.html(html); + attachEventHandlers(); +} + +/** Обработчик кнопки "Сохранить" (без изменений, т.к. работаем с полными именами из value) */ +function attachEventHandlers() { + $('#save-components-btn').on('click', async function() { + const $btn = $(this); + const $message = $('#component-status-message'); + $message.hide().removeClass('success-message').addClass('error-message'); + + // Собираем все отмеченные ключи компонентов (они содержат полные имена) + const newEnabledComponents = $('.component-checkbox:checked').map(function() { + return $(this).val(); + }).get(); + + $btn.prop('disabled', true).text('Сохранение...'); + + try { + await JSONRPC.System.setEnabledComponents(newEnabledComponents); + + enabledComponents = newEnabledComponents; + + $message.text('Настройки компонентов успешно сохранены!').addClass('success-message').show(); + + } catch (e) { + console.error('Ошибка сохранения компонентов:', e); + $message.text('Ошибка при сохранении компонентов.').show(); + } finally { + $btn.prop('disabled', false).text('Сохранить'); + setTimeout(() => $message.fadeOut(), 5000); + } + }); +} + +// --- Функции для роутера (без изменений) --- + +export const ComponentsPage = { + render: () => { + return ` +

    Управление Компонентами

    +
    +

    Загрузка доступных компонентов...

    +
    + `; + }, + mount: async () => { + const success = await loadComponentData(); + if (success) { + renderComponentList(); + } else { + $('#component-list-container').html('

    Не удалось загрузить данные. Проверьте соединение с сервером.

    '); + } + }, + unmount: () => { + availableComponents = []; + enabledComponents = []; + } +}; \ No newline at end of file diff --git a/webui/src/ui/statistics.js b/webui/src/ui/statistics.js new file mode 100644 index 0000000..aadfd6f --- /dev/null +++ b/webui/src/ui/statistics.js @@ -0,0 +1,198 @@ +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; + +const $content = () => $('#statistics-content'); + +// 🔥 Переменная для хранения ID интервала опроса +let updateInterval = null; +const POLLING_INTERVAL = 5000; // 5 секунд + +// --- Утилитарные функции для рендеринга UI-блоков --- + +/** Создает HTML-карточку для отображения статуса и ресурсов */ +const createStatusCard = (title, status, buttonId, buttonLabel, isUpdating, resourceStatsHtml = '') => { + const statusClass = status === 'Обновляется' || status === 'Обнаружено' ? 'text-yellow-500' : 'text-green-500'; + + return ` +
    +

    ${title}

    +

    + Статус: ${status} +

    + + ${resourceStatsHtml} +
    + `; +}; + +/** Создает HTML-блок для отображения ресурсов сети */ +const createResourceStatsHtml = (resources) => { + return ` +
    +

    Ресурсы:

    +
    + ${createResourceBadge("Домены", resources.domains.length)} + ${createResourceBadge("ASN", resources.ASN.length)} + ${createResourceBadge("Подсети", resources.subnets.length)} +
    +
    + `; +}; + +/** Создает HTML-бэйдж для отдельного ресурса (с уменьшенным шрифтом) */ +const createResourceBadge = (title, count) => { + return ` +
    + ${count} + ${title} +
    + `; +}; + +// --- Основные функции API и DOM --- + +/** Получение количества активных подписок. */ +async function getSubscriptionCount() { + try { + // Используем обновленный метод getSubscribedResources + let subscribedResources = await JSONRPC.SubscriptionManager.getSubscribedResources(); + return (subscribedResources && subscribedResources.length) || 0; + } catch(e) { + console.error("Ошибка при получении subscribedResources:", e); + return 0; + } +} + +/** Запрос данных о ресурсах сети. */ +async function getNetworkResources() { + try { + let res = await JSONRPC.NetworkManager.getOutputResources(); + // Используем ключи, как они приходят с API: domains, ASN, subnets + return { + domains: res.domains || [], + ASN: res.ASN || [], + subnets: res.subnets || [] + }; + } catch(e) { + console.error("Ошибка при получении ресурсов:", e); + return { domains: [], ASN: [], subnets: [] }; + } +} + +/** 2. Запрос и рендеринг данных о ПО и Статусах */ +async function renderSystemAndManagerStatus() { + const dataHtml = []; + + // --- Получение данных --- + const hasUpdates = await JSONRPC.System.hasUpdates(); + const isSubUpdating = await JSONRPC.SubscriptionManager.isUpdating(); + const isNetUpdating = await JSONRPC.NetworkManager.isUpdating(); + const subCount = await getSubscriptionCount(); + const networkResources = await getNetworkResources(); + + // 1. Информация об обновлении ПО + dataHtml.push(createStatusCard( + "Обновление ПО", + hasUpdates ? "Обнаружено" : "Нет обновлений", + "update-software-btn", + "Проверить", + false + )); + + // 2. Статус менеджера подписок (добавляем статистику) + const subStatsHtml = `
    +

    Подписки:

    +
    + ${createResourceBadge("Активно", subCount)} +
    +
    `; + dataHtml.push(createStatusCard( + "Менеджер Подписок", + isSubUpdating ? "Обновляется" : "Активен", + "update-sub-btn", + "Обновить", + isSubUpdating, + subStatsHtml + )); + + // 3. Статус менеджера сетей (ВКЛЮЧАЕМ РЕСУРСЫ) + const netResourcesHtml = createResourceStatsHtml(networkResources); + dataHtml.push(createStatusCard( + "Менеджер Сетей", + isNetUpdating ? "Обновляется" : "Активен", + "update-net-btn", + "Обновить", + isNetUpdating, + netResourcesHtml + )); + + // Обновляем DOM один раз + $content().html(`
    ${dataHtml.join('')}
    `); + + // Прикрепляем обработчики событий после рендеринга + attachEventHandlers(); +} + +// --- Главная функция монтирования --- +export async function renderStatisticsPage() { + // 1. Очищаем старый интервал (на случай, если он был) + stopPolling(); + + // 2. Первичный рендеринг + await renderSystemAndManagerStatus(); + + console.log("start interval") + // 3. Запускаем опрос каждые 5 секунд + updateInterval = setInterval(renderSystemAndManagerStatus, POLLING_INTERVAL); +} + +/** 🔥 Остановка интервала опроса при уходе со страницы */ +export function stopPolling() { + if (updateInterval) { + console.log("clear interval") + clearInterval(updateInterval); + updateInterval = null; + } +} + + +/** Прикрепление обработчиков к кнопкам */ +function attachEventHandlers() { + // Обработчик для менеджера подписок + $('#update-sub-btn').off('click').on('click', async function() { + const $btn = $(this); + $btn.prop('disabled', true).text('Обновление...'); + + try { + await JSONRPC.SubscriptionManager.triggerUpdate(); + alert("Обновление подписок запущено!"); + renderSystemAndManagerStatus(); // Обновляем немедленно + } catch(e) { + alert("Ошибка при запуске обновления подписок!"); + $btn.prop('disabled', false).text('Обновить'); + } + }); + + // Обработчик для менеджера сетей + $('#update-net-btn').off('click').on('click', async function() { + const $btn = $(this); + $btn.prop('disabled', true).text('Обновление...'); + + try { + // Передаем аргумент true в triggerUpdate + await JSONRPC.NetworkManager.triggerUpdate(true); + alert("Обновление сетей запущено!"); + renderSystemAndManagerStatus(); // Обновляем немедленно + } catch(e) { + alert("Ошибка при запуске обновления сетей!"); + $btn.prop('disabled', false).text('Обновить'); + } + }); + + // Кнопка проверки обновлений ПО + $('#update-software-btn').off('click').on('click', function() { + alert("Проверка обновлений ПО запущена..."); + }); +} \ No newline at end of file diff --git a/webui/src/ui/subscriptions.js b/webui/src/ui/subscriptions.js new file mode 100644 index 0000000..77dbbde --- /dev/null +++ b/webui/src/ui/subscriptions.js @@ -0,0 +1,126 @@ +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; + +// --- Глобальное состояние для страницы --- +let availableResources = {}; +let subscribedKeys = []; + +/** Загружает все необходимые данные для страницы */ +async function loadSubscriptionData() { + try { + subscribedKeys = await JSONRPC.SubscriptionManager.getSubscribedResources(); + availableResources = await JSONRPC.SubscriptionManager.getAvailableResources(); + + } catch (e) { + console.error("Ошибка при загрузке данных подписок:", e); + availableResources = {}; + subscribedKeys = []; + return false; + } + return true; +} + +/** Рендеринг HTML списка */ +function renderSubscriptionList() { + const $listContainer = $('#subscription-list-container'); + const availableKeys = Object.keys(availableResources).sort(); + + if (availableKeys.length === 0) { + $listContainer.html('

    Нет доступных ресурсов для подписки.

    '); + return; + } + + let listHtml = availableKeys.map(key => { + const details = availableResources[key]; + const isChecked = subscribedKeys.includes(key); + + // 🔥 Обновлено: Используем description и меняем структуру + const description = details.description || 'Нет описания'; + + // Формируем строку с деталями (теперь на новой строке) + const detailText = ` + Доменов: ${details.domains ? details.domains.length : 0}, + ASN: ${details.ASN ? details.ASN.length : 0}, + Подсетей: ${details.subnets ? details.subnets.length : 0} + `; + + return ` +
  • + +
  • + `; + }).join(''); + + const html = ` +
      ${listHtml}
    + + + `; + + $listContainer.html(html); + attachEventHandlers(); +} + +/** Обработчик кнопки "Сохранить" (остается без изменений) */ +function attachEventHandlers() { + $('#save-subscriptions-btn').on('click', async function() { + const $btn = $(this); + const $message = $('#subscription-status-message'); + $message.hide().removeClass('success-message').addClass('error-message'); + + const newSubscribedKeys = $('.subscription-checkbox:checked').map(function() { + return $(this).val(); + }).get(); + + $btn.prop('disabled', true).text('Сохранение...'); + + try { + await JSONRPC.SubscriptionManager.setSubscribedResources(newSubscribedKeys); + + subscribedKeys = newSubscribedKeys; + + $message.text('Настройки подписок успешно сохранены!').addClass('success-message').show(); + + } catch (e) { + console.error('Ошибка сохранения подписок:', e); + $message.text('Ошибка при сохранении подписок.').show(); + } finally { + $btn.prop('disabled', false).text('Сохранить'); + setTimeout(() => $message.fadeOut(), 5000); + } + }); +} + +// ... (SubscriptionsPage, loadSubscriptionData остаются без изменений) ... + +export const SubscriptionsPage = { + render: () => { + return ` +

    Управление Подписками

    +
    +

    Загрузка доступных ресурсов...

    +
    + `; + }, + mount: async () => { + const success = await loadSubscriptionData(); + if (success) { + renderSubscriptionList(); + } else { + $('#subscription-list-container').html('

    Не удалось загрузить данные. Проверьте соединение с сервером.

    '); + } + }, + unmount: () => { + availableResources = {}; + subscribedKeys = []; + } +}; \ No newline at end of file diff --git a/webui/src/utils/cookies.js b/webui/src/utils/cookies.js new file mode 100644 index 0000000..7bb42e0 --- /dev/null +++ b/webui/src/utils/cookies.js @@ -0,0 +1,42 @@ +// src/utils/cookies.js + +const TOKEN_KEY = 'sdn_auth_token'; + +/** + * Получает токен авторизации из куки. + * @returns {string|null} Токен или null, если не найден. + */ +export function getAuthToken() { + const nameEQ = TOKEN_KEY + "="; + const ca = document.cookie.split(';'); + for(let i=0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; +} + +/** + * Устанавливает токен авторизации в куки на 1 год. + * @param {string} token - Токен для сохранения. + */ +export function setAuthToken(token) { + const expirationDate = new Date(); + // Устанавливаем срок действия на 1 год + expirationDate.setTime(expirationDate.getTime() + (365 * 24 * 60 * 60 * 1000)); + const expires = "expires=" + expirationDate.toUTCString(); + + // Используем secure и samesite=Lax (рекомендуется для современных браузеров) + // path=/ делает токен доступным для всего сайта + document.cookie = `${TOKEN_KEY}=${token};${expires};path=/;samesite=Lax`; +} + +/** + * Удаляет токен авторизации. + */ +export function removeAuthToken() { + document.cookie = `${TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; +} \ No newline at end of file diff --git a/webui/vite.config.js b/webui/vite.config.js new file mode 100644 index 0000000..28f4c1e --- /dev/null +++ b/webui/vite.config.js @@ -0,0 +1,34 @@ +import { fileURLToPath, URL } from 'node:url' +// 🔥 ДОБАВЛЯЕМ ИМПОРТ node:path +import path from 'node:path' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, + + // ----------------------------------------------------------------- + // 🔥 ДОБАВЛЯЕМ ЭТОТ БЛОК! Явно разрешаем доступ к node_modules + server: { + fs: { + allow: [ + // Позволяем доступ к корневой папке проекта (для src) + '.', + // Позволяем доступ к папке node_modules, чтобы Vite мог найти стили + path.resolve(__dirname, 'node_modules') + ] + } + } + // ----------------------------------------------------------------- +}) \ No newline at end of file