commit d84861949f14c3c1ea5fdf1c5f65aeecb5f333c8 Author: Hartmut Bauer Date: Fri Oct 9 17:13:39 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ce586e --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*.iml +.kotlin +.gradle +**/build/ +xcuserdata +!src/**/build/ +local.properties +.idea +.DS_Store +captures +.externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +node_modules/ +/div/ +/server/h2.mv.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..6097e8f --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# core + +This project was created using the [Ktor Project Generator](https://start.ktor.io). + +Here are some useful links to get you started: + +* [Ktor Documentation](https://ktor.io/docs/home.html) +* [Ktor GitHub page](https://github.com/ktorio/ktor) +* [Ktor Slack chat](https://app.slack.com/client/T09229ZC6/C0A974TJ9). [Request an invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). + +## Features + +Here's a list of features included in this project: + +| Name | Description | +|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| +| [Status Pages](https://start.ktor.io/p/io.ktor/server-status-pages) | Provides exception handling for routes | +| [kotlinx.serialization](https://start.ktor.io/p/io.ktor/server-kotlinx-serialization) | Handles JSON serialization using kotlinx.serialization library | +| [Content Negotiation](https://start.ktor.io/p/io.ktor/server-content-negotiation) | Provides automatic content conversion according to Content-Type and Accept headers | +| [Authentication](https://start.ktor.io/p/io.ktor/server-auth) | Provides extension point for handling the Authorization header | +| [Static Content](https://start.ktor.io/p/io.ktor/server-static-content) | Serves static files from defined locations | +| [Caching Headers](https://start.ktor.io/p/io.ktor/server-caching-headers) | Provides options for responding with standard cache-control headers | +| [HTMX](https://start.ktor.io/p/io.ktor/server-htmx) | Includes HTMX for front-end scripting | +| [HTML DSL](https://start.ktor.io/p/org.jetbrains/server-html-dsl) | Generates HTML from Kotlin DSL | +| [PostgreSQL](https://start.ktor.io/p/org.jetbrains/server-postgres) | Adds Postgres database support | +| [Exposed](https://start.ktor.io/p/org.jetbrains/server-exposed) | Adds Exposed database to your application | +| [Dependency Injection](https://start.ktor.io/p/io.ktor/server-di) | Enables dependency injection for your server | +| [CSRF](https://start.ktor.io/p/io.ktor/server-csrf) | Cross-site request forgery mitigation | +| [Authentication OAuth](https://start.ktor.io/p/io.ktor/server-auth-oauth) | Handles OAuth Bearer authentication scheme | +| [Authentication JWT](https://start.ktor.io/p/io.ktor/server-auth-jwt) | Handles JSON Web Token (JWT) bearer authentication scheme | +| [Forwarded Headers](https://start.ktor.io/p/io.ktor/server-forwarded-header-support) | Allows handling proxied headers (X-Forwarded-*) | +| [Default Headers](https://start.ktor.io/p/io.ktor/server-default-headers) | Adds a default set of headers to HTTP responses | +| [CORS](https://start.ktor.io/p/io.ktor/server-cors) | Enables Cross-Origin Resource Sharing (CORS) | +| [Compression](https://start.ktor.io/p/io.ktor/server-compression) | Compresses responses using encoding algorithms like GZIP | + +## Structure + +This project includes the following modules: + +| Path | Description | +|--------|-------------| +| | null | +| server | null | +| web | null | + +## Building & Running + +To build or run the project, use one of the following tasks: + +| Task | Description | +|---------------------------|-------------------| +| `./gradlew :server:test` | Run the tests | +| `./gradlew :server:build` | Build the project | +| `./gradlew :server:run` | Run the server | + +If the server starts successfully, you'll see the following output: + +``` +2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds. +2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080 +``` diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..77a2590 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.serialization) apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a3293d9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..b96f737 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,42 @@ +[versions] +appsrv = "0.2.0-alpha" +books = "0.2.0-alpha" + +exposed = "1.5.0" +h2 = "2.4.240" +h2-r2dbc = "1.1.0.RELEASE" +hikari = "7.1.0" +html5Qrcode = "2.3.8" +htmx = "2.0.8" +jvm = "25" +koin = "4.2.2" +koinAnnotations = "2.3.1" +kotlin = "2.4.20" +kotlinx-html = "0.12.0" +kspPlugin = "2.3.12" +logback = "1.6.5}" +postgresql = "42.7.13" + +[libraries] +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } + +exposed-r2dbc = { module = "org.jetbrains.exposed:exposed-r2dbc", version.ref = "exposed" } +h2database-h2 = { module = "com.h2database:h2", version.ref = "h2" } +h2database-r2dbc = { module = "io.r2dbc:r2dbc-h2", version.ref = "h2-r2dbc" } + +hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } +koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } +koin-logger = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" } +koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotations" } +koin-ksp = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koinAnnotations" } +kotlinx-kotlinxHtml = { module = "org.jetbrains.kotlinx:kotlinx-html", version.ref = "kotlinx-html" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "kspPlugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..b9bb139 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..24c62d5 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 0000000..4e824e5 --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,3181 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz#44b5ce3485e95235aca06e628eeaaa3c816bc4cc" + integrity sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" + integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.31": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jsonjoy.com/base64@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-17.67.0.tgz#7eeda3cb41138d77a90408fd2e42b2aba10576d7" + integrity sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw== + +"@jsonjoy.com/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/buffers@17.67.0", "@jsonjoy.com/buffers@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz#5c58dbcdeea8824ce296bd1cfce006c2eb167b3d" + integrity sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw== + +"@jsonjoy.com/buffers@^1.0.0", "@jsonjoy.com/buffers@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz#8d99c7f67eaf724d3428dfd9826c6455266a5c83" + integrity sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA== + +"@jsonjoy.com/codegen@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz#3635fd8769d77e19b75dc5574bc9756019b2e591" + integrity sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q== + +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + +"@jsonjoy.com/fs-core@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-core/-/fs-core-4.79.0.tgz#ad4dd92688da6812184fc04d1eda0afbfd663723" + integrity sha512-bxSwMrDY71OjEdj2mAYBxSe4BnHtAOjOxqUoFJc0WsllcGgA8iYl6wRF+4aVdFUjjEyHfBwxVv0QqW4AweIUZw== + dependencies: + "@jsonjoy.com/fs-node-builtins" "4.79.0" + "@jsonjoy.com/fs-node-utils" "4.79.0" + thingies "^2.5.0" + +"@jsonjoy.com/fs-fsa@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-fsa/-/fs-fsa-4.79.0.tgz#80b3df89e3402ff0abbc87386f12c85a412acf89" + integrity sha512-EdadmStAvZw4jYg4iW3mDxBmzMFSI8jcHQkSl+rIQIO7dWbPdXaB83PBQhNCCtMBeX7zwcOFjpp0ecXqbYbjUA== + dependencies: + "@jsonjoy.com/fs-core" "4.79.0" + "@jsonjoy.com/fs-node-builtins" "4.79.0" + "@jsonjoy.com/fs-node-utils" "4.79.0" + thingies "^2.5.0" + +"@jsonjoy.com/fs-node-builtins@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.79.0.tgz#68de66759363b641579b7e0e60a0699434a91fe4" + integrity sha512-LoBHewVHp/bZQ8Om1Iivl6NEHg7Mmby9kXJZysfnHYW4256rzXFSEcEpto1qgSAka+sq2v4N/Cnl562Z15MByA== + +"@jsonjoy.com/fs-node-to-fsa@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.79.0.tgz#8a2d73acf27cb56b1c50c8dd82a42801e552b1f2" + integrity sha512-xFza/t81sVL4rik7h3SlcymC6X7pvNllI88LW1GESEANDXinf5lZoqju59i3AsGLyy4i0eBQvPiizWSMwCunQA== + dependencies: + "@jsonjoy.com/fs-fsa" "4.79.0" + "@jsonjoy.com/fs-node-builtins" "4.79.0" + "@jsonjoy.com/fs-node-utils" "4.79.0" + +"@jsonjoy.com/fs-node-utils@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.79.0.tgz#2027d373b9bfc6c57629a4b95f41543c847612da" + integrity sha512-Rj/WevHB8DV8VyEvFGSvDD3m3JHOVk3fO1biH26iworL9/rqdUMKQQeyiKTLbdf3Z9s9vVZnn8ZC0FWZgtO2mg== + dependencies: + "@jsonjoy.com/fs-node-builtins" "4.79.0" + glob-to-regex.js "^1.3.1" + +"@jsonjoy.com/fs-node@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node/-/fs-node-4.79.0.tgz#2fa2b746b069482394988132f61012ec168214b3" + integrity sha512-/k18aT1X+A2yLdlTe3WKpL5y2qrvbcUY1BXA4A+s8fBfTeXfi+xdvEB85JVXVf2i9kzASQZFx8dHw9OchgMUhQ== + dependencies: + "@jsonjoy.com/fs-core" "4.79.0" + "@jsonjoy.com/fs-node-builtins" "4.79.0" + "@jsonjoy.com/fs-node-utils" "4.79.0" + "@jsonjoy.com/fs-print" "4.79.0" + "@jsonjoy.com/fs-snapshot" "4.79.0" + glob-to-regex.js "^1.3.1" + thingies "^2.5.0" + +"@jsonjoy.com/fs-print@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-print/-/fs-print-4.79.0.tgz#eb65991853ab80491110ae42a637455d9d272e71" + integrity sha512-Bp5Q+OV1Dnl8oKf8hVhmboeI+NWbWRk1GxtJzuRvopDeNBDBh0RLevEsfnOVtwBwgRZaptSXn3e/mm3kbOzfDw== + dependencies: + "@jsonjoy.com/fs-node-utils" "4.79.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/fs-snapshot@4.79.0": + version "4.79.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.79.0.tgz#eadd70e354afaeb97fc996c8ef7431249aea73fa" + integrity sha512-/GY+eMvOl4ZQHUnrKYjC/t3N0BImBg7zv/cAGm3kiwUfqSQuo87jtbjmD3mzgQMv16K8zttkHE2wT9GaVsehvA== + dependencies: + "@jsonjoy.com/buffers" "^17.65.0" + "@jsonjoy.com/fs-node-utils" "4.79.0" + "@jsonjoy.com/json-pack" "^17.65.0" + "@jsonjoy.com/util" "^17.65.0" + +"@jsonjoy.com/json-pack@^1.11.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz#93f8dd57fe3a3a92132b33d1eb182dcd9e7629fa" + integrity sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg== + dependencies: + "@jsonjoy.com/base64" "^1.1.2" + "@jsonjoy.com/buffers" "^1.2.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/json-pointer" "^1.0.2" + "@jsonjoy.com/util" "^1.9.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/json-pack@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz#8dd8ff65dd999c5d4d26df46c63915c7bdec093a" + integrity sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w== + dependencies: + "@jsonjoy.com/base64" "17.67.0" + "@jsonjoy.com/buffers" "17.67.0" + "@jsonjoy.com/codegen" "17.67.0" + "@jsonjoy.com/json-pointer" "17.67.0" + "@jsonjoy.com/util" "17.67.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/json-pointer@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz#74439573dc046e0c9a3a552fb94b391bc75313b8" + integrity sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA== + dependencies: + "@jsonjoy.com/util" "17.67.0" + +"@jsonjoy.com/json-pointer@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408" + integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + dependencies: + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/util" "^1.9.0" + +"@jsonjoy.com/util@17.67.0", "@jsonjoy.com/util@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-17.67.0.tgz#7c4288fc3808233e55c7610101e7bb4590cddd3f" + integrity sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew== + dependencies: + "@jsonjoy.com/buffers" "17.67.0" + "@jsonjoy.com/codegen" "17.67.0" + +"@jsonjoy.com/util@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46" + integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + dependencies: + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@noble/hashes@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@peculiar/asn1-cms@^2.6.0", "@peculiar/asn1-cms@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz#1c0b42ba5dcbaf8e1e060a1e76cd2442affbc37c" + integrity sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + "@peculiar/asn1-x509-attr" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-csr@^2.6.0": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz#d306964f97bd7eb4edc6e3dc5e5ac87b2414e858" + integrity sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-ecc@^2.6.0": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz#2bf1bd8734584689a2d3f43c57b25ce3a60a0ce2" + integrity sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pfx@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz#795ddfe7ee3669b50f42cdaed42cc82b22a5d440" + integrity sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw== + dependencies: + "@peculiar/asn1-cms" "^2.9.4" + "@peculiar/asn1-pkcs8" "^2.9.4" + "@peculiar/asn1-rsa" "^2.9.4" + "@peculiar/asn1-schema" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs8@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz#79ac7696eb7acf3fe68b121ac39fcb265f8b219e" + integrity sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs9@^2.6.0": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz#18c89ab8d5e34469d43497bdbde7fc84cf437991" + integrity sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ== + dependencies: + "@peculiar/asn1-cms" "^2.9.4" + "@peculiar/asn1-pfx" "^2.9.4" + "@peculiar/asn1-pkcs8" "^2.9.4" + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + "@peculiar/asn1-x509-attr" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-rsa@^2.6.0", "@peculiar/asn1-rsa@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz#0547359f09b10876a21a96ef32b70af6db9855ad" + integrity sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-schema@^2.6.0", "@peculiar/asn1-schema@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz#99003d4bc87ec2ed2c4deb2843404496a912c3cf" + integrity sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg== + dependencies: + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-x509-attr@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz#c74094ffb97442c3fac7b965bd1345f9a0b59d48" + integrity sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/asn1-x509" "^2.9.4" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-x509@^2.6.0", "@peculiar/asn1-x509@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz#f997898c48ba6536bed640f788734d36477c21ad" + integrity sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q== + dependencies: + "@peculiar/asn1-schema" "^2.9.4" + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/utils@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/utils/-/utils-2.0.3.tgz#a27ca4c4b73652e110f19a7d16d664f458a5528e" + integrity sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ== + dependencies: + tslib "^2.8.1" + +"@peculiar/x509@^1.14.2": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@peculiar/x509/-/x509-1.14.3.tgz#2c44c2b89474346afec38a0c2803ec4fb8ce959e" + integrity sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA== + dependencies: + "@peculiar/asn1-cms" "^2.6.0" + "@peculiar/asn1-csr" "^2.6.0" + "@peculiar/asn1-ecc" "^2.6.0" + "@peculiar/asn1-pkcs9" "^2.6.0" + "@peculiar/asn1-rsa" "^2.6.0" + "@peculiar/asn1-schema" "^2.6.0" + "@peculiar/asn1-x509" "^2.6.0" + pvtsutils "^1.3.6" + reflect-metadata "^0.2.2" + tslib "^2.8.1" + tsyringe "^4.10.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.12": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + +"@types/estree@^1.0.8": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0", "@types/express-serve-static-core@^5.1.1": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz#9d34c88c0c9ee62b9a6e4d9f8ab8d7e29688e6b4" + integrity sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^5.0.6": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc" + integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "^2" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*", "@types/node@>=10.0.0": + version "26.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.6.1.tgz#059fb87569c8b47a495c75ed8a884832c73eab1d" + integrity sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA== + dependencies: + undici-types "~8.9.0" + +"@types/qs@*": + version "6.15.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.1.tgz#8606884272c63f0db96986bd3548650d8a9388bf" + integrity sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@^2", "@types/serve-static@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-2.2.0.tgz#d4a447503ead0d1671132d1ab6bd58b805d8de6a" + integrity sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + +"@types/ws@^8.18.1", "@types/ws@^8.5.12": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +accepts@~1.3.4, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-phases@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== + +acorn@^8.15.0, acorn@^8.16.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940" + integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ== + +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^8.0.0, ajv@^8.20.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.2.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.3.0.tgz#247c8e7b70a1a43b10ce14c0226fcbf58e8815d5" + integrity sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +asn1js@^3.0.10, asn1js@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd" + integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg== + dependencies: + pvtsutils "^1.3.6" + pvutils "^1.1.5" + tslib "^2.8.1" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +baseline-browser-mapping@^2.11.25: + version "2.11.26" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz#01bf6774efb3fbb9de20afc6b923ab85c7d230a6" + integrity sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@^1.19.0: + version "1.20.8" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.8.tgz#328d51ce46cb19c983f3d9661016df2765283a37" + integrity sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA== + dependencies: + bytes "~3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "~1.2.0" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + on-finished "~2.4.1" + qs "~6.16.0" + raw-body "~2.5.3" + type-is "~1.6.18" + unpipe "~1.0.0" + +body-parser@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437" + integrity sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw== + dependencies: + bytes "^3.1.2" + content-type "^2.0.0" + debug "^4.4.3" + http-errors "^2.0.1" + iconv-lite "^0.7.2" + on-finished "^2.4.1" + qs "^6.15.2" + raw-body "^3.0.2" + type-is "^2.1.0" + +bonjour-service@^1.3.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.4.4.tgz#8ff5b85fa0641e0996bf40bcf61fc0c6727b66d3" + integrity sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.21" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.21.tgz#edf4fab5c64d051aea5a8def49aba1c7522279f3" + integrity sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.2: + version "2.1.7" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.7.tgz#14ea3836731823bb24740225ea43b941b436f9d1" + integrity sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.28.1: + version "4.29.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.29.1.tgz#f723071987a4f094177d94e01f6dc6e7493b972c" + integrity sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw== + dependencies: + baseline-browser-mapping "^2.11.25" + caniuse-lite "^1.0.30001810" + electron-to-chromium "^1.5.438" + node-releases "^2.0.57" + update-browserslist-db "^1.3.3" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +bytestreamjs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" + integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001810: + version "1.0.30001810" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz#4970b477dea3278374de9bc43aa8f5d39fc3cda2" + integrity sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== + dependencies: + readdirp "^5.0.0" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^14.0.3: + version "14.0.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.3.tgz#425d79b48f9af82fcd9e4fc1ea8af6c5ec07bbc2" + integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.2.tgz#781397127ae2da00c0c5f7c76bda5b2a9b32a49f" + integrity sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + destroy "1.2.0" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-disposition@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17" + integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g== + +content-type@^1.0.5, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +content-type@^2.0.0, content-type@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.1.0.tgz#d9389c43c0a8cf6a355db464d21e07092a40493a" + integrity sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag== + +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@^0.7.1, cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cors@~2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.3, debug@~4.4.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +default-browser-id@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8" + integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== + +default-browser@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.1.tgz#1790affc52680fbb11e17cab2752d69aa2a37d2c" + integrity sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0, destroy@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.438: + version "1.5.439" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.439.tgz#d14ad21f157ee83b77d1e46fc031776eec9013c9" + integrity sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.10" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.10.tgz#67822e942929c10c5918fb6fb7c71bde0fe01ef3" + integrity sha512-9/lX2bdlizlCXMHRMOIm03VBQHQYC7VvydcxtTAUJRxNW1QzM/2PMFSmr6h/lCiMHcyCP6abK+t9Q+j4vekk8Q== + dependencies: + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + "@types/ws" "^8.5.12" + accepts "~1.3.4" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.4.1" + engine.io-parser "~5.2.1" + ws "~8.21.0" + +enhanced-resolve@^5.22.2: + version "5.25.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz#9d919bfa898b116e58f20e95127c047693158771" + integrity sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.3.3" + +ent@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85" + integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + punycode "^1.4.1" + safe-regex-test "^1.1.0" + +envinfo@^7.21.0: + version "7.21.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.21.0.tgz#04a251be79f92548541f37d13c8b6f22940c3bae" + integrity sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^2.1.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz#311fa4f40168c1975c505477c51b23234d41ad55" + integrity sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + dependencies: + es-errors "^1.3.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.1" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + depd "^2.0.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-uri@^3.0.1: + version "3.1.8" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.8.tgz#f7db8d942e20eead3cfe93b0beeb4eb0169e938b" + integrity sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.4.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.4.tgz#aeeca2a506303f0cee61c59e6c9f2a88d2f29fc6" + integrity sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q== + +follow-redirects@^1.0.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regex.js@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.3.1.tgz#2f9fe8821218b18e6dcced9bcb4413932162db4b" + integrity sha512-zhWhMRsgnOym+1txhUmYJGD6eUsbUdrZv55gCYKuJX8t2p3+M8My23PD09bEIsreGUs5pnSa/idmcfp2/IlqGg== + +glob@^10.4.5: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2, hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +html5-qrcode@2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d" + integrity sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ== + +htmx.org@2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-2.0.8.tgz#8ac8ba87c141b7bfda7576117476062eeb4aceda" + integrity sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q== + +http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +http-errors@~1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + +http-proxy-middleware@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-4.2.0.tgz#f0790fc34eb3c42aeb4e908414bc454dc6aa274d" + integrity sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw== + dependencies: + debug "^4.4.3" + httpxy "^0.5.4" + is-glob "^4.0.3" + is-plain-obj "^4.1.0" + micromatch "^4.0.8" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +httpxy@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.5.5.tgz#250363163cc95858dc36233bb08bf9c98a55faba" + integrity sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA== + +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@^0.7.2, iconv-lite@~0.7.0: + version "0.7.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.3.tgz#84ee12f963e7de50bc01a13e160a078b3b0f415f" + integrity sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@~0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +import-local@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.5.0.tgz#7d4b6c39f9392fb61cf807de6e425e22c10e061f" + integrity sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.1: + version "2.17.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.17.0.tgz#75c1c76236c9f7e38ddbfff292e127dc13b21027" + integrity sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA== + dependencies: + hasown "^2.0.4" + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-in-ssh@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz#8eb73c1cabba77748d389588eeea132a63057622" + integrity sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw== + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.3.2.tgz#9460bc30f8419a4bca77114f4de88a3ee5e0c519" + integrity sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-wsl@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f" + integrity sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw== + dependencies: + is-inside-container "^1.0.0" + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb" + integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +"karma@github:Kotlin/karma#6.4.5": + version "6.4.4" + resolved "https://codeload.github.com/Kotlin/karma/tar.gz/239a8fc984584f0d96b1dd750e7a5e2c79da93a6" + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.7.2" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kotlin-web-helpers@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-3.3.0.tgz#4c0bf6bcfa64752f6f1531e4c84583943bc7e9d6" + integrity sha512-sXnwHZ2YGMtIia+NQlf/c0UDtwVNHhXsIH+uHkTv87AvvZQJjJ+XbdkvUUuomPWgBC5TgcQLDEGLcjL48QkFkQ== + dependencies: + format-util "^1.0.5" + +launch-editor@^2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.14.1.tgz#f7e0da3f58aaea03fea01074d840b5f739ed7ddc" + integrity sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA== + dependencies: + picocolors "^1.1.1" + shell-quote "^1.8.4" + +loader-runner@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.2.tgz#9913d3a15971f8f635915e601fb5c9d495d918e9" + integrity sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +media-typer@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.1.tgz#6f035400dfe3ab9d5607bc77546ce30cc2f9c6b8" + integrity sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ== + +memfs@^4.68.2: + version "4.79.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.79.0.tgz#2e007b983d3e27f1b64ba49dfe6fd51e82f51fdb" + integrity sha512-vDFqA9WSa//xa3XIKwMmVqxgWWft4PMKW0k69gBM2rYOzZ3VqL9BrK3Kyl45jSxS7qImDgwejZzEb/xDWhP37g== + dependencies: + "@jsonjoy.com/fs-core" "4.79.0" + "@jsonjoy.com/fs-fsa" "4.79.0" + "@jsonjoy.com/fs-node" "4.79.0" + "@jsonjoy.com/fs-node-builtins" "4.79.0" + "@jsonjoy.com/fs-node-to-fsa" "4.79.0" + "@jsonjoy.com/fs-node-utils" "4.79.0" + "@jsonjoy.com/fs-print" "4.79.0" + "@jsonjoy.com/fs-snapshot" "4.79.0" + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.3.1" + thingies "^2.5.0" + tree-dump "^1.0.3" + tslib "^2.0.0" + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^3.0.0, mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.35: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minimizer-webpack-plugin@^5.6.1: + version "5.12.0" + resolved "https://registry.yarnpkg.com/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.12.0.tgz#acef4a0bd3c5ee405f767a9d90e0173b55b16458" + integrity sha512-PCN1x1OzFeFB2GlP91s46q7Gz8Jf16Z7M45bzpoqrYZlh12QeY6tYVM5h299wbwy1DxnQ69VFmmh8Hb2Ff8mbw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.31" + jest-worker "^27.4.5" + schema-utils "^4.3.3" + terser "^5.51.0" + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@11.7.6: + version "11.7.6" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.6.tgz#ebbe22989d04cbb9424a36307320476624c41a33" + integrity sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA== + dependencies: + browser-stdout "^1.3.1" + chokidar "^4.0.1" + debug "^4.3.5" + diff "^7.0.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^10.4.5" + he "^1.2.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^9.0.5" + ms "^2.1.3" + picocolors "^1.1.1" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^9.2.0" + yargs "^17.7.2" + yargs-parser "^21.1.1" + yargs-unparser "^2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.1.0.tgz#16e003d0db4ac24fd9df168edf871625deeae3df" + integrity sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg== + dependencies: + content-type "^2.1.0" + +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.57: + version "2.0.57" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.57.tgz#47827d34b86a98b4b7afacc1222878c286b6ef2d" + integrity sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@^2.4.1, on-finished@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^11.0.0: + version "11.0.4" + resolved "https://registry.yarnpkg.com/open/-/open-11.0.4.tgz#10d20dfdb558c71e5149b2d789ad6995caf697ca" + integrity sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg== + dependencies: + default-browser "^5.5.1" + define-lazy-prop "^3.0.0" + is-in-ssh "^1.0.0" + is-inside-container "^1.0.0" + powershell-utils "^0.2.1" + wsl-utils "^1.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-8.0.1.tgz#57c6d66995b0a9e9fe3aab9b574411717c56a242" + integrity sha512-NAigyu8hfvJe7MsS18mnc4is0MZtau3QMdBklaJKK23oBQb6occO3UPM3vHmTckW8KhfyjMZaUOqdTFEYliHgg== + dependencies: + is-network-error "^1.3.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz#795c420c4f7ca45c5b887366f622ee0c9852cccd" + integrity sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + +picomatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkijs@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.4.0.tgz#d9164def30ff6d97be2d88966d5e36192499ca9c" + integrity sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw== + dependencies: + "@noble/hashes" "1.4.0" + asn1js "^3.0.6" + bytestreamjs "^2.0.1" + pvtsutils "^1.3.6" + pvutils "^1.1.3" + tslib "^2.8.1" + +powershell-utils@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2" + integrity sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A== + +powershell-utils@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.2.1.tgz#0853016f5b3c12d52b9571c8fe8059677d9ddbe2" + integrity sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A== + +proxy-addr@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932" + integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +pvtsutils@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.3, pvutils@^1.1.5: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.2.0.tgz#4b5487a9cccd52d275b0538775790a3ca982bc22" + integrity sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@^6.14.0, qs@^6.15.2, qs@~6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd" + integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.3.0.tgz#d7f19be812bb62721472b45d3be219ef09572b47" + integrity sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw== + +raw-body@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +raw-body@~2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + unpipe "~1.0.0" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +readdirp@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.1.1.tgz#520bca06f9d1ae1b96cc0800dbe84b983d19422c" + integrity sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +run-applescript@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" + integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== + +safe-buffer@5.2.1, safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^4.3.3: + version "4.5.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.5.0.tgz#aaf8d588bd2b19511175175a5222baef648c009c" + integrity sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA== + dependencies: + "@types/json-schema" "^7.0.15" + ajv "^8.20.0" + ajv-formats "^3.0.1" + ajv-keywords "^5.1.0" + +selfsigned@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-5.5.0.tgz#4c9ab7c7c9f35f18fb6a9882c253eb0e6bd6557b" + integrity sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew== + dependencies: + "@peculiar/x509" "^1.14.2" + pkijs "^3.3.3" + +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.2.tgz#2988e3612106d78a5e4849ddff552ce7bd3d9bcb" + integrity sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ== + dependencies: + accepts "~1.3.8" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.8.0" + mime-types "~2.1.35" + parseurl "~1.3.3" + +serve-static@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +setprototypeof@1.2.0, setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.4: + version "1.10.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.10.0.tgz#482033e192e4f5c07151521ffa03400ec71b1b0f" + integrity sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA== + +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +socket.io-adapter@~2.5.2: + version "2.5.8" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz#c9fcabf602dbd5b09258ca98e5ec6a7ae4360698" + integrity sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw== + dependencies: + debug "~4.4.1" + ws "~8.21.0" + +socket.io-parser@~4.2.4: + version "4.2.7" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz#679e51fe24d1c81df90fc5f7efe4a5f432fe99c0" + integrity sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.4.1" + +socket.io@^4.7.2: + version "4.8.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.3.tgz#ca6ba1431c69532e1e0a6f496deebeb601dbc4df" + integrity sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.4.1" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3" + integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== + dependencies: + ansi-regex "^6.2.2" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.3.0, tapable@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" + integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== + +terser@^5.51.0: + version "5.51.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.51.2.tgz#555912425a7f2ff7737425c3718fa76c7069f6ef" + integrity sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.15.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +thingies@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.6.1.tgz#4eb28e2585a75288f1765a0b08f1dfa020357a6f" + integrity sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tinyglobby@^0.2.15: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tmp@^0.2.1: + version "0.2.7" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059" + integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1, toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tree-dump@^1.0.3, tree-dump@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" + integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== + +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tsyringe@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.10.0.tgz#d0c95815d584464214060285eaaadd94aa03299c" + integrity sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw== + dependencies: + tslib "^1.9.3" + +type-is@^2.0.1, type-is@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.1.0.tgz#71d1a7053293582e16ac9f3ebaf1ab9aa49e5570" + integrity sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA== + dependencies: + content-type "^2.0.0" + media-typer "^1.1.0" + mime-types "^3.0.0" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +ua-parser-js@^0.7.30: + version "0.7.41" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452" + integrity sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg== + +undici-types@~8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.9.0.tgz#e240d97c8b5d85e5347ce73d25865c7906c1ec9f" + integrity sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz#197e21fb2561fa89f8b94fad605a2b296a18993a" + integrity sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1, vary@^1.1.2, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.2.tgz#e12e82d84674266fc1c6dbfe38891b92ff0522ec" + integrity sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg== + dependencies: + graceful-fs "^4.1.2" + +webpack-cli@7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-7.2.1.tgz#180a5b2232ca582f909eccf32c6fb3a56647d294" + integrity sha512-YwSGbcZdfz12DM8JIseVPr3oBb09IgVCVc4vY3oDvZnI/mALTGPAP1QiqOi4/bBLSJrRHaqDIXeHcNA0+G38aw== + dependencies: + "@discoveryjs/json-ext" "^1.1.0" + commander "^14.0.3" + cross-spawn "^7.0.6" + envinfo "^7.21.0" + import-local "^3.2.0" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^6.0.1" + +webpack-dev-middleware@^8.0.3: + version "8.3.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-8.3.0.tgz#509b0dac459efe187d22ae57fb151e6b8be6cf1d" + integrity sha512-jFNNMB29ugmkEUFW9cpWNqcWFQNwuMTMVoRpKVAiz/5nh3gA4x9x6DRPhNHGc/SLre7pEP9yrAPDAmCvdCblKg== + dependencies: + ansi-html-community "^0.0.8" + memfs "^4.68.2" + mime-types "^3.0.2" + range-parser "^1.3.0" + schema-utils "^4.3.3" + +webpack-dev-server@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-6.0.0.tgz#af30f80cd4bc1c5b2c6224e4c7903adcd9514dcd" + integrity sha512-q9SD4ItOGhZLeU6EGT10caDZdHjF50Pz1DtkRZZOPsfluMXOkacWKKOtSBSLVkPqKiF67eFUC0rI88U/tSFPEw== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^5.0.6" + "@types/express-serve-static-core" "^5.1.1" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^2.2.0" + "@types/ws" "^8.18.1" + ansi-html-community "^0.0.8" + bonjour-service "^1.3.0" + chokidar "^5.0.0" + compression "^1.8.1" + connect-history-api-fallback "^2.0.0" + express "^5.2.1" + graceful-fs "^4.2.11" + http-proxy-middleware "^4.1.1" + ipaddr.js "^2.3.0" + launch-editor "^2.14.1" + open "^11.0.0" + p-retry "^8.0.0" + schema-utils "^4.3.3" + selfsigned "^5.5.0" + serve-index "^1.9.2" + tinyglobby "^0.2.15" + webpack-dev-middleware "^8.0.3" + ws "^8.20.0" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + +webpack-sources@^3.5.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.3.tgz#35e2bec56951011bbaaffb450936222113bac031" + integrity sha512-F/zS5rEcCKxS6hrhxkLjOzblTsuv89rkuDyP+vToquds6KfPJ3fDqg9UNzbtJcA2AgXZOLMYLQqZpuyE5zDfxg== + +webpack@5.108.1: + version "5.108.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.108.1.tgz#cd5856f9a88e55f344d64a6623c09bf34446cc9d" + integrity sha512-UUCihHQK3O7483Woa0SulNLDeAiOhHI2PN2PAPU4fVWJqbzhv04EJ8FaWtB9WWh3i8fRt28543U7VfuJTOrpgQ== + dependencies: + "@types/estree" "^1.0.8" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.16.0" + acorn-import-phases "^1.0.3" + browserslist "^4.28.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.22.2" + es-module-lexer "^2.1.0" + eslint-scope "5.1.1" + events "^3.2.0" + graceful-fs "^4.2.11" + loader-runner "^4.3.2" + mime-db "^1.54.0" + minimizer-webpack-plugin "^5.6.1" + neo-async "^2.6.2" + schema-utils "^4.3.3" + tapable "^2.3.0" + watchpack "^2.5.2" + webpack-sources "^3.5.0" + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@^9.2.0: + version "9.3.4" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" + integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.20.0, ws@~8.21.0: + version "8.21.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.3.tgz#660b4faddb6a3e575c86e078126919961f4de4fc" + integrity sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw== + +wsl-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-1.0.0.tgz#e113d9964e766657c53f5d570da1bbbdfee7f837" + integrity sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA== + dependencies: + is-wsl "^3.1.0" + powershell-utils "^0.1.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.1.1: + version "16.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d" + integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.7.2: + version "17.7.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa" + integrity sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..bcadb32 --- /dev/null +++ b/publish.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +function switch_srv() { + local cmd=$1 + ssh container@nathan systemctl --user $cmd kas_srv + return $? +} + +function copy_to_server(){ + rsync -rti --delete --stats server/build/install/server/* container@nathan:/var/containers/vol/kas/server + return $? +} + +./gradlew :server:installDist && switch_srv stop && copy_to_server && switch_srv start diff --git a/server/build.gradle.kts b/server/build.gradle.kts new file mode 100644 index 0000000..d18097e --- /dev/null +++ b/server/build.gradle.kts @@ -0,0 +1,126 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(ktorLibs.plugins.ktor) + alias(libs.plugins.kotlin.serialization) +// alias(libs.plugins.ksp) + alias(masterLibs.plugins.versionPlugin) +} + +group = "net.keinekekse.app" +version = libs.versions.appsrv.get() +description = "Ktor Application Server" +extra["displayname"] = "Application Server" +extra["copyright"] = "Copyright (c) 2026 Hartmut Bauer" + +versionConfig { + versionClassFile.set(layout.projectDirectory.file("src/main/kotlin/Version.kt")) + versionPackageName.set("net.keinekekse.app") + + modules.set( + mapOf( + "books" to mapOf( + "version" to libs.versions.books.get(), + "displayName" to "Books-DB", + "description" to "Books + Comics Database" + ) + ) + ) +} + +application { + mainClass = "io.ktor.server.netty.EngineMain" + + // java >= 24 + // WARNING: A restricted method in java.lang.System has been called + // WARNING: java.lang.System::loadLibrary has been called by io.netty.util.internal.NativeLibraryUtil in an unnamed module + // (file:/Users/hb/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.2.16.Final/c2bc7fa5acfa2afc777e944d84f0aa65559cc81e/netty-common-4.2.16.Final.jar) + // WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module + // WARNING: Restricted methods will be blocked in a future release unless native access is enabled + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") +} + +val buildTailwind = tasks.register("buildTailwind", Exec::class) { + description = "build tailwind css file" + workingDir = rootProject.layout.projectDirectory.dir("tailwind").asFile + // direkt ins buildDir schreiben, weil Task processResources hier zu spät kommt + commandLine( + "/Users/hb/.npm-packages/bin/npx", + "@tailwindcss/cli", + "-i", + "./input.css", + "-o", + layout.buildDirectory.dir("resources/main/static").get() + .toString() + "/styles/tailwind.css", + "--minify" + ) +} +val copyWebDistToServerResources = tasks.register("copyWebDistToServerResources", Copy::class) { + description = "copy web dist to server resources" + dependsOn(project(":web").tasks.named("jsBrowserDistribution")) + from(project(":web").layout.buildDirectory.dir("dist/js/productionExecutable")) + into(layout.buildDirectory.dir("resources/main/web")) + +// from(rootProject.layout.projectDirectory.file("build/js/node_modules/bootstrap/dist/css/bootstrap.min.css")) +// into(layout.buildDirectory.dir("resources/main/web")) +} + +tasks.named("processResources") { + dependsOn(buildTailwind) + dependsOn(copyWebDistToServerResources) +} + +kotlin { + jvmToolchain(libs.versions.jvm.get().toInt()) +} +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.jvm.get())) + } +} + +dependencies { + implementation(project(":shared")) + implementation(masterLibs.utilLibrary) + + implementation(ktorLibs.client.apache5) + implementation(ktorLibs.client.cio) + implementation(ktorLibs.client.core) + implementation(ktorLibs.htmx.html) + implementation(ktorLibs.serialization.kotlinx.json) + implementation(ktorLibs.server.auth) + implementation(ktorLibs.server.auth.oidc) + implementation(ktorLibs.server.auth.jwt) + implementation(ktorLibs.server.cachingHeaders) + implementation(ktorLibs.server.compression) + implementation(ktorLibs.server.config.yaml) + implementation(ktorLibs.server.contentNegotiation) + implementation(ktorLibs.server.core) + implementation(ktorLibs.server.cors) + implementation(ktorLibs.server.csrf) + implementation(ktorLibs.server.defaultHeaders) + implementation(ktorLibs.server.di) + implementation(ktorLibs.server.forwardedHeader) + implementation(ktorLibs.server.htmlBuilder) + implementation(ktorLibs.server.htmx) + implementation(ktorLibs.server.netty) + implementation(ktorLibs.server.statusPages) + implementation(ktorLibs.server.sessions) + implementation(libs.exposed.core) + implementation(libs.exposed.jdbc) + implementation(libs.exposed.dao) + implementation(libs.exposed.r2dbc) + implementation(libs.h2database.h2) + implementation(libs.h2database.r2dbc) + implementation(libs.kotlinx.kotlinxHtml) + implementation(libs.logback.classic) + implementation(libs.postgresql) + implementation(libs.hikaricp) + +// implementation(libs.koin.ktor) +// implementation(libs.koin.logger) +// implementation(libs.koin.annotations) +// ksp(libs.koin.ksp) + +// testImplementation(kotlin("test")) +// testImplementation(ktorLibs.server.testHost) +} diff --git a/server/buildnumber.properties b/server/buildnumber.properties new file mode 100644 index 0000000..3ded9e4 --- /dev/null +++ b/server/buildnumber.properties @@ -0,0 +1,3 @@ +#auto generated +#Fri Oct 09 01:13:32 CEST 2026 +build.number=533 diff --git a/server/src/main/kotlin/Version.kt b/server/src/main/kotlin/Version.kt new file mode 100644 index 0000000..66974b3 --- /dev/null +++ b/server/src/main/kotlin/Version.kt @@ -0,0 +1,32 @@ +package net.keinekekse.app + +import net.keinekekse.version.Version + +object VERSION: Version { + override val version = "0.2.0-alpha" + override val fullVersion = "0.2.0-alpha+build.533" + override val major = 0 + override val minor = 2 + override val patch = 0 + override val preRelease = "alpha" + override val buildMetadata = "build.533" + override val buildDate = "2026-10-08T23:13:32.459638Z" + override val displayName = "Application Server" + override val copyright = "Copyright (c) 2026 Hartmut Bauer" + override val name = "server" + override val description = "Ktor Application Server" +} +object VERSION_books: Version { + override val version = "0.2.0-alpha" + override val fullVersion = "0.2.0-alpha+build.533" + override val major = 0 + override val minor = 2 + override val patch = 0 + override val preRelease = "alpha" + override val buildMetadata = "build.533" + override val buildDate = "2026-10-08T23:13:32.459742Z" + override val displayName = "Books-DB" + override val copyright = "Copyright (c) 2026 Hartmut Bauer" + override val name = "books" + override val description = "Books + Comics Database" +} \ No newline at end of file diff --git a/server/src/main/kotlin/books/config/DI.kt b/server/src/main/kotlin/books/config/DI.kt new file mode 100644 index 0000000..299b143 --- /dev/null +++ b/server/src/main/kotlin/books/config/DI.kt @@ -0,0 +1,18 @@ +package net.keinekekse.app.books.config + +import io.ktor.server.application.* +import io.ktor.server.plugins.di.* +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.db.BookRepositoryImpl + +fun Application.configureBookDependencyInjection() { + log.info("Configuring book dependency injection") + + val booksDb = configureBooksDatabase() + + dependencies { + provide("booksDb") { booksDb } + provide { BookRepositoryImpl(booksDb) } + } + +} diff --git a/server/src/main/kotlin/books/config/Database.kt b/server/src/main/kotlin/books/config/Database.kt new file mode 100644 index 0000000..824d77f --- /dev/null +++ b/server/src/main/kotlin/books/config/Database.kt @@ -0,0 +1,34 @@ +package net.keinekekse.app.books.config + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.ktor.server.application.* +import net.keinekekse.app.core.config.getKeyFromConfiguration +import org.jetbrains.exposed.v1.core.DatabaseConfig +import org.jetbrains.exposed.v1.jdbc.Database + +fun Application.configureBooksDatabase(): Database { + log.info("Configuring books database") + val config = HikariConfig().apply { + dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" + username = getKeyFromConfiguration("postgres.books_schema") + password = getKeyFromConfiguration("postgres.books_pwd") + + addDataSourceProperty("serverName", getKeyFromConfiguration("postgres.host", "localhost")) + addDataSourceProperty("portNumber", getKeyFromConfiguration("postgres.port", "5432")) + addDataSourceProperty("databaseName", username) + + maximumPoolSize = 6 + isReadOnly = false + transactionIsolation = "TRANSACTION_SERIALIZABLE" + } + + val dataSource = HikariDataSource(config) + + return Database.connect( + datasource = dataSource, + databaseConfig = DatabaseConfig { + // set other parameters here + } + ) +} diff --git a/server/src/main/kotlin/books/config/Routing.kt b/server/src/main/kotlin/books/config/Routing.kt new file mode 100644 index 0000000..59f90a1 --- /dev/null +++ b/server/src/main/kotlin/books/config/Routing.kt @@ -0,0 +1,177 @@ +package net.keinekekse.app.books.config + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.oidc.* +import io.ktor.server.html.* +import io.ktor.server.htmx.* +import io.ktor.server.plugins.di.annotations.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.utils.io.* +import kotlinx.html.div +import kotlinx.html.p +import kotlinx.html.span +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.db.CalibreBookInfoRepositoryImpl +import net.keinekekse.app.books.scan.getCover +import net.keinekekse.app.books.scan.getIsbnFromCalibre +import net.keinekekse.app.books.ui.* +import net.keinekekse.app.core.auth.UserRoles +import net.keinekekse.app.core.config.KeycloakSession +import net.keinekekse.app.core.config.onUnauthorized +import org.jetbrains.exposed.v1.jdbc.Database + +var thumbnailUrl: String? = null + +@OptIn(ExperimentalKtorApi::class) +fun Application.configureRouting( + @Named("booksDb") db: Database, + booksRepo: BookRepository, + keycloakSession: KeycloakSession, +// resultCallback: (String, String) -> Unit, +) { + log.info("Configuring books routing") + + routing { + authenticateWith( + keycloakSession, + roles = setOf(UserRoles.BOOKS_USER), + onUnauthorized = { onUnauthorized(call) } + ) { + // Start + get(navBooks.second) { + log.info(" ### Books page requested") + + val p = call.principal + call.application.log.info("AppPrincipal userId: ${p.userId}") + + val pp = call.principal() + call.application.log.info("OidcToken.Id.Principal userId: ${pp?.userInfo?.preferredUsername}") + + call.respondHtml { + testPage(db) + } + } + log.info(" ### Books page done") + + // New/ Import + get(navBooksNew.second) { + val books = CalibreBookInfoRepositoryImpl(db).readAllBookInfo() + call.respondHtml { + importPage(books) + } + } + get(navBooksScan.second) { + call.respondHtml { + scanPage() + } + } + get("${navBooksScanEdit.second}/{id}") { + val id = call.parameters["id"]?.toInt() ?: 0 + val bookInfo = CalibreBookInfoRepositoryImpl(db).readBookInfo(id) + call.respondHtml { + importEditPage(bookInfo, booksRepo) + } + } + delete("${navBooksScanEdit.second}/{id}") { + val id = call.parameters["id"]?.toInt() ?: 0 + val biRepo = CalibreBookInfoRepositoryImpl(db) + val bookInfo = biRepo.readBookInfo(id) + if (bookInfo != null) { + biRepo.removeBookInfo(bookInfo.id) + } + val books = biRepo.readAllBookInfo() + call.respondHtml { + importPage(books) + } + } + + hx.get("${navBooksScanFetch.second}/{isbn}") { +// val isbn = call.request.queryParameters["isbn"] ?: "" // "?isbn=..." + val isbn = call.parameters["isbn"] ?: "" + call.application.log.info("ISBN lookup for $isbn") + val bookInfo = getIsbnFromCalibre(isbn, db) + val image = getCover(isbn) + val dataUrl = embedImageURL(image) + call.respondHtmlPartial(status = HttpStatusCode.OK) { + isbnResultCallback(bookInfo, dataUrl) + } + } + + // Edit + get(navBooksEdit.second) { + // TODO + } + hx.post(navBooksEdit.second) { + call.application.log.info("Edit book") + call.receiveParameters().forEach { param: String, value: List -> + call.application.log.info("param: $param, value: $value") + } + + call.respondHtmlPartial(HttpStatusCode.OK) { + p { + +"OK" + } + } + } +// hx.get("/isbnlookupreset") { +// call.respondHtmlPartial(HttpStatusCode.OK) { +// p { +"Bitte Barcode vor die Kamera halten..." } +// } +// } +// get("/testimage/{isbn}") { +// val isbn = call.parameters["isbn"] ?: "" +// val image = getCover(isbn) +// call.application.log.info("/testimage img len ${image.size}") +// call.respondBytes( +// bytes = image, +// contentType = ContentType.Image.JPEG, +// status = HttpStatusCode.OK +// ) +// } + // "hx." lässt den Aufruf nur per HTMX zu. Der Pfad kann nicht + // direkt im Browser aufgerufen werden. + hx.get("/books/testdb") { + log.info(" ### DB test requested") + dbTest(db, booksRepo) + call.respondHtmlPartial(HttpStatusCode.OK) { + span { + +"DB created" + } + } + } + hx.post("/books/testform") { + log.info(" ### Form test requested") + val inp = call.receiveParameters()["testInput"] + log.info(" ### Form input: $inp") + +// val formContent = call.receiveParameters() +// formContent.forEach { key, value -> // key -> input field name +// call.application.log.info(" ### Form key: $key") // == formContent[key] +// for (v in value) { +// call.application.log.info(" ### Form value(s): $v") +// } +// } + call.respondHtmlPartial(HttpStatusCode.OK) { + div { + +"form submitted: $inp" + } + } + } + + // Test 401 + authenticateWith( + keycloakSession, + roles = setOf(UserRoles.BOOKS_USER), +// onUnauthorized = { onUnauthorized(call) } + ) { + get("/test") { + call.respondText("Test") + } + } + } + } +} diff --git a/server/src/main/kotlin/books/db/BooksRepository.kt b/server/src/main/kotlin/books/db/BooksRepository.kt new file mode 100644 index 0000000..9dccc76 --- /dev/null +++ b/server/src/main/kotlin/books/db/BooksRepository.kt @@ -0,0 +1,18 @@ +package net.keinekekse.app.books.db + +import net.keinekekse.app.books.model.* + +interface BookRepository { + suspend fun allBooks(): List + suspend fun bookById(id: Int): Book? + suspend fun booksBySeries(series: Series): List + suspend fun bookByTitle(title: String): List + suspend fun booksByArtist(artist: Artist): List + suspend fun addBook(book: Book) + suspend fun removeBook(id: Int): Boolean + + suspend fun allPublishers(): List + suspend fun allArtists(): List + suspend fun allSeries(): List + suspend fun allTags(): List +} diff --git a/server/src/main/kotlin/books/db/BooksRepositoryImpl.kt b/server/src/main/kotlin/books/db/BooksRepositoryImpl.kt new file mode 100644 index 0000000..56e3b7e --- /dev/null +++ b/server/src/main/kotlin/books/db/BooksRepositoryImpl.kt @@ -0,0 +1,61 @@ +package net.keinekekse.app.books.db + +import net.keinekekse.app.books.db.schema.* +import net.keinekekse.app.books.model.* +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.like +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction + +class BookRepositoryImpl(val booksDb: Database) : BookRepository { + override suspend fun allBooks(): List = suspendTransaction(booksDb) { + BookDAO.all().map(::bookDaoToModel) + } + + override suspend fun bookById(id: Int): Book? = suspendTransaction(booksDb) { + BookDAO.findById(id)?.let { bookDaoToModel(it) } + } + + // TODO noch zu testen + override suspend fun booksBySeries(series: Series): List = suspendTransaction(booksDb) { + BookDAO.find { BooksTable.series eq series.id }.map(::bookDaoToModel) + } + + // TODO noch zu testen + override suspend fun bookByTitle(title: String): List = suspendTransaction(booksDb) { + val comp = "%$title%" + BookDAO.find { BooksTable.title like comp }.map(::bookDaoToModel) + } + + // TODO noch zu testen + override suspend fun booksByArtist(artist: Artist): List = suspendTransaction(booksDb) { + BookDAO.find { (ArtistToBookTable.artistId eq artist.id) and (ArtistToBookTable.bookId eq BooksTable.id) } + .map(::bookDaoToModel) + } + + override suspend fun addBook(book: Book) { + TODO("Not yet implemented") + } + + override suspend fun removeBook(id: Int): Boolean { + TODO("Not yet implemented") + } + + override suspend fun allPublishers(): List = suspendTransaction(booksDb) { + PublisherDAO.all().map(::publisherDaoToModel) + } + + override suspend fun allArtists(): List = suspendTransaction(booksDb) { + ArtistDAO.all().map(::artistDaoToModel) + } + + override suspend fun allSeries(): List = suspendTransaction(booksDb) { + SeriesDAO.all().map(::seriesDaoToModel) + } + + override suspend fun allTags(): List = suspendTransaction(booksDb) { + TagsDAO.all().map(::tagDaoToModel) + } + +} diff --git a/server/src/main/kotlin/books/db/CalibreBookInfoRepository.kt b/server/src/main/kotlin/books/db/CalibreBookInfoRepository.kt new file mode 100644 index 0000000..e49b344 --- /dev/null +++ b/server/src/main/kotlin/books/db/CalibreBookInfoRepository.kt @@ -0,0 +1,11 @@ +package net.keinekekse.app.books.db + +import net.keinekekse.app.books.db.schema.CalibreBookInfoDAO +import net.keinekekse.app.books.model.CalibreBookInfo + +interface CalibreBookInfoRepository { + suspend fun addBookInfo(bookInfo: CalibreBookInfo): CalibreBookInfoDAO + suspend fun readAllBookInfo(): List + suspend fun readBookInfo(id: Int): CalibreBookInfo? + suspend fun removeBookInfo(id: Int) +} diff --git a/server/src/main/kotlin/books/db/CalibreBookInfoRepositoryImpl.kt b/server/src/main/kotlin/books/db/CalibreBookInfoRepositoryImpl.kt new file mode 100644 index 0000000..58d1381 --- /dev/null +++ b/server/src/main/kotlin/books/db/CalibreBookInfoRepositoryImpl.kt @@ -0,0 +1,31 @@ +package net.keinekekse.app.books.db + +import net.keinekekse.app.books.db.schema.CalibreBookInfoDAO +import net.keinekekse.app.books.db.schema.calibreBookInfoDaoToModel +import net.keinekekse.app.books.db.schema.modelToCalibreBookInfoDao +import net.keinekekse.app.books.model.CalibreBookInfo +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction + +class CalibreBookInfoRepositoryImpl(val booksDb: Database) : CalibreBookInfoRepository { + override suspend fun addBookInfo(bookInfo: CalibreBookInfo) = + suspendTransaction(booksDb) { + modelToCalibreBookInfoDao(bookInfo) + } + + override suspend fun readAllBookInfo(): List = + suspendTransaction(booksDb) { + CalibreBookInfoDAO.all().map(::calibreBookInfoDaoToModel) + } + + override suspend fun readBookInfo(id: Int): CalibreBookInfo? = + suspendTransaction(booksDb) { + CalibreBookInfoDAO.findById(id)?.let { calibreBookInfoDaoToModel(it) } + } + + override suspend fun removeBookInfo(id: Int) = + suspendTransaction(booksDb) { + CalibreBookInfoDAO[id].delete() + } + +} diff --git a/server/src/main/kotlin/books/db/schema/ArtistDAO.kt b/server/src/main/kotlin/books/db/schema/ArtistDAO.kt new file mode 100644 index 0000000..7e8de43 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/ArtistDAO.kt @@ -0,0 +1,21 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.Artist +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object ArtistsTable : IntIdTable("artists") { + var name = varchar("name", 100).index() + var givenName = varchar("given_name", 100).index() +} + +class ArtistDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(ArtistsTable) + + var name by ArtistsTable.name + var givenName by ArtistsTable.givenName +} + +fun artistDaoToModel(dao: ArtistDAO) = Artist(dao.id.value, dao.name, dao.givenName) diff --git a/server/src/main/kotlin/books/db/schema/ArtistRoleDAO.kt b/server/src/main/kotlin/books/db/schema/ArtistRoleDAO.kt new file mode 100644 index 0000000..eb6707a --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/ArtistRoleDAO.kt @@ -0,0 +1,19 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.ArtistRole +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object ArtistRolesTable : IntIdTable("artist_roles") { + var role = varchar("role", 50).index() +} + +class ArtistRoleDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(ArtistRolesTable) + + var role by ArtistRolesTable.role +} + +fun artistToRoleDaoToModel(dao: ArtistRoleDAO) = ArtistRole(dao.id.value, dao.role) diff --git a/server/src/main/kotlin/books/db/schema/ArtistToBookDAO.kt b/server/src/main/kotlin/books/db/schema/ArtistToBookDAO.kt new file mode 100644 index 0000000..d44f924 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/ArtistToBookDAO.kt @@ -0,0 +1,20 @@ +package net.keinekekse.app.books.db.schema + +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object ArtistToBookTable : IntIdTable("artist_to_book") { + val artistId = reference("artist_id", ArtistsTable.id).index() + val bookId = reference("book_id", BooksTable.id).index() + val role = reference("role_id", ArtistRolesTable.id).index() +} + +class ArtistToBookDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(ArtistToBookTable) + + var artist by ArtistDAO referencedOn ArtistToBookTable.artistId + var book by BookDAO referencedOn ArtistToBookTable.bookId + var role by ArtistRoleDAO referencedOn ArtistToBookTable.role +} diff --git a/server/src/main/kotlin/books/db/schema/BookDAO.kt b/server/src/main/kotlin/books/db/schema/BookDAO.kt new file mode 100644 index 0000000..2064709 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/BookDAO.kt @@ -0,0 +1,91 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.* +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object BooksTable : IntIdTable("books") { + var title = varchar("title", 150).index() + var subTitle = varchar("sub_title", 150).nullable() + var originalTitle = varchar("original_title", 150).nullable() + var year = integer("year") + var originalYear = integer("original_year").nullable() + var language = varchar("language", 2) + var isbn = varchar("isbn", 13).index() + var publisher = reference("publisher_id", PublisherTable.id) + var series = optReference("series_id", SeriesTable.id) + var seriesIndex = integer("seriesIndex").nullable() + var comments = varchar("comments", 255) + var parentBook = optReference("parent_book_id", id) + val cover = blob("cover").nullable() +} + +class BookDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(BooksTable) + + var title by BooksTable.title + var subTitle by BooksTable.subTitle + var seriesIndex by BooksTable.seriesIndex + var originalTitle by BooksTable.originalTitle + var year by BooksTable.year + var originalYear by BooksTable.originalYear + var language: Languages by BooksTable.language.transform({ it.name }, { Languages.valueOf(it) }) + var isbn by BooksTable.isbn + var publisher by PublisherDAO.Companion referencedOn BooksTable.publisher + var comments by BooksTable.comments + var cover by BooksTable.cover + + var series by SeriesDAO.Companion optionalReferencedOn BooksTable.series + val artists by ArtistToBookDAO referrersOn ArtistToBookTable.bookId + val tags by TagsToBookDAO referrersOn TagsToBookTable.bookId + + // Verweis auf Sammelband (falls dieses Buch ein Teilroman ist) + var parentBook by BookDAO optionalReferencedOn BooksTable.parentBook + + // Enthaltene Einzelromane (falls dieses Buch der Sammelband ist) + val containedBooks by BookDAO optionalReferrersOn BooksTable.parentBook +} + +fun bookDaoToModel(dao: BookDAO, includeChildren: Boolean = true): Book = Book( + dao.id.value, + dao.title, + dao.subTitle, + dao.originalTitle, + dao.year, + dao.originalYear, + dao.language, + dao.isbn, + publisher = Publisher(dao.publisher.id.value, dao.publisher.name), + dao.comments, + cover = dao.cover?.bytes ?: ByteArray(0), + + series = if (dao.series != null) Series(dao.series!!.id.value, dao.series!!.name) else null, + seriesIndex = dao.seriesIndex, + + dao.parentBook?.id?.value, + containedBooks = if (includeChildren) { + dao.containedBooks.map { bookDaoToModel(it, includeChildren = false) } + } else { + emptyList() + }, + + artists = dao.artists.map { + ArtistForBook( + Artist(it.artist.id.value, it.artist.name, it.artist.givenName), + ArtistRole(it.role.id.value, it.role.role) + ) + }, + tags = dao.tags.map { + Tag(it.tag.id.value, it.tag.tag) + }, +) + +fun emptyBook(title: String = "", year: Int = 0, isbn: String = ""): Book = + Book( + title = title, + year = year, + isbn = isbn, + publisher = Publisher(0, ""), + ) diff --git a/server/src/main/kotlin/books/db/schema/CalibreBookInfoDAO.kt b/server/src/main/kotlin/books/db/schema/CalibreBookInfoDAO.kt new file mode 100644 index 0000000..0f9039a --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/CalibreBookInfoDAO.kt @@ -0,0 +1,65 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.CalibreBookInfo +import net.keinekekse.app.books.model.Languages +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.core.statements.api.ExposedBlob +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object CalibreBookInfoTable : IntIdTable("calibre_book_info") { + var title = varchar("title", 150) + val authors = varchar("authors", 200) + val publisher = varchar("publisher", 100) + val tags = varchar("tags", 200) + val language = varchar("language", 2) + val published = integer("published") + val identifiers = varchar("identifiers", 200) + val isbn = varchar("isbn", 20) + val comments = varchar("comments", 255) + val cover = blob("cover") +} + +class CalibreBookInfoDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(CalibreBookInfoTable) + + var title by CalibreBookInfoTable.title + var authors by CalibreBookInfoTable.authors + var publisher by CalibreBookInfoTable.publisher + var tags by CalibreBookInfoTable.tags + var language by CalibreBookInfoTable.language.transform({ it.name }, { Languages.valueOf(it) }) + var published by CalibreBookInfoTable.published + var identifiers by CalibreBookInfoTable.identifiers + var isbn by CalibreBookInfoTable.isbn + var comments by CalibreBookInfoTable.comments + var cover by CalibreBookInfoTable.cover +} + +fun calibreBookInfoDaoToModel(dao: CalibreBookInfoDAO): CalibreBookInfo = CalibreBookInfo( + dao.id.value, + dao.title, + dao.authors, + dao.publisher, + dao.tags, + dao.language, + dao.published, + dao.identifiers, + dao.isbn, + dao.comments, + dao.cover.bytes +) + +fun modelToCalibreBookInfoDao(bookInfo: CalibreBookInfo): CalibreBookInfoDAO = + CalibreBookInfoDAO.new { + title = bookInfo.title.take(150) + authors = bookInfo.authors.take(200) + publisher = bookInfo.publisher.take(100) + tags = bookInfo.tags.take(200) + language = bookInfo.language + published = bookInfo.published + identifiers = bookInfo.identifiers.take(200) + isbn = bookInfo.isbn.take(20) + comments = bookInfo.comments.take(255) + cover = ExposedBlob(bookInfo.cover) + } diff --git a/server/src/main/kotlin/books/db/schema/PublisherDAO.kt b/server/src/main/kotlin/books/db/schema/PublisherDAO.kt new file mode 100644 index 0000000..9631f14 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/PublisherDAO.kt @@ -0,0 +1,19 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.Publisher +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object PublisherTable : IntIdTable("publisher") { + var name = varchar("name", 100).index() +} + +class PublisherDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(PublisherTable) + + var name by PublisherTable.name +} + +fun publisherDaoToModel(dao: PublisherDAO) = Publisher(dao.id.value, dao.name) diff --git a/server/src/main/kotlin/books/db/schema/SeriesDAO.kt b/server/src/main/kotlin/books/db/schema/SeriesDAO.kt new file mode 100644 index 0000000..2645aa1 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/SeriesDAO.kt @@ -0,0 +1,19 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.Series +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object SeriesTable : IntIdTable("series") { + var name = varchar("name", 100).index() +} + +class SeriesDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(SeriesTable) + + var name by SeriesTable.name +} + +fun seriesDaoToModel(dao: SeriesDAO) = Series(dao.id.value, dao.name) diff --git a/server/src/main/kotlin/books/db/schema/TagDAO.kt b/server/src/main/kotlin/books/db/schema/TagDAO.kt new file mode 100644 index 0000000..e6521e5 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/TagDAO.kt @@ -0,0 +1,19 @@ +package net.keinekekse.app.books.db.schema + +import net.keinekekse.app.books.model.Tag +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object TagsTable : IntIdTable("tags") { + var tag = varchar("tag", 50).index() +} + +class TagsDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TagsTable) + + var tag by TagsTable.tag +} + +fun tagDaoToModel(dao: TagsDAO): Tag = Tag(dao.id.value, dao.tag) diff --git a/server/src/main/kotlin/books/db/schema/TagsToBookDAO.kt b/server/src/main/kotlin/books/db/schema/TagsToBookDAO.kt new file mode 100644 index 0000000..df4d557 --- /dev/null +++ b/server/src/main/kotlin/books/db/schema/TagsToBookDAO.kt @@ -0,0 +1,18 @@ +package net.keinekekse.app.books.db.schema + +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.IntIdTable +import org.jetbrains.exposed.v1.dao.IntEntity +import org.jetbrains.exposed.v1.dao.IntEntityClass + +object TagsToBookTable : IntIdTable("tag_to_book") { + var tagId = reference("tag_id", TagsTable.id).index() + var bookId = reference("book_id", BooksTable.id).index() +} + +class TagsToBookDAO(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TagsToBookTable) + + var tag by TagsDAO referencedOn TagsToBookTable.tagId + var book by BookDAO referencedOn TagsToBookTable.bookId +} diff --git a/server/src/main/kotlin/books/model/Artist.kt b/server/src/main/kotlin/books/model/Artist.kt new file mode 100644 index 0000000..129e3d3 --- /dev/null +++ b/server/src/main/kotlin/books/model/Artist.kt @@ -0,0 +1,10 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Artist( + val id: Int = 0, + val name: String = "", + val givenName: String = "", +) diff --git a/server/src/main/kotlin/books/model/ArtistRole.kt b/server/src/main/kotlin/books/model/ArtistRole.kt new file mode 100644 index 0000000..da1c128 --- /dev/null +++ b/server/src/main/kotlin/books/model/ArtistRole.kt @@ -0,0 +1,9 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class ArtistRole( + val id: Int = 0, + val role: String = "", +) diff --git a/server/src/main/kotlin/books/model/Book.kt b/server/src/main/kotlin/books/model/Book.kt new file mode 100644 index 0000000..f17866e --- /dev/null +++ b/server/src/main/kotlin/books/model/Book.kt @@ -0,0 +1,76 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class ArtistForBook(val artist: Artist, val role: ArtistRole) + +@Serializable +data class Book( + val id: Int = 0, + val title: String = "", + val subTitle: String? = null, + val originalTitle: String? = null, + val year: Int = 0, + val originalYear: Int? = null, + val language: Languages = Languages.DE, + val isbn: String = "", + val publisher: Publisher, + val comments: String = "", + val cover: ByteArray = byteArrayOf(0), + val series: Series? = null, + val seriesIndex: Int? = null, + val parentBookId: Int? = null, // ID des Sammelbands + + val containedBooks: List = emptyList(), // Enthaltene Romane + val artists: List = emptyList(), + val tags: List = emptyList(), +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Book + + if (id != other.id) return false + if (year != other.year) return false + if (originalYear != other.originalYear) return false + if (seriesIndex != other.seriesIndex) return false + if (parentBookId != other.parentBookId) return false + if (title != other.title) return false + if (subTitle != other.subTitle) return false + if (originalTitle != other.originalTitle) return false + if (language != other.language) return false + if (isbn != other.isbn) return false + if (publisher != other.publisher) return false + if (comments != other.comments) return false + if (!cover.contentEquals(other.cover)) return false + if (series != other.series) return false + if (containedBooks != other.containedBooks) return false + if (artists != other.artists) return false + if (tags != other.tags) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + year + result = 31 * result + originalYear.hashCode() + result = 31 * result + seriesIndex.hashCode() + result = 31 * result + parentBookId.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + subTitle.hashCode() + result = 31 * result + originalTitle.hashCode() + result = 31 * result + language.hashCode() + result = 31 * result + isbn.hashCode() + result = 31 * result + publisher.hashCode() + result = 31 * result + comments.hashCode() + result = 31 * result + cover.contentHashCode() + result = 31 * result + series.hashCode() + result = 31 * result + containedBooks.hashCode() + result = 31 * result + artists.hashCode() + result = 31 * result + tags.hashCode() + return result + } +} diff --git a/server/src/main/kotlin/books/model/CalibreBookInfo.kt b/server/src/main/kotlin/books/model/CalibreBookInfo.kt new file mode 100644 index 0000000..fd69e62 --- /dev/null +++ b/server/src/main/kotlin/books/model/CalibreBookInfo.kt @@ -0,0 +1,18 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class CalibreBookInfo( + val id: Int = 0, + val title: String, + val authors: String, + val publisher: String, + val tags: String, + val language: Languages, + val published: Int, + val identifiers: String, + val isbn: String, + val comments: String, + val cover: ByteArray, +) diff --git a/server/src/main/kotlin/books/model/Languages.kt b/server/src/main/kotlin/books/model/Languages.kt new file mode 100644 index 0000000..291efc2 --- /dev/null +++ b/server/src/main/kotlin/books/model/Languages.kt @@ -0,0 +1,7 @@ +package net.keinekekse.app.books.model + +enum class Languages { + DE, + EN, + FR, +} diff --git a/server/src/main/kotlin/books/model/Publisher.kt b/server/src/main/kotlin/books/model/Publisher.kt new file mode 100644 index 0000000..19a089f --- /dev/null +++ b/server/src/main/kotlin/books/model/Publisher.kt @@ -0,0 +1,6 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Publisher(val id: Int, val name: String) diff --git a/server/src/main/kotlin/books/model/Series.kt b/server/src/main/kotlin/books/model/Series.kt new file mode 100644 index 0000000..472fc20 --- /dev/null +++ b/server/src/main/kotlin/books/model/Series.kt @@ -0,0 +1,6 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Series(val id: Int, val name: String) diff --git a/server/src/main/kotlin/books/model/Tag.kt b/server/src/main/kotlin/books/model/Tag.kt new file mode 100644 index 0000000..bbd60b5 --- /dev/null +++ b/server/src/main/kotlin/books/model/Tag.kt @@ -0,0 +1,6 @@ +package net.keinekekse.app.books.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Tag(val id: Int, val tag: String) diff --git a/server/src/main/kotlin/books/scan/fetchBookInfo.kt b/server/src/main/kotlin/books/scan/fetchBookInfo.kt new file mode 100644 index 0000000..a0ecd55 --- /dev/null +++ b/server/src/main/kotlin/books/scan/fetchBookInfo.kt @@ -0,0 +1,98 @@ +package net.keinekekse.app.books.scan + +import io.ktor.util.logging.* +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import net.keinekekse.app.books.db.CalibreBookInfoRepositoryImpl +import net.keinekekse.app.books.model.CalibreBookInfo +import net.keinekekse.app.books.model.Languages +import net.keinekekse.exec.CommandExecutionException +import net.keinekekse.exec.ExecutionResult +import org.jetbrains.exposed.v1.jdbc.Database +import java.io.File +import kotlin.time.Instant + +val LOG = KtorSimpleLogger("net.keinekekse.app.books.scan.fetchBookInfo") + +suspend fun getIsbnFromCalibre(isbn: String, db: Database): String { + try { + val runResult = ExecutionResult( + listOf( + "fetch-ebook-metadata", + "--isbn", + isbn, + "--cover", + "/opt/app/download/$isbn.jpg" + ) + ) + LOG.info("fetch-ebook-metadata for isbn: $isbn") + LOG.info("RC = ${runResult.rc}") + LOG.info("Output = ${runResult.stdout}") + LOG.info("Error = ${runResult.stderr}") + + val cali = extractData(runResult.stdout, isbn) + + CalibreBookInfoRepositoryImpl(db).addBookInfo(cali) + + LOG.info("CalibreBookInfo.language = ${cali.language}") + LOG.info("CalibreBookInfo.published = ${cali.published}") + return runResult.stdout + } catch (e: CommandExecutionException) { + LOG.error("Error running fetch-ebook-metadata for isbn: $isbn", e) + return "nothing found for $isbn" + } + +} + +fun extractData(input: String, isbn: String): CalibreBookInfo { + val title = extractLine(input, "Title")?.trim() ?: "" + val authors = extractLine(input, "Author.s.")?.trim() ?: "" + val publisher = extractLine(input, "Publisher")?.trim() ?: "" + val tags = extractLine(input, "Tags")?.trim() ?: "" + val languages = extractLine(input, "Languages")?.trim() ?: "" + val published = extractLine(input, "Published")?.trim() ?: "" + val identifiers = extractLine(input, "Identifiers")?.trim() ?: "" + val comments = extractLine(input, "Comments")?.trim() ?: "" + +// val authorList = authors.split("&").map { it.trim() } + val lang = Languages.valueOf(languages.uppercase().slice(0..1)) + val year = Instant.parse(published).toLocalDateTime(TimeZone.UTC).year + + val cover = getCover(isbn) + + return CalibreBookInfo( + 0, + title, + authors, + publisher, + tags, + lang, + year, + identifiers, + isbn, + comments, + cover + ) +} + +fun extractLine(input: String, text: String): String? { + val pat = """$text\s*:\s*(.*)""" + val regex = Regex(pat) + return regex.find(input)?.groupValues?.get(1) +// if (matchResult != null) { +// val title = matchResult.groupValues[1] +// LOG.info("Title: $title") +// } +} + +fun getCover(isbn: String): ByteArray { + var coverBytes: ByteArray = ByteArray(0) + val file = File("/opt/app/download/$isbn.jpg") + if (file.exists()) { + LOG.info("Cover file exists") + coverBytes = file.readBytes() + } else { + LOG.info("Cover file does not exist") + } + return coverBytes +} diff --git a/server/src/main/kotlin/books/ui/BookEdit.kt b/server/src/main/kotlin/books/ui/BookEdit.kt new file mode 100644 index 0000000..2af7436 --- /dev/null +++ b/server/src/main/kotlin/books/ui/BookEdit.kt @@ -0,0 +1,202 @@ +package net.keinekekse.app.books.ui + +import io.ktor.htmx.html.* +import kotlinx.coroutines.runBlocking +import kotlinx.html.* +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.model.* +import net.keinekekse.app.books.model.Tag + +fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { + + val publishers: List = runBlocking { + booksRepo.allPublishers() + } + val series: List = runBlocking { + booksRepo.allSeries() + } + val artists: List = runBlocking { + booksRepo.allArtists() + } + val tags: List = runBlocking { + booksRepo.allTags() + } + + div(classes = "flex flex-col m-8") { + form { + attributes.hx { + post = navBooksEdit.second + target = "#response" + swap = "innerHTML" + } + + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30 ") { + +"Titel:" + } + input(classes = "$cssTextInput w-150") { + name = "title" + tabIndex = "1" + value = book.title + } + + } + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30 ") { + +"Originaltitel:" + } + input(classes = "$cssTextInput w-150") { + name = "originaltitle" + tabIndex = "2" + value = book.originalTitle ?: "" + } + + } + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30 ") { + +"Untertitel:" + } + input(classes = "$cssTextInput w-150") { + name = "subtitle" + tabIndex = "3" + value = book.subTitle ?: "" + } + } + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30") { + +"Sprache" + } + select(classes = "$cssTextInput w-16") { + name = "language" + tabIndex = "4" + + for (language in Languages.entries) { + option { + if (language == book.language) + selected = true + value = language.ordinal.toString() + +language.name + } + } + } + + div(classes = "ml-9 pr-3 ") { + +"Erscheinungsjahr:" + } + input(classes = "$cssTextInput w-16") { + name = "year" + tabIndex = "5" + value = book.year.toString() + } + + div(classes = "ml-8 pr-3") { + +"Jahr der Originalausgabe:" + } + input(classes = "$cssTextInput w-16") { + name = "originalyear" + tabIndex = "6" + value = book.originalYear?.toString() ?: "" + } + + } + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30") { + +"ISBN" + } + input(classes = "$cssTextInput w-40") { + name = "isbn" + tabIndex = "7" + value = book.isbn + } + + div(classes = "ml-14 pr-3") { + +"Verlag" + } + select(classes = "$cssTextInput w-74") { + name = "publisher" + tabIndex = "8" + + option { + value = "0" + +"---" + } + for (pub in publishers) { + option { + if (pub == book.publisher) + selected = true + value = pub.id.toString() + +pub.name + } + } + } + a(classes = cssFormIcon) { + tabIndex = "9" + title = "Verlag hinzufügen" + materialSymbolFormButton("add_box") + } + + } + + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30") { + +"Serie" + } + select(classes = "$cssTextInput w-98") { + name = "series" + tabIndex = "10" + + option { + value = "0" + +"---" + } + for (serie in series) { + option { + if (serie == book.series) + selected = true + value = serie.id.toString() + +serie.name + } + } + } + a(classes = cssFormIcon) { + tabIndex = "11" + title = "Serie hinzufügen" + materialSymbolFormButton("add_box") + } + + div(classes = "ml-13 pr-3 ") { + +"Index" + } + input(classes = "$cssTextInput w-16") { + name = "seriesindex" + tabIndex = "12" + value = book.seriesIndex?.toString() ?: "" + } + } + + div(classes = "flex flex-row mb-5 h-9") { +// artists tags +// | | | | +// | | | | + + } + + div(classes = "flex flex-row mb-5 h-9") { +// evtl. switch +// Teil von Sammelband _____v / enthält (-> Link) +// comment + + } + + + button(classes = "$cssButtonPri block mt-8") { + tabIndex = "20" + type = ButtonType.submit + +"Save" + } + } + div { + id = "response" + } + } +} diff --git a/server/src/main/kotlin/books/ui/BooksTemplate.kt b/server/src/main/kotlin/books/ui/BooksTemplate.kt new file mode 100644 index 0000000..5d0479b --- /dev/null +++ b/server/src/main/kotlin/books/ui/BooksTemplate.kt @@ -0,0 +1,97 @@ +package net.keinekekse.app.books.ui + +import kotlinx.html.* +import net.keinekekse.app.VERSION_books +import net.keinekekse.app.core.ui.cssFullscreen +import net.keinekekse.app.core.ui.stdHead + +fun FlowContent.programName() { + + a(classes = "flex") { + href = navBooks.second + title = "v ${VERSION_books.fullVersion}" + + img(classes = "h-14 my-2 $bgHeaderIcon") { + src = "/static/books.png" + } + div(classes = "self-center text-right ml-3") { + div { + div(classes = "text-3xl") { + +VERSION_books.displayName + } + div(classes = "text-xs") { + +VERSION_books.version + } + } + } + } +} + +fun HTML.booksPage(title: String, breadcrumbs: Map, block: DIV.() -> Unit = {}) { +// val booksDb = Dependencies.getBooksDb() + LOG.info(" ### building test page") + +// var breadcrumbMap = emptyMap() +// if (breadcrumbs.isEmpty()) { +// breadcrumbMap = breadcrumbs.plus(Pair("Start", "")) +// } else { +// breadcrumbMap = mapOf(Pair("Start", "/books")).plus(breadcrumbs) +// } + head { + stdHead(title) + } + + body { + // page + div(classes = "$cssFullscreen flex-col") { + // header + div(classes = "flex flex-row justify-between py-2 px-5 $bgHeader $textHeader") { + // header left + div(classes = "flex flex-row") { + programName() + div(classes = "ml-10 flex self-end pr-5 pt-2 border-t-1 items-baseline") { + a(classes = cssHeaderLink) { + href = "/" + materialSymbol("home") +// +"Start" + } + div(classes = "px-3") { +">" } + + breadcrumbs.forEach { (name, url) -> + if (url.isNotEmpty()) { + a(classes = cssHeaderLink) { + href = url + +name + } + div(classes = "px-3") { +">" } + } else { + div { + +name + } + } + } + } + } + // header right TODO nötig? +// div(classes = "flex") { +// div(classes = "self-center text-xl") { +// materialSymbol("Settings") +// } +// } + } + + div(classes = "flex flex-row flex-auto overflow-hidden") { + + block() + + } + + // footer + div(classes = "p-1 text-sm $bgFooter") { + div(classes = "col") { + +"some footer content" + } + } + } + } +} diff --git a/server/src/main/kotlin/books/ui/DbTest.kt b/server/src/main/kotlin/books/ui/DbTest.kt new file mode 100644 index 0000000..369151c --- /dev/null +++ b/server/src/main/kotlin/books/ui/DbTest.kt @@ -0,0 +1,137 @@ +package net.keinekekse.app.books.ui + +import io.ktor.util.logging.* +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.db.schema.* +import net.keinekekse.app.books.model.Languages +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import org.jetbrains.exposed.v1.jdbc.transactions.transaction + +val logger = KtorSimpleLogger("net.keinekekse.app.books.ui") + +suspend fun dbTest(db: Database, booksRepo: BookRepository) { +// val db = Dependencies.getBooksDb() +// val repo = Dependencies.getBookRepository() + + // DB-Test + transaction(db) { + logger.debug("start DB reset - drop") + // create if not exists + SchemaUtils.drop( + PublisherTable, + TagsTable, + TagsToBookTable, + SeriesTable, + ArtistToBookTable, + ArtistRolesTable, + ArtistsTable, + BooksTable, + CalibreBookInfoTable + ) + logger.debug("create tables") + SchemaUtils.create( + PublisherTable, + TagsTable, + TagsToBookTable, + SeriesTable, + ArtistRolesTable, + ArtistsTable, + ArtistToBookTable, + BooksTable, + CalibreBookInfoTable + ) + logger.debug("Tables created") + val auth = ArtistRoleDAO.new { role = "Autor" } + ArtistRoleDAO.new { role = "Übersetzer" } + + val kia1 = ArtistDAO.new { + name = "Aigner" + givenName = "Sebastian" + } + val kia2 = ArtistDAO.new { + name = "Elizarov" + givenName = "Roman" + } + val kia3 = ArtistDAO.new { + name = "Isakova" + givenName = "Svetlana" + } + val kia4 = ArtistDAO.new { + name = "Jemerov" + givenName = "Dmitry" + } + + val kp1 = ArtistDAO.new { + name = "Soshin" + givenName = "Alexey" + } + + val pub1 = PublisherDAO.new { + name = "Manning" + } + + val pub2 = PublisherDAO.new { + name = "Packt Publishing" + } + + val kia = BookDAO.new { + title = "Kotlin in Action, 2nd Edition" + originalTitle = "" + subTitle = "" + year = 2024 + originalYear = 2024 + seriesIndex = 0 + language = Languages.EN + isbn = "9781617296823" + publisher = pub1 + comments = "" + } + val kp = BookDAO.new { + title = "Kotlin Design Patterns and Best Practices, 3rd Edition" + originalTitle = "" + subTitle = "" + year = 2024 + originalYear = 2024 + seriesIndex = 0 + language = Languages.EN + isbn = "9781617298823" + publisher = pub2 + comments = "" + } + + ArtistToBookDAO.new { + book = kia + role = auth + artist = kia1 + } + ArtistToBookDAO.new { + book = kia + role = auth + artist = kia2 + } + ArtistToBookDAO.new { + book = kia + role = auth + artist = kia3 + } + ArtistToBookDAO.new { + book = kia + role = auth + artist = kia4 + } + ArtistToBookDAO.new { + book = kp + role = auth + artist = kp1 + } + } + val books = booksRepo.allBooks() + books.forEach { book -> + logger.debug("### ${book.title}:") + book.artists.forEach { atb -> + logger.debug("### ${atb.role.role}: ${atb.artist.givenName} ${atb.artist.name}") + } + } + +} diff --git a/server/src/main/kotlin/books/ui/Helpers.kt b/server/src/main/kotlin/books/ui/Helpers.kt new file mode 100644 index 0000000..edbc517 --- /dev/null +++ b/server/src/main/kotlin/books/ui/Helpers.kt @@ -0,0 +1,119 @@ +package net.keinekekse.app.books.ui + +import kotlinx.html.FlowContent +import kotlinx.html.div +import kotlinx.html.span +import kotlinx.html.style +import net.keinekekse.app.books.isbnScanResultPath +import kotlin.io.encoding.Base64 + +// * colors * + +const val bgHeader = "bg-orange-500" +const val textHeader = "text-orange-100" +const val bgHeaderIcon = "bg-orange-100" +const val bgFooter = "bg-orange-50" + +const val bgLeftColumn = "bg-slate-200" +const val bgRightColumn = "bg-slate-50" +const val bgThead = "bg-slate-200" + +const val borderTable = "border-slate-400" + +const val bgStdButton = "bg-taupe-500" +const val bgStdButtonHover = "hover:bg-taupe-700" +const val bgStdButtonFocus = "focus:bg-taupe-700" +const val bgPriButton = "bg-orange-400" +const val bgPriButtonHover = "hover:bg-orange-600" +const val bgPriButtonFocus = "focus:bg-orange-600" +const val bgSecButton = "bg-indigo-400" +const val bgSecButtonHover = "hover:bg-indigo-600" +const val bgSecButtonFocus = "focus:bg-indigo-600" +const val textLink = "text-orange-400" +const val textLinkHover = "hover:text-orange-600" +const val textLinkFocus = "focus:text-orange-600" +const val bgLinkHover = "hover:bg-orange-200" +const val bgLinkFocus = "focus:bg-orange-200" + +// * button & link * + +const val cssButton = "cursor-pointer rounded-md px-4 py-2" +const val cssButtonStd = + "text-white $bgStdButton $bgStdButtonHover $cssButton $bgStdButtonFocus" +const val cssButtonPri = + "text-white $bgPriButton $bgPriButtonHover $cssButton $bgPriButtonFocus" +const val cssButtonSec = + "text-white $bgSecButton $bgSecButtonHover $cssButton $bgSecButtonFocus" +const val cssHeaderLink = + "font-bold px-1 $textLinkHover $textLinkFocus $bgLinkHover $bgLinkFocus rounded-md cursor-pointer m-0 no-underline" +const val cssLink = + "font-bold $textLink $textLinkHover $textLinkFocus $bgLinkHover $bgLinkFocus rounded-md cursor-pointer m-0 no-underline" +const val cssLinkText = + "px-1 $cssLink" +const val cssLinkIcon = + "px-2 $cssLink " +const val cssFormIcon = + "font-bold text-slate-400 $textLinkHover $textLinkFocus $bgLinkHover $bgLinkFocus rounded-md " + + "cursor-pointer m-0 px-2 pt-1 no-underline " +const val cssTextInput = + "rounded-sm border-1 border-slate-400 px-2 py-1 focus:border-orange-400 focus:border-4" + + " invalid:focus:bg-red-200 invalid:focus:border-red-500 invalid:focus:border-5" + + " invalid:bg-red-200 invalid:border-red-500 invalid:border-3 " +const val _cssTextInput = + "rounded-sm border-b-1 px-2 py-1 focus:border-b-orange-400 focus:border-b-4" + + " invalid:focus:bg-red-200 invalid:focus:border-red-500 invalid:focus:border-5" + + " invalid:bg-red-200 invalid:border-red-500 invalid:border-3 " +// * fixed table header * + +const val cssFixedTHead = "fixed-table-header $bgThead " + +// m-1 -> margin (--spacing * 1) -> --spacing = .25em = 4px => m-1 = 4px, m-2 = 8px + +// Navigation + +val navBooks = "Bücher" to "/books" +val navBooksEdit = "Buch bearbeiten" to "/books/edit" +val navBooksNew = "Neue Bücher" to "/books/new" +val navBooksScan = "ISBN Scan" to "/books/new/scan" +val navBooksScanFetch = "" to isbnScanResultPath // has to be shared with :web-Project +val navBooksScanEdit = "Scan bearbeiten" to "/books/new/editscan" + +fun FlowContent.materialSymbol(symbol: String, styles: String = "") { + // Den Icon via Tailwind formatieren klappt nicht. Daher muss z. B. font-size oder + // font-weight per style= gesetzt werden. + span(classes = "material-symbols-outlined align-text-bottom") { + style = styles + +symbol + } +} + +fun FlowContent.materialSymbolButton(symbol: String, styles: String = "") { + div(classes = "$cssLinkIcon inline-block") { + materialSymbol(symbol, styles) + } +} + +fun FlowContent.materialSymbolFormButton(symbol: String, styles: String = "") { + div(classes = " inline-block") { + materialSymbol(symbol, styles) + } + +} + +/** + * Converts a byte array representing an image into a Base64 encoded Data URL string. + * + * Use in <img src="..."> + * + * @param image A nullable byte array containing the binary data of the image. + * When null or empty, an empty string is returned. + * @return A Data URL string containing the Base64 encoded image data, + * or an empty string if the input is null or empty. + */ +fun embedImageURL(image: ByteArray?): String = + if (image != null && image.isNotEmpty()) { + val base64String = + Base64.encode(image) // kotlin.io.encoding.Base64 + val mimeType = "image/jpeg" + "data:$mimeType;base64,$base64String" + } else "" diff --git a/server/src/main/kotlin/books/ui/ImportEditPage.kt b/server/src/main/kotlin/books/ui/ImportEditPage.kt new file mode 100644 index 0000000..fe3c666 --- /dev/null +++ b/server/src/main/kotlin/books/ui/ImportEditPage.kt @@ -0,0 +1,72 @@ +package net.keinekekse.app.books.ui + +import kotlinx.html.HTML +import kotlinx.html.div +import kotlinx.html.img +import kotlinx.html.span +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.model.Book +import net.keinekekse.app.books.model.CalibreBookInfo +import net.keinekekse.app.books.model.Languages +import net.keinekekse.app.books.model.Publisher + +fun HTML.importEditPage(bookInfo: CalibreBookInfo?, booksRepo: BookRepository) { + val dataUrl = embedImageURL(bookInfo?.cover) + booksPage( + navBooksScanEdit.first, + mapOf(navBooks, navBooksNew, Pair(navBooksScanEdit.first, "")) + ) { + div(classes = "flex flex-col flex-auto") { + div(classes = "flex flex-row h-60 m-8 w-3/4") { + img(classes = "inline-block h-60 pr-6 ") { + src = dataUrl + } + div { + div { + span(classes = "inline-block w-25 ") { +"ISBN: " } + +(bookInfo?.isbn ?: "") + } + div { + span(classes = "inline-block w-25") { +"Titel: " } + +(bookInfo?.title ?: "") + } + div { + span(classes = "inline-block w-25") { +"Autor(en): " } + +(bookInfo?.authors ?: "") + } + div { + span(classes = "inline-block w-25") { +"Verlag: " } + +(bookInfo?.publisher ?: "") + } + div { + span(classes = "inline-block w-25") { +"Jahr: " } + +bookInfo?.published.toString() + } + div { + span(classes = "inline-block w-25") { +"Tags: " } + +(bookInfo?.tags ?: "") + } + div { + span(classes = "inline-block w-25") { +"Identifiers: " } + +(bookInfo?.identifiers ?: "") + } + div { + span(classes = "inline-block w-25") { +"Comment: " } + +(bookInfo?.comments ?: "") + } + + } + } + + bookEdit( + Book( + title = bookInfo?.title ?: "", + year = bookInfo?.published ?: 0, + isbn = bookInfo?.isbn ?: "", + publisher = Publisher(0, ""), + language = bookInfo?.language ?: Languages.DE + ), booksRepo + ) + } + } +} diff --git a/server/src/main/kotlin/books/ui/ImportPage.kt b/server/src/main/kotlin/books/ui/ImportPage.kt new file mode 100644 index 0000000..e31c0d4 --- /dev/null +++ b/server/src/main/kotlin/books/ui/ImportPage.kt @@ -0,0 +1,110 @@ +package net.keinekekse.app.books.ui + +import kotlinx.html.* +import net.keinekekse.app.books.model.CalibreBookInfo + +fun HTML.importPage(books: List) { +// head { +// stdHead("ISBN Scannen", false) +// } +// body { + booksPage(navBooksNew.first, mapOf(navBooks, Pair(navBooksNew.first, ""))) { + div(classes = "basis-64 px-1 py-2 text-center $bgLeftColumn") { + div { + +"some content on the left " + } + div(classes = "my-6") { + a(classes = cssButtonPri) { + href = navBooksScan.second + +"ISBN Scan" + } + } + + } + + // content right + div(classes = "flex flex-auto $bgRightColumn") { + div(classes = "flex flex-col flex-auto") { + div { +// +"some heading" + } + importTable(books) + } + } + } +} + +fun FlowContent.importTable(books: List) { + + div(classes = "flex-auto overflow-auto px-1 pb-1") { + table { + id = "importtable" + thead(classes = "$cssFixedTHead ") { + tr { + th(classes = "py-3 px-2") { +"ISBN" } + th(classes = "py-3 px-2") { +"Titel" } + th(classes = "py-3 px-2") { +"Autoren" } + th(classes = "py-3 px-2") { +"Verlag" } + th(classes = "py-3 px-2") { +"Jahr" } + th(classes = "py-3 px-2") { +"Sprache" } + th(classes = "py-3 px-2") { +" " } + } + } + tbody { + for ((id, title, authors, publisher, tags, language, published, identifiers, isbn, comment, cover) in books) { + tr { + td(classes = "border-t-1 $borderTable px-2 py-1") { + +isbn + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +title + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +authors + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +publisher + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +published.toString() + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +language.name + } + td(classes = "border-t-1 $borderTable px-2 py-1 text-nowrap") { + a { + href = "${navBooksScanEdit.second}/$id" + materialSymbolButton("Edit") + } + materialSymbolButton("Delete") + } + } + tr { + td {} + td { + colSpan = "5" + details(classes = "mb-2") { + attributes["name"] = "comments" + summary(classes = "text-sm opacity-50") { + +"Tags, Comments" + } + div(classes = "flex flex-row") { + img(classes = "inline-block h-30 pr-6 ") { + src = embedImageURL(cover) + } + div { + +tags + br {} + +identifiers + br {} + +comment + } + } + } + } + } + } + } + } + } +} diff --git a/server/src/main/kotlin/books/ui/ScanPage.kt b/server/src/main/kotlin/books/ui/ScanPage.kt new file mode 100644 index 0000000..c327255 --- /dev/null +++ b/server/src/main/kotlin/books/ui/ScanPage.kt @@ -0,0 +1,94 @@ +package net.keinekekse.app.books.ui + +import kotlinx.html.* + +fun HTML.scanPage() { +// head { +// stdHead("ISBN Scannen", false) +// } +// body { + booksPage( + navBooksScan.first, + mapOf(navBooks, navBooksNew, Pair(navBooksScan.first, "")) + ) { + style { + type = "text/css" + +""" + button { + background-color: var(--color-orange-400); + color: white; + padding: 8px 16px; + border: none; + border-radius: 5px; + cursor: pointer; + } + button:hover { + background-color: var(--color-orange-600); + } + h3 { + font-weight: bold; + margin: 10px; + } + """.trimIndent() + } + div(classes = "m-4 overflow-auto flex-auto") { + h3 { +"ISBN / Barcode erfassen" } + + // 1. Kamera-Ausgabebereich + div { + id = "camera" + style = "width: 400px;" + } + + // 2. Zielbereich für das Buch-Ergebnis (wird per HTMX befüllt) + div { + id = "scan-result" +// p { +"Bitte Barcode vor die Kamera halten..." } + } + } + // 3. JavaScript zur Initialisierung & HTMX-Bridge + script { + unsafe { + raw( + """ + document.addEventListener('DOMContentLoaded', function () { + renderScanner("camera"); + }); + """.trimIndent() + + ) + } + } + } + +} + +fun TagConsumer.isbnResultCallback(bookInfo: String, dataUrl: String) { + + div { + pre { + // gefundene Daten anzeigen -> Bearbeitung später + +bookInfo + } + if (dataUrl.isNotEmpty()) { + div { +// img(src = "/books/testimage/$isbn") + img(src = dataUrl) { + style = "height: 200px; margin-bottom: 10px;" + } + } + } + button { + style = "margin-left: 10px; font-size: 16px;" + onClick = "renderScanner('camera')" + +"Nächtes Buch" + } + button { + style = "margin-left: 10px; font-size: 16px;" + type = ButtonType.button + onClick = "location.href='${navBooksNew.second}'" + +"Fertig" + } + + } +} diff --git a/server/src/main/kotlin/books/ui/TestPage.kt b/server/src/main/kotlin/books/ui/TestPage.kt new file mode 100644 index 0000000..1ba7582 --- /dev/null +++ b/server/src/main/kotlin/books/ui/TestPage.kt @@ -0,0 +1,279 @@ +@file:OptIn(ExperimentalKtorApi::class) + +package net.keinekekse.app.books.ui + +import io.ktor.htmx.* +import io.ktor.htmx.html.* +import io.ktor.util.logging.* +import io.ktor.utils.io.* +import kotlinx.html.* +import net.keinekekse.app.core.config.LOGOUT_PATH +import org.jetbrains.exposed.v1.jdbc.Database +import kotlin.random.Random + +val LOG = KtorSimpleLogger("net.keinekekse.app.books.ui.TestPage") + +//fun FlowContent.programName() { +// +// a(classes = "flex") { +// href = "/" +// title = "v ${VERSION_books.fullVersion}" +// +// img(classes = "h-14 my-2 $bgHeaderIcon") { +// src = "/static/books.png" +// } +// div(classes = "self-center text-right ml-3") { +// div { +// div(classes = "text-3xl") { +// +VERSION_books.displayName +// } +// div(classes = "text-xs") { +// +VERSION_books.version +// } +// } +// } +// } +//} +// +//fun HTML.testPage(booksDb: Database) { +//// val booksDb = Dependencies.getBooksDb() +// LOG.info(" ### building test page") +// +// head { +// stdHead("Test Page") +// } +// +// body { +// // page +// div(classes = "$cssFullscreen flex-col") { +// // header +// div(classes = "flex flex-row justify-between py-2 px-5 $bgHeader $textHeader") { +// // header left +// div(classes = "flex flex-row") { +// programName() +// div(classes = "ml-10 flex self-center") { +// +"MENÜ" +// } +// } +// // header right +// div(classes = "flex") { +// div(classes = "self-center text-xl") { +// materialSymbol("Settings") +// } +// } +// } +fun HTML.testPage(booksDb: Database) { + booksPage("Books-DB", mapOf(Pair(navBooks.first, ""))) { + // content left + div(classes = "basis-64 px-1 py-2 text-center $bgLeftColumn") { + div { + +"some content on the left " + } + div { + button(classes = "$cssButtonStd m-2") { + attributes.hx { + // TODO + get = "/books/testdb" + target = "#testdb" + swap = HxSwap.innerHtml + trigger = "click" + } + +"DB test" + } + } + div { + button(classes = "$cssButtonPri m-2 focus:outline-none focus:outline-orange-800") { + +"primary button" + } + } + div { + +"text text " + button(classes = "$cssLinkText ") { +"click me" } + +" test test" + } + div { + button(classes = "$cssButtonSec m-2") { +"secondary button" } + } + div { + +"Database: ${booksDb.vendor} ${booksDb.version}" + } + div { + form(action = LOGOUT_PATH, method = FormMethod.post) { + button(classes = cssLinkText) { + type = ButtonType.submit + +"Logout" + } + } + } + div { + id = "testdb" + } + div(classes = "mt-6") { + +// script { +// unsafe { +// raw( +// """ +// function toggleField(hideObj,showObj){ +// hideObj.disabled=true; +// hideObj.style.display='none'; +// showObj.disabled=false; +// showObj.style.display='inline'; +// showObj.focus(); +// } +// """.trimIndent() +// ) +// } +// +// } + + // fkt. nur so lala (onBlur!), lässt sich das hinbiegen? +// form { +// attributes.hx { +// post = "/books/testform" +// target = "#testForm" +// swap = HxSwap.innerHtml +// } +// name = "BrowserSurvey" +// select { +// name = "browser" +// onChange = """ +// if(this.options[this.selectedIndex].value=='customOption'){ +// toggleField(this,this.nextSibling); +// this.selectedIndex='0'; +// } +// """.trimIndent() +// label { +"Browser: " } +// option { +// value = "customOption" +// +"[type a custom value]" +// } +// option { +// value = "vChrome" +// +"Chrome" +// } +// option { +// value = "vFirefox" +// +"Firefox" +// } +// option { +// value = "vInternetExplorer" +// +"Internet Explorer" +// } +// option { +// value = "vOpera" +// +"Opera" +// } +// } +// input { +// type = InputType.text +// name = "browser" +// style = "display:none;" +// disabled = true +// onBlur = """ +// if(this.value==null || this.value==''){toggleField(this,this.previousSibling);} +// """.trimIndent() +// } +// input(type = InputType.submit) { +// value = "Submit" +// } +// } + + // klappt, es fehlt halt die "value" -> alpha search ?! + form { + attributes.hx { + post = "/books/testform" + target = "#testForm" + swap = HxSwap.innerHtml + } + input( + classes = cssTextInput, + type = InputType.text + ) { + name = "testInput" + list = "lst" + minLength = "2" + + dataList { + id = "lst" + option(content = "alpha") + option(content = "beta") + option(content = "gamma") + } + } + input(type = InputType.submit) { + value = "Submit" + } + } + div { + id = "testForm" + } + } + } + // content right + div(classes = "flex flex-auto $bgRightColumn") { + div(classes = "flex flex-col flex-auto") { +// div { +// +"some heading" +// } + testTable() + } + } + } +} +// // footer +// div(classes = "p-1 text-sm $bgFooter") { +// div(classes = "col") { +// +"some footer content" +// } +// } +// } +//// } +// +// } +//} + +fun FlowContent.testTable() { + div(classes = "flex-auto overflow-auto px-1 pb-1") { + table { + id = "testtable" + thead(classes = "$cssFixedTHead ") { + tr { + th(classes = "py-3 px-2") { +"#" } + th(classes = "py-3 px-2") { +"Alias" } + th(classes = "py-3 px-2") { +"Score" } + th(classes = "py-3 px-2") { + a { + href = navBooksNew.second + title = navBooksNew.first + materialSymbolButton( + "auto_stories", + "font-weight: bold; font-size: 2em" + ) + // library_add + } + } + } + } + tbody { + for (i in 0..100) { + tr { + td(classes = "border-t-1 $borderTable px-2 py-1") { + +i.toString() + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +Random.nextLong().toString() + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +Random.nextLong().toString() + } + td(classes = "border-t-1 $borderTable px-2 py-1 text-nowrap") { + materialSymbolButton("Edit") + materialSymbolButton("Delete") + } + } + } + } + } + } +} diff --git a/server/src/main/kotlin/core/auth/AppPrincipal.kt b/server/src/main/kotlin/core/auth/AppPrincipal.kt new file mode 100644 index 0000000..d7d2e04 --- /dev/null +++ b/server/src/main/kotlin/core/auth/AppPrincipal.kt @@ -0,0 +1,52 @@ +package net.keinekekse.app.core.auth + +import io.ktor.server.auth.oidc.* +import io.ktor.utils.io.* +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.security.Principal +import kotlin.time.Instant + +@OptIn(ExperimentalKtorApi::class) +class AppPrincipal(credential: OidcToken.Id) : Principal { + + val userId: String = credential.userInfo.preferredUsername ?: "" + val vorname: String = credential.userInfo.givenName ?: "" + val nachname: String = credential.userInfo.familyName ?: "" + val email: String = credential.userInfo.email ?: "" + val roles: Set = extractRoles(credential) + val expiresAt: Instant? = credential.claims.expiresAt + + private fun extractRoles(credential: OidcToken.Id): Set { + val payload = credential.claims.payload + + // 1. Realm-Rollen extrahieren + val realmRoles = payload["realm_access"] + ?.jsonObject?.get("roles") + ?.jsonArray?.mapNotNull { it.jsonPrimitive.content } + ?: emptyList() + + // 2. Client-Rollen extrahieren + val clientRoles = payload["resource_access"] + ?.jsonObject?.get("pkce-book-app") // TODO parametrisieren + ?.jsonObject?.get("roles") + ?.jsonArray?.mapNotNull { it.jsonPrimitive.content } + ?: emptyList() + + println("AppPrincipal: User ${userId}, Roles extracted: ${realmRoles.size}") + // 3. Zusammenführen und auf UserRoles-Enum mappen + return (realmRoles + clientRoles).mapNotNull { rawRole -> + val formatted = rawRole.uppercase().replace("-", "_") + try { + UserRoles.valueOf(formatted) + } catch (_: IllegalArgumentException) { + null // Rollen ignorieren, die nicht im Enum definiert sind + } + }.toSet() + } + + override fun getName(): String { + return "$userId ($vorname $nachname) <$email>" + } +} diff --git a/server/src/main/kotlin/core/auth/UserRoles.kt b/server/src/main/kotlin/core/auth/UserRoles.kt new file mode 100644 index 0000000..8b4167d --- /dev/null +++ b/server/src/main/kotlin/core/auth/UserRoles.kt @@ -0,0 +1,28 @@ +package net.keinekekse.app.core.auth + +import io.ktor.server.auth.* +import io.ktor.utils.io.* + +/** + * Die enum spiegelt die verfügbaren Rollen aus Keycloak wider. + * + * Für den Vergleich wird die Keycloak-Rolle in Großbuchstaben umgewandelt + * und "-" durch "_" ersetzt. + * + * Alle Änderungen auf der Keycloak-Seite müssen hier nachgefahren werden. + * + * Wegen der Art des Vergleichs (für eine Route wird immer nur eine Rolle geprüft) + * ist zu beachten, dass der User jeweils alle benötigten Rollen bekommt. Implizit + * ist nur Admin, der für alle Routen zugelassen ist. + * + */ +@OptIn(ExperimentalKtorApi::class) +enum class UserRoles : AuthenticationRole { + ADMIN, + + BOOKS_ADMIN, + BOOKS_USER, + BOOKS_EDITOR, + BOOKS_RO + +} diff --git a/server/src/main/kotlin/core/config/ApplicationHelpers.kt b/server/src/main/kotlin/core/config/ApplicationHelpers.kt new file mode 100644 index 0000000..903d2e7 --- /dev/null +++ b/server/src/main/kotlin/core/config/ApplicationHelpers.kt @@ -0,0 +1,78 @@ +package net.keinekekse.app.core.config + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import kotlinx.serialization.json.Json + +const val LOGIN_PATH = "/oidc/keycloak/login" +const val LOGOUT_PATH = "/oidc/keycloak/logout" +const val REFRESH_PATH = "/oidc/keycloak/refresh" + +const val SESSION_NAME = "KEYCLOAK_SESSION" + +const val REDIRECT_COOKIE = "loginredirect" + +val httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + isLenient = true + explicitNulls = false // wg. fehlender Felder + } + ) + } + install(HttpTimeout) { + requestTimeoutMillis = 10000 + connectTimeoutMillis = 5000 + } +} + +/** + * Handles unauthorized access attempts by logging the request URI and redirecting the user + * to the login page. + * + * @param call The `ApplicationCall` instance that contains details about the current HTTP call, + * including the request and response. + */ +suspend fun onUnauthorized(call: ApplicationCall) { + call.application.log.info("UNAUTHORIZED: uri = ${call.request.local.uri}") + setRedirectCookie(call) + call.respondRedirect(LOGIN_PATH) +} + +fun setRedirectCookie(call: ApplicationCall) { + call.response.cookies.append( + name = REDIRECT_COOKIE, + value = call.request.local.uri, + maxAge = 240, + path = "/", + httpOnly = true + ) +} + +fun getRedirectUri(call: ApplicationCall): String { + val uri = call.request.cookies[REDIRECT_COOKIE] + if (uri != null) { + call.response.cookies.append( + name = REDIRECT_COOKIE, + value = "", + maxAge = 0, + path = "/", + httpOnly = true + ) + } + return uri ?: "/" +} + +fun Application.getKeyFromConfiguration(key: String, default: String? = null): String { + val value = environment.config.propertyOrNull(key)?.getString() ?: default + // ohne die Konfig-Params geht nix, hier ist nur noch Abbruch möglich + return value + ?: throw IllegalStateException("Configuration key '$key' not found, server start aborted.") +} diff --git a/server/src/main/kotlin/core/config/DI.kt b/server/src/main/kotlin/core/config/DI.kt new file mode 100644 index 0000000..26157c3 --- /dev/null +++ b/server/src/main/kotlin/core/config/DI.kt @@ -0,0 +1,13 @@ +package net.keinekekse.app.core.config + +import io.ktor.server.application.* +import io.ktor.server.plugins.di.* + +fun Application.configureDependencyInjection() { + log.info("Configuring dependency injection") + val kasDb = configureCoreDatabase() + + dependencies { + provide("kasDb") { kasDb } + } +} diff --git a/server/src/main/kotlin/core/config/Database.kt b/server/src/main/kotlin/core/config/Database.kt new file mode 100644 index 0000000..85f4093 --- /dev/null +++ b/server/src/main/kotlin/core/config/Database.kt @@ -0,0 +1,34 @@ +package net.keinekekse.app.core.config + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.ktor.server.application.* +import org.jetbrains.exposed.v1.core.DatabaseConfig +import org.jetbrains.exposed.v1.jdbc.Database + +fun Application.configureCoreDatabase(): Database { + log.info("Configuring database") + + val config = HikariConfig().apply { + dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" + username = getKeyFromConfiguration("postgres.kas_schema") + password = getKeyFromConfiguration("postgres.kas_pwd") + + addDataSourceProperty("serverName", getKeyFromConfiguration("postgres.host", "localhost")) + addDataSourceProperty("portNumber", getKeyFromConfiguration("postgres.port", "5432")) + addDataSourceProperty("databaseName", username) + + maximumPoolSize = 6 + isReadOnly = false + transactionIsolation = "TRANSACTION_SERIALIZABLE" + } + + val dataSource = HikariDataSource(config) + + return Database.connect( + datasource = dataSource, + databaseConfig = DatabaseConfig { + // set other parameters here + } + ) +} diff --git a/server/src/main/kotlin/core/config/HtmlDsl.kt b/server/src/main/kotlin/core/config/HtmlDsl.kt new file mode 100644 index 0000000..bdc5d5e --- /dev/null +++ b/server/src/main/kotlin/core/config/HtmlDsl.kt @@ -0,0 +1,7 @@ +package net.keinekekse.app.core.config + +import io.ktor.server.application.* + +fun Application.configureHtmlDsl() { + log.info("Configuring html dsl") +} diff --git a/server/src/main/kotlin/core/config/Http.kt b/server/src/main/kotlin/core/config/Http.kt new file mode 100644 index 0000000..eea79ba --- /dev/null +++ b/server/src/main/kotlin/core/config/Http.kt @@ -0,0 +1,44 @@ +package net.keinekekse.app.core.config + +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.application.* +import io.ktor.server.plugins.cachingheaders.* +import io.ktor.server.plugins.compression.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.defaultheaders.* +import io.ktor.server.plugins.forwardedheaders.* +import net.keinekekse.app.VERSION + +fun Application.configureHttp() { + log.info("Configuring HTML") + + log.info(" *** ${VERSION.description} ${VERSION.fullVersion} - ${VERSION.copyright} ***") + + install(CachingHeaders) { + options { call, outgoingContent -> + when (outgoingContent.contentType?.withoutParameters()) { + ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60)) + else -> null + } + } + } + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + allowHeader("MyCustomHeader") + allowHost("kas.keinekekse.net") + allowHost("kec.keinekekse.net") +// anyHost() // Don't do this in production if possible. Try to limit it. + } + install(Compression) + install(DefaultHeaders) { + header("X-Engine", "Ktor") // will send this header with each response + } + install(ForwardedHeaders) // WARNING: for security, do not include this if not behind a reverse proxy + install(XForwardedHeaders) + +} diff --git a/server/src/main/kotlin/core/config/Routing.kt b/server/src/main/kotlin/core/config/Routing.kt new file mode 100644 index 0000000..2f8c6bf --- /dev/null +++ b/server/src/main/kotlin/core/config/Routing.kt @@ -0,0 +1,43 @@ +package net.keinekekse.app.core.config + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.html.* +import io.ktor.server.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.utils.io.* +import net.keinekekse.app.core.ui.adminPage +import net.keinekekse.app.core.ui.landingPage + +@OptIn(ExperimentalKtorApi::class) +fun Application.configureRouting(keycloakSession: KeycloakSession) { + log.info("Configuring core routing") + routing { + + staticResources("/", "/web") + staticResources("/static", "static") + + authenticateWithOptional(keycloakSession) { + get("/") { + call.respondHtml { + // ist ein User angemeldet? + val loggedOn = call.principalOrNull != null + landingPage(loggedOn) + } + } + + get("/admin") { + call.respondHtml { + + adminPage() + } + } + } + + get("/json/kotlinx-serialization") { + call.respond(mapOf("hello" to "world")) + } + } + +} diff --git a/server/src/main/kotlin/core/config/Security.kt b/server/src/main/kotlin/core/config/Security.kt new file mode 100644 index 0000000..ed06c48 --- /dev/null +++ b/server/src/main/kotlin/core/config/Security.kt @@ -0,0 +1,196 @@ +package net.keinekekse.app.core.config + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.oidc.* +import io.ktor.server.plugins.di.* +import io.ktor.server.response.* +import io.ktor.server.sessions.* +import io.ktor.utils.io.* +import net.keinekekse.app.core.auth.AppPrincipal +import net.keinekekse.app.core.auth.UserRoles +import java.io.File +import java.security.SecureRandom +import java.util.* +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalKtorApi::class) +typealias KeycloakSession = AuthenticationSchemeWithRoles, AuthenticationScheme>> + +@OptIn(ExperimentalKtorApi::class) +suspend fun Application.configureSecurity() { + + log.info("configure Security") + // install(CSRF) { +// // tests Origin is an expected value +// allowOrigin("http://localhost:8080") +// +// // tests Origin matches Host header +// originMatchesHost() +// +// // custom header checks +// checkHeader("X-CSRF-Token") +// } + fun generateRandomString(): String { + val bytes = ByteArray(64) + SecureRandom().nextBytes(bytes) + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) + } + + val keycloakUrl = getKeyFromConfiguration("keycloak.url") + val keycloakRealm = getKeyFromConfiguration("keycloak.realm") + val keycloakClientId = getKeyFromConfiguration("keycloak.clientId") + + val keycloakIssuer = "$keycloakUrl/realms/$keycloakRealm" + val keycloakBaseUrl = "$keycloakIssuer/protocol/openid-connect" + + // werden automatisch gesetzt +// val authorizationEndpoint = "$keycloakBaseUrl/auth" +// val tokenEndpoint = "$keycloakBaseUrl/token" +// val certificatesEndpoint = "$keycloakBaseUrl/certs" + + // TODO besser irgendwo speichern, damit es Server-Restarts überlebt +// val secret = "vncosiifikvjbaiorfhosdjbkjcfehjdc" // + val secret = generateRandomString() + // TODO Zufallswert extern speichern + val b: ByteArray = byteArrayOf() + val state = byteArrayOf( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2 + ) + + val oidc = install(Oidc) { // OAuth2 OpenId w/PKCE + initialDiscoveryAttempts = 5 + initialDiscoveryRetryDelay = 15.seconds + } + + val keycloakIdentity = oidc.identityProvider("keycloak") { + issuer = keycloakIssuer + + oauth { + clientId = keycloakClientId + clientSecret = secret + stateEncryptionKey = OidcStateEncryptionKey.of(state) + + onAuthenticated { idToken -> + call.application.log.info("keycloak: onAuthenticated ${idToken.value}") + // == accessToken + println("keycloak claims ${idToken.claims}") + println(" expiresAt ${idToken.claims.expiresAt}") + println(" issuer ${idToken.claims.issuer}") + // issuer https://kec.keinekekse.net/realms/app-server-realm + println(" audience ${idToken.claims.audience}") + // audience [pkce-book-app] + println(" header ${idToken.claims.header}") + // header {"alg":"RS256","typ":"JWT","kid":"WEQgQnKpr4yjwbehpXIsz0HlW3u5pIA8ie3wv1qi-2I"} + println(" jwtId ${idToken.claims.jwtId}") + // jwtId 0bc5a09a-e1b1-d249-5990-aaf2970dd04f + println(" subject ${idToken.claims.subject}") + // subject b5b40fa3-468f-443c-8eb0-e220bdd353bc + println(" type ${idToken.claims.type}") + // type JWT + println(" payload ${idToken.claims.payload}") + // JWT as JSON (preferred_username -> UserID) + println("keycloak userInfo ${idToken.userInfo}") + println(" subject ${idToken.userInfo.subject}") + // subject b5b40fa3-468f-443c-8eb0-e220bdd353bc + println(" name ${idToken.userInfo.name}") + println(" familyName ${idToken.userInfo.familyName}") + println(" preferredUsername ${idToken.userInfo.preferredUsername}") + // preferredUsername hb42 + + // URI von der aus der Login ausgelöst wurde (default: "/") + // wird ggf. im authenticateWith-Block via onUnauthenticated(call) gesetzt + val redirect = getRedirectUri(call) + call.application.log.info("loginredirect cookie: $redirect") + // zurück zur Seite, die den Login angefordert hat (oder "/") + call.respondRedirect(redirect) + } + + onAuthenticationFailed { cause -> + val reason = (cause as? AuthenticationFailedCause.Error)?.message + call.application.log.error("keycloak: Login failed: $reason") + call.respondRedirect(LOGIN_PATH) + } + + logout { + call.application.log.info("keycloak: Logout") + call.respondRedirect("/") + } + + sessions { + name = SESSION_NAME + // hier werden die keycloak tokens gespeichert + storage = directorySessionStorage( + File("/opt/app/.sessions") // liegt auf dem Volume + ) + // hier ist nur cookie möglich (speichert sessionId) + cookie { + cookie.path = "/" + cookie.maxAgeInSeconds = 600 // length of session + } + // Macht Ärger, wenn die Session serverseitig abgelaufen ist und der + // Client über den Ablauf hinaus idle war. Der nächste Seitenaufruf + // triggert dann einen Refresh, dessen Antwort "400 - invalid_grant" Ktor + // leider mit einer Exception quittiert. + // (s. a. Exception-Handler in StatusPages.kt). + // Ohne läuft die Session einfach ohne Refresh ab, was nicht Sinn + // der Sache ist. Also nachsehen, ob abgelaufen und dann Login triggern. + val custom = OidcTokenRefreshStrategy.Custom { provider, token, now -> + val expiresAt = token.claims.expiresAt + call.application.log.info("#### Refreshing token $expiresAt - $now") + if (expiresAt != null && expiresAt <= now) { + // Token ist abgelaufen + // -> aktuelle Route merken, zum Login und zurück zur Route + call.application.log.info("#### redirect to login") + onUnauthorized(call) + call.application.log.info("#### exit refresh (!?)") + return@Custom null + } + val stale = expiresAt != null && expiresAt <= now + 1.minutes + val refreshToken = token.refreshToken + if (stale && refreshToken != null) { + call.application.log.info("#### Refreshing token with refresh token") + provider.refreshToken(refreshToken).idToken + } else { + token + } + } + tokenRefreshStrategy = custom + } + } + } + +// log.info("keycloak issuer: ${keycloakIdentity.issuer}") +// log.info("keycloak name: ${keycloakIdentity.name}") +// log.info("keycloak session name: ${keycloakIdentity.session.name}") + + /* Keycloak-Session + * + * Die Rollen des jeweiligen Users werden zwar von Keycloak geliefert + * (Keycloak: Client Scopes->"Roles"->Mappers->"realm roles"/ "client roles"->"Add to ID token") + * allerdings werden sie nicht automatisch in die Session übernommen. Deshalb müssen + * zunächst die Rollen aus dem ID-Token extrahiert werden. Das wird in AppPrincipal erledigt. + * Das Objekt übernimmt zudem alle sonstigen für die Anwendung relevanten Userdaten. + * Mit .mapPrincipal() wird es dann als weiterer Principal in der Session übernommen und mit + * .withRoles() werden die extrahierten Rollen in die Session übernommen. + * + * Fürs Handling ist dabei wichtig, wie auf die AppPrincipal-Instanz im Routing-Kontext + * zugegriffen werden kann: + * val appPrincipal = call.principal // ohne () aufrufen, wird bei .mapPrincipal() als + * // Attribut angelegt! + * val oicdPrincipal = call.principal() // der automatisch angelegte principal + */ + val keycloakSession = keycloakIdentity.session + .mapPrincipal { idToken -> AppPrincipal(idToken) } + .withRoles { principal -> principal.roles } + + dependencies { + provide { keycloakIdentity } + provide { keycloakSession } + } + +} diff --git a/server/src/main/kotlin/core/config/Serialization.kt b/server/src/main/kotlin/core/config/Serialization.kt new file mode 100644 index 0000000..e7a1ef5 --- /dev/null +++ b/server/src/main/kotlin/core/config/Serialization.kt @@ -0,0 +1,18 @@ +package net.keinekekse.app.core.config + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* + +fun Application.configureSerialization() { + log.info("Configuring serialization") + install(ContentNegotiation) { + json( + json = kotlinx.serialization.json.Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + } + ) + } +} diff --git a/server/src/main/kotlin/core/config/StatusPages.kt b/server/src/main/kotlin/core/config/StatusPages.kt new file mode 100644 index 0000000..d29c08b --- /dev/null +++ b/server/src/main/kotlin/core/config/StatusPages.kt @@ -0,0 +1,96 @@ +package net.keinekekse.app.core.config + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.html.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import kotlinx.html.a +import kotlinx.html.div +import kotlinx.html.h1 +import kotlinx.html.span +import net.keinekekse.app.core.ui.* + +fun Application.configureStatusPages() { + log.info("Configuring status pages") + + install(StatusPages) { + status(HttpStatusCode.Forbidden) { + // TODO Hinweis an den User? + call.application.log.warn("403 Forbidden - redirect to /") + call.respondRedirect("/") + } + status(HttpStatusCode.Unauthorized) { + call.application.log.warn("401 Unauthorized - redirect to /") + + // für Redirect nach Login (Cookie läuft nach 2 Minuten ab!) + setRedirectCookie(call) + + call.respondHtml { + mainApplicationPage { + div(classes = "$cssFlexColumnCenter w-full pt-10") { + h1(classes = "m-8 px-8 text-xl font-bold $textMainBody") { + +"Fehlende Berechtigung für die Seite " + span(classes = cssCode) { + +call.request.path() + } + } + div(classes = "m-8 inline-flex flex-row") { + div(classes = "px-8") { + a(classes = cssMainLink) { + href = "/" + +"Zurück zur Startseite" + } + } + div(classes = "ml-4 px-8") { + a(classes = cssMainLink) { + href = LOGIN_PATH + +"Anmelden" + } + } + } + } + } + } + } + exception { call, cause -> + call.application.log.error("+++ Exception ", cause) + + // Session-Cookie verhindert u. U. die Weiterleitung zur Startseite + // also hier löschen (ein Cookie mit httponly=true kann im Browser + // nicht gelöscht werden). + call.response.cookies.append( + name = SESSION_NAME, + value = "", + maxAge = 0, + path = "/", + domain = null, + secure = true, + httpOnly = true + ) + call.respondHtml { + + mainApplicationPage { + div(classes = "inline-flex flex-col $cssMainWidth") { + h1(classes = "m-8 text-xl font-bold $textMainBody") { + +"Ein Fehler ist aufgetreten" + } + div(classes = "p-2 font-bold $textMainBody") { + +(cause.message ?: "") + } + div(classes = "flex-auto overflow-auto mt-2 p-2 font-mono $textMainBody") { + +cause.stackTraceToString() + } + div(classes = "m-8") { + a(classes = cssMainLink) { + href = "/" + +"Zurück zur Startseite" + } + } + } + } + } + } + } +} diff --git a/server/src/main/kotlin/core/ui/AdminPage.kt b/server/src/main/kotlin/core/ui/AdminPage.kt new file mode 100644 index 0000000..2854ad8 --- /dev/null +++ b/server/src/main/kotlin/core/ui/AdminPage.kt @@ -0,0 +1,25 @@ +package net.keinekekse.app.core.ui + +import kotlinx.html.* +import net.keinekekse.app.books.ui.materialSymbol + +fun HTML.adminPage() { + val title: String = "Configuration" + + head { + stdHead(title) + } + + body { + div(classes = "m-4") { + a(classes = "px-2 font-bold text-sky-500 hover:text-sky-700 hover:bg-sky-100 cursor-pointer") { + href = "/" + materialSymbol("Home") + +"Home" + } + } + div(classes = "m-4") { + +"Admin Page" + } + } +} diff --git a/server/src/main/kotlin/core/ui/LandingPage.kt b/server/src/main/kotlin/core/ui/LandingPage.kt new file mode 100644 index 0000000..90d15f1 --- /dev/null +++ b/server/src/main/kotlin/core/ui/LandingPage.kt @@ -0,0 +1,74 @@ +package net.keinekekse.app.core.ui + +import io.ktor.util.logging.* +import io.ktor.utils.io.* +import kotlinx.html.* +import net.keinekekse.app.VERSION_books +import net.keinekekse.app.books.ui.materialSymbol +import net.keinekekse.app.books.ui.navBooks +import net.keinekekse.app.core.config.LOGIN_PATH +import net.keinekekse.app.core.config.LOGOUT_PATH + +val LOG = KtorSimpleLogger("net.keinekekse.app.core.ui.LandingPage") + +@OptIn(ExperimentalKtorApi::class) +fun HTML.landingPage(loggedOn: Boolean) { +// val String = "Ktor Application Server" + LOG.info("LandingPage loggedOn $loggedOn") + + mainApplicationPage { + div(classes = "$cssFlexColumnCenter w-full") { + div(classes = "p-8") { + a(classes = "inline-flex $cssMainLink") { + href = navBooks.second + + img(classes = "h-14 my-2") { + src = "/static/books.png" + } + div(classes = "self-center text-right ml-3") { + div { + div { + +VERSION_books.description + } + div(classes = "text-xs") { + +VERSION_books.version + } + } + } + } + +// a(classes = "p-2 text-xl font-bold text-sky-500 hover:text-sky-700 hover:bg-sky-100 cursor-pointer") { +// href = "/books" +// +VERSION_books.description +// } + } + div(classes = "m-8") { + a(classes = cssMainLink) { + href = "/admin" + materialSymbol("Settings") + +" Configuration" + } + } + div(classes = "m-8") { +// a(classes = "p-2 font-bold text-sky-500 hover:text-sky-700 hover:bg-cyan-100 cursor-pointer") { +// href = "/oidc/keycloak/login" +// +"Login" +// } + var url = LOGIN_PATH + var method = FormMethod.get + var text = "Login" + if (loggedOn) { + url = LOGOUT_PATH // /logout ? + method = FormMethod.post + text = "Logout" + } + form(action = url, method = method) { + button(classes = cssMainButton) { // "ml-4 $cssMainLink disabled:text-slate-400 disabled:cursor-not-allowed") { + type = ButtonType.submit + +text + } + } + } + } + } +} diff --git a/server/src/main/kotlin/core/ui/PageTemplate.kt b/server/src/main/kotlin/core/ui/PageTemplate.kt new file mode 100644 index 0000000..f9e305a --- /dev/null +++ b/server/src/main/kotlin/core/ui/PageTemplate.kt @@ -0,0 +1,59 @@ +package net.keinekekse.app.core.ui + +import kotlinx.html.* +import net.keinekekse.app.VERSION +import net.keinekekse.app.books.ui.cssButton + +const val bgMainHeader = "bg-sky-300" +const val textMainHeader = "text-sky-900" +const val bgMainBody = "bg-cyan-50" +const val textMainBody = "text-sky-500" +const val textMainBodyHover = "hover:text-sky-700" +const val bgMainBodyHover = "hover:bg-sky-200" + +const val cssMainWidth = "max-w-270" + +const val cssFlexColumnCenter = "inline-flex flex-col items-center" +const val cssFlexRowCenter = "inline-flex flex-row justify-center" +const val cssFullscreen = "flex w-full h-screen overflow-hidden" + +const val cssCode = "px-1 font-mono bg-slate-100 rounded-md border-slate-300 border-1" + +const val cssMainLink = + "py-2 px-4 text-xl cursor-pointer font-bold $textMainBody $textMainBodyHover $bgMainBodyHover" +const val cssMainButton = + "text-xl font-bold $cssButton $textMainBody bg-sky-200 hover:bg-sky-400 hover:text-sky-50" + +fun HTML.mainApplicationPage(block: DIV.() -> Unit = {}) { + head { + stdHead(VERSION.displayName) + } + + body { + // Box + div(classes = "$cssFullscreen justify-center") { + // Head + div(classes = "$cssFlexColumnCenter m-10 w-8/10 h-9/10 overflow-hidden $cssMainWidth shadow-xl") { + h1(classes = "$cssFlexRowCenter w-full rounded-t-lg text-4xl font-bold py-3 px-6 $textMainHeader $bgMainHeader") { + a { + href = "https://ktor.io" + img(classes = "h-12 mr-4 ") { + src = "/static/ktor_icon.png" + } + } + div { + title = VERSION.fullVersion + +VERSION.description + } + } + // body + div(classes = "inline-flex $bgMainBody py-10 h-full overflow-hidden w-full rounded-b-lg") { + + block() + + } + } + } + } + +} diff --git a/server/src/main/kotlin/core/ui/StdHead.kt b/server/src/main/kotlin/core/ui/StdHead.kt new file mode 100644 index 0000000..c248a98 --- /dev/null +++ b/server/src/main/kotlin/core/ui/StdHead.kt @@ -0,0 +1,29 @@ +package net.keinekekse.app.core.ui + +import kotlinx.html.* + +fun HEAD.stdHead(title: String = "", importStyles: Boolean = true) { + title(title) + meta(name = "viewport", content = "width=device-width, initial-scale=1") + script(src = "/web.js") {} + script { + unsafe { + raw( + """ + function toggleField(hideObj,showObj){ + hideObj.disabled=true; + hideObj.style.display='none'; + showObj.disabled=false; + showObj.style.display='inline'; + showObj.focus(); + } + """.trimIndent() + ) + } + + } + + // alle weiteren Stylesheets werden in books.css importiert + if (importStyles) + link(rel = "stylesheet", href = "/static/styles/books.css") +} diff --git a/server/src/main/kotlin/core/zz/CitySchema.kt b/server/src/main/kotlin/core/zz/CitySchema.kt new file mode 100644 index 0000000..c929b93 --- /dev/null +++ b/server/src/main/kotlin/core/zz/CitySchema.kt @@ -0,0 +1,72 @@ +package net.keinekekse.app.app.zz + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import java.sql.Connection +import java.sql.Statement + +@Serializable +data class City(val name: String, val population: Int) +class CityService(private val connection: Connection) { + companion object { + private const val CREATE_TABLE_CITIES = + "CREATE TABLE CITIES (ID SERIAL PRIMARY KEY, NAME VARCHAR(255), POPULATION INT);" + private const val SELECT_CITY_BY_ID = "SELECT name, population FROM cities WHERE id = ?" + private const val INSERT_CITY = "INSERT INTO cities (name, population) VALUES (?, ?)" + private const val UPDATE_CITY = "UPDATE cities SET name = ?, population = ? WHERE id = ?" + private const val DELETE_CITY = "DELETE FROM cities WHERE id = ?" + + } + + init { + val statement = connection.createStatement() + statement.executeUpdate(CREATE_TABLE_CITIES) + } + + // Create new city + suspend fun create(city: City): Int = withContext(Dispatchers.IO) { + val statement = connection.prepareStatement(INSERT_CITY, Statement.RETURN_GENERATED_KEYS) + statement.setString(1, city.name) + statement.setInt(2, city.population) + statement.executeUpdate() + + val generatedKeys = statement.generatedKeys + if (generatedKeys.next()) { + return@withContext generatedKeys.getInt(1) + } else { + throw Exception("Unable to retrieve the id of the newly inserted city") + } + } + + // Read a city + suspend fun read(id: Int): City = withContext(Dispatchers.IO) { + val statement = connection.prepareStatement(SELECT_CITY_BY_ID) + statement.setInt(1, id) + val resultSet = statement.executeQuery() + + if (resultSet.next()) { + val name = resultSet.getString("name") + val population = resultSet.getInt("population") + return@withContext City(name, population) + } else { + throw Exception("Record not found") + } + } + + // Update a city + suspend fun update(id: Int, city: City) = withContext(Dispatchers.IO) { + val statement = connection.prepareStatement(UPDATE_CITY) + statement.setString(1, city.name) + statement.setInt(2, city.population) + statement.setInt(3, id) + statement.executeUpdate() + } + + // Delete a city + suspend fun delete(id: Int) = withContext(Dispatchers.IO) { + val statement = connection.prepareStatement(DELETE_CITY) + statement.setInt(1, id) + statement.executeUpdate() + } +} diff --git a/server/src/main/kotlin/core/zz/Exposed.kt b/server/src/main/kotlin/core/zz/Exposed.kt new file mode 100644 index 0000000..d87a5c2 --- /dev/null +++ b/server/src/main/kotlin/core/zz/Exposed.kt @@ -0,0 +1,58 @@ +package net.keinekekse.app.core.zz + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import net.keinekekse.app.app.zz.ExposedUser +import net.keinekekse.app.app.zz.ExposedUserService +import org.jetbrains.exposed.v1.r2dbc.R2dbcDatabase + +// TODO kann weg sobald sich die Beispiele erledigt habne + +suspend fun Application.configureExposed() { + val database = R2dbcDatabase.connect( + url = "r2dbc:h2:file:///./h2", + user = "root", + password = "", + ) + val userService = ExposedUserService(database).also { + it.createSchema() + } + + routing { + // Create user + post("/users") { + val user = call.receive() + val id = userService.create(user) + call.respond(HttpStatusCode.Created, id) + } + + // Read user + get("/users/{id}") { + val id = call.parameters["id"]?.toUInt() ?: throw IllegalArgumentException("Invalid ID") + val user = userService.read(id) + if (user != null) { + call.respond(HttpStatusCode.OK, user) + } else { + call.respond(HttpStatusCode.NotFound) + } + } + + // Update user + put("/users/{id}") { + val id = call.parameters["id"]?.toUInt() ?: throw IllegalArgumentException("Invalid ID") + val user = call.receive() + userService.update(id, user) + call.respond(HttpStatusCode.NoContent) + } + + // Delete user + delete("/users/{id}") { + val id = call.parameters["id"]?.toUInt() ?: throw IllegalArgumentException("Invalid ID") + userService.delete(id) + call.respond(HttpStatusCode.NoContent) + } + } +} diff --git a/server/src/main/kotlin/core/zz/GreetingService.kt b/server/src/main/kotlin/core/zz/GreetingService.kt new file mode 100644 index 0000000..8d0657b --- /dev/null +++ b/server/src/main/kotlin/core/zz/GreetingService.kt @@ -0,0 +1,5 @@ +package net.keinekekse.app.app.zz + +fun interface GreetingService { + fun sayHello(): String +} diff --git a/server/src/main/kotlin/core/zz/LeaderboardPage.kt b/server/src/main/kotlin/core/zz/LeaderboardPage.kt new file mode 100644 index 0000000..b0eebc1 --- /dev/null +++ b/server/src/main/kotlin/core/zz/LeaderboardPage.kt @@ -0,0 +1,95 @@ +package net.keinekekse.app.app.zz + +import io.ktor.htmx.html.* +import io.ktor.utils.io.* +import kotlinx.html.* +import java.nio.file.Paths +import kotlin.io.path.exists +import kotlin.io.path.useLines +import kotlin.random.Random + +fun HTML.leaderboardPage(random: Random) { + head { + title("HTMX Example") + script(src = "/web.js") {} + link(rel = "stylesheet", href = "/leaderboard.css") + } + body { + h1 { + +"Leaderboard" + } + table { + id = "leaderboard" + thead { + tr { + th { +"Alias" } + th { +"Score" } + } + } + tbody { + randomRows(random) + } + } + h2 { + id = "total-count" + +"Total: 10" + } + } +} + +fun TBODY.randomRows(random: Random) { + for (contestant in generateSequence { random.nextContestant() }.take(10)) { + tr { + td { +contestant.alias } + td { +contestant.score.toString() } + } + } + loadMoreRows() +} + +@OptIn(ExperimentalKtorApi::class) +fun TBODY.loadMoreRows() { + tr { + id = "replaceMe" + td { + colSpan = "3" + style = "text-align: center;" + + button { + attributes.hx { + get = "/more-rows" + target = "#replaceMe" + swap = "outerHTML" + trigger = "click" + select = "tr" + } + + +"Load More..." + } + } + } +} + +private val dictionary: List by lazy { + listOf("/usr/share/dict/words", "/usr/dict/words").map(Paths::get).firstOrNull { + it.exists() + }?.let { dictionaryFile -> + dictionaryFile.useLines(Charsets.UTF_8) { lines -> + lines.filter { it.length >= 4 }.toList() + } + } ?: listOf("red", "blue", "green", "yellow", "pink", "violet", "black") +} + +fun Random.nextContestant(): Contestant = + Contestant( + nextAlias(), + nextInt(1_000_000) + ) + +fun Random.nextAlias(): String = + dictionary[nextInt(dictionary.size - 1)] + +data class Contestant( + val alias: String, + val score: Int, +) diff --git a/server/src/main/kotlin/core/zz/Postgres.kt b/server/src/main/kotlin/core/zz/Postgres.kt new file mode 100644 index 0000000..baaf2eb --- /dev/null +++ b/server/src/main/kotlin/core/zz/Postgres.kt @@ -0,0 +1,96 @@ +package net.keinekekse.app.core.zz + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import net.keinekekse.app.app.zz.City +import net.keinekekse.app.app.zz.CityService +import java.sql.Connection +import java.sql.DriverManager + +// TODO kann weg sobald sich die Beispiele erledigt haben + +fun Application.configurePostgres() { + + val dbConnection: Connection = connectToPostgres(embedded = true) + val cityService = CityService(dbConnection) + + // dependency injection +// dependencies { +// provide("testDb") { dbConnection } +// } + + routing { + + // Create city + post("/cities") { + val city = call.receive() + val id = cityService.create(city) + call.respond(HttpStatusCode.Created, id) + } + + // Read city + get("/cities/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + try { + val city = cityService.read(id) + call.respond(HttpStatusCode.OK, city) + } catch (e: Exception) { + call.respond(HttpStatusCode.NotFound) + } + } + + // Update city + put("/cities/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + val user = call.receive() + cityService.update(id, user) + call.respond(HttpStatusCode.OK) + } + + // Delete city + delete("/cities/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") + cityService.delete(id) + call.respond(HttpStatusCode.OK) + } + } +} + +/** + * Makes a connection to a Postgres database. + * + * In order to connect to your running Postgres process, + * please specify the following parameters in your configuration file: + * - postgres.url -- Url of your running database process. + * - postgres.user -- Username for database connection + * - postgres.password -- Password for database connection + * + * If you don't have a database process running yet, you may need to [download]((https://www.postgresql.org/download/)) + * and install Postgres and follow the instructions [here](https://postgresapp.com/). + * Then, you would be able to edit your url, which is usually "jdbc:postgresql://host:port/database", as well as + * user and password values. + * + * + * @param embedded -- if [true] defaults to an embedded database for tests that runs locally in the same process. + * In this case you don't have to provide any parameters in configuration file, and you don't have to run a process. + * + * @return [Connection] that represent connection to the database. Please, don't forget to close this connection when + * your application shuts down by calling [Connection.close] + * */ +fun Application.connectToPostgres(embedded: Boolean): Connection { + Class.forName("org.postgresql.Driver") + if (embedded) { + log.info("Using embedded H2 database for testing; replace this flag to use postgres") + return DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "root", "") + } else { + val url = environment.config.property("postgres.url").getString() + log.info("Connecting to postgres database at $url") + val user = environment.config.property("postgres.user").getString() + val password = environment.config.property("postgres.password").getString() + + return DriverManager.getConnection(url, user, password) + } +} diff --git a/server/src/main/kotlin/core/zz/UserSession.kt b/server/src/main/kotlin/core/zz/UserSession.kt new file mode 100644 index 0000000..d3d6dfd --- /dev/null +++ b/server/src/main/kotlin/core/zz/UserSession.kt @@ -0,0 +1,3 @@ +package net.keinekekse.app.app.zz + +class UserSession(accessToken: String) diff --git a/server/src/main/kotlin/core/zz/UsersService.kt b/server/src/main/kotlin/core/zz/UsersService.kt new file mode 100644 index 0000000..fad3557 --- /dev/null +++ b/server/src/main/kotlin/core/zz/UsersService.kt @@ -0,0 +1,56 @@ +package net.keinekekse.app.app.zz + +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.v1.core.dao.id.UIntIdTable +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.r2dbc.* +import org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction + +@Serializable +data class ExposedUser(val name: String, val age: Int) + +class ExposedUserService(val database: R2dbcDatabase) { + object Users : UIntIdTable() { + val name = varchar("name", length = 50) + val age = integer("age") + } + + suspend fun createSchema() { + suspendTransaction(database) { + SchemaUtils.create(Users) + } + } + + suspend fun create(user: ExposedUser): UInt = suspendTransaction(database) { + val newRecord = Users.insert { + it[name] = user.name + it[age] = user.age + } + newRecord[Users.id].value + } + + suspend fun read(id: UInt): ExposedUser? { + return suspendTransaction(database) { + Users.selectAll() + .where { Users.id eq id } + .map { ExposedUser(it[Users.name], it[Users.age]) } + .singleOrNull() + } + } + + suspend fun update(id: UInt, user: ExposedUser) { + suspendTransaction(database) { + Users.update({ Users.id eq id }) { + it[name] = user.name + it[age] = user.age + } + } + } + + suspend fun delete(id: UInt) { + suspendTransaction(database) { Users.deleteWhere { Users.id.eq(id) } } + } + +} diff --git a/server/src/main/kotlin/main.kt b/server/src/main/kotlin/main.kt new file mode 100644 index 0000000..144aed4 --- /dev/null +++ b/server/src/main/kotlin/main.kt @@ -0,0 +1,5 @@ +package net.keinekekse.app + +fun main(args: Array) { + io.ktor.server.netty.EngineMain.main(args) +} diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml new file mode 100644 index 0000000..884af0d --- /dev/null +++ b/server/src/main/resources/application.yaml @@ -0,0 +1,33 @@ +ktor: + deployment: + port: 7770 + application: + modules: + - net.keinekekse.app.core.config.HttpKt.configureHttp + - net.keinekekse.app.core.config.SerializationKt.configureSerialization + - net.keinekekse.app.core.config.StatusPagesKt.configureStatusPages + - net.keinekekse.app.core.config.HtmlDslKt.configureHtmlDsl + - net.keinekekse.app.core.config.SecurityKt.configureSecurity + - net.keinekekse.app.core.config.DatabaseKt.configureCoreDatabase + - net.keinekekse.app.core.config.DIKt.configureDependencyInjection + - net.keinekekse.app.core.config.RoutingKt.configureRouting + - net.keinekekse.app.books.config.DatabaseKt.configureBooksDatabase + - net.keinekekse.app.books.config.DIKt.configureBookDependencyInjection + - net.keinekekse.app.books.config.RoutingKt.configureRouting + +postgres: + # DB-Params werden vom Container als env vars gesetzt + host: ${DB_HOST} + port: ${DB_PORT} + books_schema: ${BOOKS_SCHEMA} + books_pwd: ${BOOKS_DB_PASSWORD} + kas_schema: ${KAS_SCHEMA} + kas_pwd: ${KAS_DB_PASSWORD} + +keycloak: + url: "https://kec.keinekekse.net" + realm: "app-server-realm" + clientId: "pkce-book-app" + +kas: + serverUrl: "https://kas.keinekekse.net" diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml new file mode 100644 index 0000000..ff966fb --- /dev/null +++ b/server/src/main/resources/logback.xml @@ -0,0 +1,18 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/server/src/main/resources/static/books.png b/server/src/main/resources/static/books.png new file mode 100644 index 0000000..0f83eb6 Binary files /dev/null and b/server/src/main/resources/static/books.png differ diff --git a/server/src/main/resources/static/fonts/material-symbols.woff2 b/server/src/main/resources/static/fonts/material-symbols.woff2 new file mode 100644 index 0000000..e0904bd Binary files /dev/null and b/server/src/main/resources/static/fonts/material-symbols.woff2 differ diff --git a/server/src/main/resources/static/index.html b/server/src/main/resources/static/index.html new file mode 100644 index 0000000..4ae4f5b --- /dev/null +++ b/server/src/main/resources/static/index.html @@ -0,0 +1,7 @@ + + + + +

Hello Ktor!

+ + diff --git a/server/src/main/resources/static/ktor_icon.png b/server/src/main/resources/static/ktor_icon.png new file mode 100644 index 0000000..2cc68e9 Binary files /dev/null and b/server/src/main/resources/static/ktor_icon.png differ diff --git a/server/src/main/resources/static/scripts/books.js b/server/src/main/resources/static/scripts/books.js new file mode 100644 index 0000000..ce990c3 --- /dev/null +++ b/server/src/main/resources/static/scripts/books.js @@ -0,0 +1,21 @@ +function onScanSuccess (decodedText, decodedResult) { + console.log(`Gescannter Code: ` + decodedText) + + // Scanner stoppen, sobald ein Code erkannt wurde + html5QrcodeScanner.clear() + + // HTMX AJAX-Aufruf auslösen, um Buchdaten vom Backend zu laden: + htmx.ajax('GET', '/books/isbnlookup/' + encodeURIComponent(decodedText), { + target: '#book-result', + swap : 'innerHTML' + }) +} + +function renderScanner () { + const html5QrcodeScanner = new Html5QrcodeScanner( + 'reader', + {fps: 10, qrbox: {width: 250, height: 150}}, + /* verbose= */ false + ) + html5QrcodeScanner.render(onScanSuccess) +} diff --git a/server/src/main/resources/static/styles/books.css b/server/src/main/resources/static/styles/books.css new file mode 100644 index 0000000..a05c899 --- /dev/null +++ b/server/src/main/resources/static/styles/books.css @@ -0,0 +1,31 @@ +/* Google Material Symbols (die Datei landet beim Build im web root) */ +@import url("/static/styles/google.css"); + +/* Bootstrap Icon - Alternative zu Google, Datei ist etwa halb so groß */ +/* @import url("/bootstrap-icons.min.css"); */ + +/* Tailwind - .css wird beim Build generiert */ +@import url("/static/styles/tailwind.css"); + +/* + Fixed Table Header -> THEAD + + zusätzlich passende background-color setzten + */ +thead.fixed-table-header { + position: sticky; + inset-block-start: 0; + z-index: 1; +} + +/* + Den lästigen blauen Rand abschalten, muss bei interaktiven Elementen + gezielt gesetzt werden (i. A. analog hover). + */ +@layer utilities { + *:focus, + *:focus-visible { + outline: none; + border: 0; + } +} diff --git a/server/src/main/resources/static/styles/google.css b/server/src/main/resources/static/styles/google.css new file mode 100644 index 0000000..e312007 --- /dev/null +++ b/server/src/main/resources/static/styles/google.css @@ -0,0 +1,35 @@ +/* + Material Symbols + ---------------- + Ansehen unter: https://fonts.google.com/icons + Doku: https://developers.google.com/fonts/docs/material_symbols?hl=de + + > git clone https://github.com/google/material-design-icons + > cp material-design-icons/variablefont/*.woff2 . + > rm -rf material-design-icons + + MaterialSymbolsOutline*.woff2 nach resources/material-symbols.woff kopieren + + symbol + bzw. + span(classes = "material-symbols-outlined") { +"symbol" } +*/ +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + src: url(/static/fonts/material-symbols.woff2) format('woff2'); +} + +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; +} diff --git a/server/src/test_/kotlin/ServerTest.kt b/server/src/test_/kotlin/ServerTest.kt new file mode 100644 index 0000000..b217a4f --- /dev/null +++ b/server/src/test_/kotlin/ServerTest.kt @@ -0,0 +1,15 @@ +package net.keinekekse.app + +class ServerTest { + +// @Test +// klappt nicht, da die Container-Umgebung fehlt (incl. DB) +// -> evtl. später mal checken +// fun `test root endpoint`() = testApplication { +// // loads default configuration +// configure() +// // verify server root returns 200 +// assertEquals(HttpStatusCode.OK, client.get("/").status) +// } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..035b575 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,42 @@ +pluginManagement { + repositories { + maven("https://nexus.keinekekse.net/repository/maven-public/") + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + maven("https://nexus.keinekekse.net/repository/maven-public/") + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + } + versionCatalogs { + create("masterLibs") { + from("net.keinekekse:version-catalog:141") + } + create("ktorLibs").from("io.ktor:ktor-version-catalog:3.6.0") + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +rootProject.name = "app" + +include(":server") +include(":web") +include(":shared") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..836bee0 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) +} + +kotlin { + sourceSets { + jvm() + js { + browser() + binaries.executable() + } + commonMain.dependencies { +// implementation(project(":server")) + } + } +} diff --git a/shared/src/commonMain/kotlin/books/WebSharedConst.kt b/shared/src/commonMain/kotlin/books/WebSharedConst.kt new file mode 100644 index 0000000..ebbbe16 --- /dev/null +++ b/shared/src/commonMain/kotlin/books/WebSharedConst.kt @@ -0,0 +1,3 @@ +package net.keinekekse.app.books + +const val isbnScanResultPath = "/books/new/scan/fetch" diff --git a/tailwind/build.sh b/tailwind/build.sh new file mode 100755 index 0000000..ebd91a3 --- /dev/null +++ b/tailwind/build.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd $(dirname $0) +npx @tailwindcss/cli -i ./input.css -o ../server/build/resources/main/web/tailwind.css diff --git a/tailwind/input.css b/tailwind/input.css new file mode 100644 index 0000000..21fbaad --- /dev/null +++ b/tailwind/input.css @@ -0,0 +1 @@ +@import "tailwindcss" source("../server/src"); diff --git a/tailwind/package-lock.json b/tailwind/package-lock.json new file mode 100644 index 0000000..f2b16cb --- /dev/null +++ b/tailwind/package-lock.json @@ -0,0 +1,1061 @@ +{ + "name": "server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@tailwindcss/cli": "^4.3.3", + "tailwindcss": "^4.3.3" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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==", + "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==", + "hasInstallScript": true, + "license": "MIT", + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.3.tgz", + "integrity": "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "2.5.1", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "enhanced-resolve": "^5.24.1", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.3" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "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==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + } + } +} diff --git a/tailwind/package.json b/tailwind/package.json new file mode 100644 index 0000000..c0a542c --- /dev/null +++ b/tailwind/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@tailwindcss/cli": "^4.3.3", + "tailwindcss": "^4.3.3" + } +} diff --git a/web/build.gradle.kts b/web/build.gradle.kts new file mode 100644 index 0000000..a53fc8d --- /dev/null +++ b/web/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) +} + + +kotlin { + js { + browser() + binaries.executable() + } + + sourceSets { + jsMain.dependencies { + implementation(npm("htmx.org", libs.versions.htmx.get())) + implementation(npm("html5-qrcode", libs.versions.html5Qrcode.get())) +// implementation(npm("@popperjs/core", "2.11.8")) +// implementation(npm("bootstrap", "5.3.8")) +// implementation(npm("tailwindcss", "*")) + + } + } + sourceSets { + commonMain.dependencies { + implementation(project(":shared")) + } + } +} diff --git a/web/src/jsMain/kotlin/Html5Qrcode.kt b/web/src/jsMain/kotlin/Html5Qrcode.kt new file mode 100644 index 0000000..77cde43 --- /dev/null +++ b/web/src/jsMain/kotlin/Html5Qrcode.kt @@ -0,0 +1,30 @@ +@file:JsModule("html5-qrcode") +@file:JsNonModule + +package net.keinekekse.app + +/* AI sagt: + In Kotlin/JS werden Named Exports eines npm-Pakets deklariert, indem die + Annotation auf Dateiebene (@file:JsModule) gesetzt wird. Dadurch weiß der + Compiler, dass jede Top-Level-Klasse in dieser Datei ein benannter Export + des Moduls ist. + */ +external class Html5QrcodeScanner( + elementId: String, + config: dynamic, + verbose: Boolean = definedExternally, +) { + fun render( + successCallback: (decodedText: String, decodedResult: dynamic) -> Unit, + errorCallback: ((errorMessage: String) -> Unit)? = definedExternally, + ) + + fun clear(): dynamic +} + +external class Html5Qrcode( + elementId: String, + config: dynamic = definedExternally, +) { + // Falls später benötigt +} diff --git a/web/src/jsMain/kotlin/import.kt b/web/src/jsMain/kotlin/import.kt new file mode 100644 index 0000000..06ca8f8 --- /dev/null +++ b/web/src/jsMain/kotlin/import.kt @@ -0,0 +1,5 @@ +package net.keinekekse.app + +@JsModule("htmx.org") +@JsNonModule +external object htmx diff --git a/web/src/jsMain/kotlin/main.kt b/web/src/jsMain/kotlin/main.kt new file mode 100644 index 0000000..226020d --- /dev/null +++ b/web/src/jsMain/kotlin/main.kt @@ -0,0 +1,71 @@ +package net.keinekekse.app + +import kotlinx.browser.document +import kotlinx.browser.window +import net.keinekekse.app.books.isbnScanResultPath + +fun main() { + window.asDynamic().renderScanner = ::renderScanner + + // htmx reference is needed for import + console.log("htmx loaded ", htmx) + + // event handling for htmx + var rowCount = 1 + document.addEventListener("htmx:beforeSwap", { + document.getElementById("total-count")?.innerHTML = "Total: ${(++rowCount) * 10}" + }) + // Scroll to the bottom of the page after adding content + document.addEventListener("htmx:afterSwap", { + val scrollHeight = document.body?.scrollHeight ?: 0 + window.scrollTo(0.0, scrollHeight.toDouble()) + }) +} + +/* + Die fn wird in den Namensraum . veröffentlicht + -> web.net.keinekekse.app + Wenn die fn ohne den Rattenschwanz aufgerufen werden soll, muss sie + in main() so exportiert werden: + js("window.renderScanner = renderScanner") + oder + window.asDynamic().renderScanner = ::renderScanner + + Des Weiteren benötigt sie ein DOM-Element (id="reader"), daher ist es sinnvoll, + sie erst bei geladenem DOM auszuführen: + document.addEventListener('DOMContentLoaded', function () { + renderScanner(); + }) + + Beim Import von htmx wird der gesamte Import an ein Objekt namens "default" + gehängt. Da sich auch Ktor auf diesen Import verlässt, ist es hier besser, + zu kapitulieren und das Objekt als htmx.asDynamic().default. zu + verwenden. + */ +@OptIn(ExperimentalJsExport::class) +@JsExport +fun renderScanner(scanTarget: String = "reader") { + // Ausgabebereich fürs Ergebnis zurücksetzen + document.getElementById("scan-result")?.innerHTML = + "

Bitte Barcode vor die Kamera halten...

" + + // Scanner öffnen + val config = js("{ fps: 10, qrbox: { width: 300, height: 180 } }") + val scanner = Html5QrcodeScanner(scanTarget, config, /*debug*/ false) + + // Ergebnis verarbeiten + scanner.render({ decodedText, _ -> + scanner.clear() + document.getElementById("scan-result")?.innerHTML = + "

Hole Daten für ISBN: $decodedText

" + console.log("Scanned: $decodedText") +// console.log("htmx: ", htmx) + htmx.asDynamic().default.ajax( + "GET", + "${isbnScanResultPath}/" + js("encodeURIComponent(decodedText)"), + js("{ target: '#scan-result', swap: 'innerHTML' }") + ) + }, { errorMessage -> + console.log("Error: $errorMessage") + }) +} diff --git a/web/src/jsMain/resources/favicon.ico b/web/src/jsMain/resources/favicon.ico new file mode 100644 index 0000000..b2dedd1 Binary files /dev/null and b/web/src/jsMain/resources/favicon.ico differ