From 26e8909998cf5bd0e9c7fb6e12e63845dba716ec Mon Sep 17 00:00:00 2001 From: ngn Date: Sat, 4 Jan 2025 00:00:10 +0300 Subject: [PATCH 01/12] restructure the API and update the admin script --- admin/Makefile | 12 + admin/admin.py | 429 +++++++++++++++++++++++----------- admin/install.sh | 8 - admin/tests/test_news.json | 12 + admin/tests/test_service.json | 9 + api/.gitignore | 2 +- api/Dockerfile | 8 +- api/Makefile | 12 +- api/config/config.go | 133 +++++++---- api/config/option.go | 49 ++++ api/database/admin.go | 62 +++++ api/database/database.go | 96 +++++--- api/database/multilang.go | 58 +++++ api/database/news.go | 116 +++++++++ api/database/post.go | 71 ------ api/database/service.go | 125 +++++++--- api/database/vote.go | 49 ---- api/go.mod | 8 +- api/go.sum | 10 +- api/main.go | 126 +++++----- api/routes/admin.go | 173 ++++++-------- api/routes/blog.go | 203 ---------------- api/routes/index.go | 32 +++ api/routes/news.go | 47 ++++ api/routes/services.go | 35 ++- api/routes/vote.go | 139 ----------- api/status/service.go | 105 +++++++++ api/status/status.go | 139 +++++++++++ api/util/log.go | 17 +- api/util/res.go | 89 +++++++ api/util/util.go | 67 ++++++ api/util/utils.go | 74 ------ api/views/index.md | 161 +++++++++++++ api/views/news.xml | 6 + 34 files changed, 1699 insertions(+), 983 deletions(-) create mode 100644 admin/Makefile delete mode 100755 admin/install.sh create mode 100644 admin/tests/test_news.json create mode 100644 admin/tests/test_service.json create mode 100644 api/config/option.go create mode 100644 api/database/admin.go create mode 100644 api/database/multilang.go create mode 100644 api/database/news.go delete mode 100644 api/database/post.go delete mode 100644 api/database/vote.go delete mode 100644 api/routes/blog.go create mode 100644 api/routes/index.go create mode 100644 api/routes/news.go delete mode 100644 api/routes/vote.go create mode 100644 api/status/service.go create mode 100644 api/status/status.go create mode 100644 api/util/res.go create mode 100644 api/util/util.go delete mode 100644 api/util/utils.go create mode 100644 api/views/index.md create mode 100644 api/views/news.xml diff --git a/admin/Makefile b/admin/Makefile new file mode 100644 index 0000000..f284023 --- /dev/null +++ b/admin/Makefile @@ -0,0 +1,12 @@ +SRCS = $(wildcard *.py) +PREFIX = /usr + +all: + +format: + black $(SRCS) + +install: + install -Dm755 admin.py $(PREFIX)/bin/admin_script + +.PHONY: format install diff --git a/admin/admin.py b/admin/admin.py index a7c1dea..87f9ceb 100644 --- a/admin/admin.py +++ b/admin/admin.py @@ -1,176 +1,331 @@ #!/bin/python3 """ -Administration script for my website (ngn.tf) -############################################# -I really enjoy doing stuff from the terminal, -so I wrote this simple python script that interacts -with the API and lets me add/remove new posts/services -from the terminal + +website/admin | Administration script for my personal website +written by ngn (https://ngn.tf) (2025) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + """ -from os import remove, getenv +from urllib.parse import quote_plus +from typing import Dict, List, Any +from datetime import datetime, UTC +from colorama import Fore, Style +from json import dumps, loads from getpass import getpass import requests as req +from os import getenv from sys import argv -URL = "" +API_URL_ENV = "API_URL" -def join(pth: str) -> str: - if URL == None: - return "" - if URL.endswith("/"): - return URL+pth - return URL+"/"+pth +# logger used by the script +class Log: + def __init__(self) -> None: + self.reset = Fore.RESET + Style.RESET_ALL -def get_token() -> str: - try: - f = open("/tmp/wa", "r") - token = f.read() - f.close() - return token - except: - print("[-] You are not authenticated") - exit(1) + def info(self, m: str) -> None: + print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m) -def login() -> None: - pwd = getpass("[>] Enter your password: ") - res = req.get(join("admin/login")+f"?pass={pwd}").json() - if res["error"] != "": - print(f"[-] Error logging in: {res['error']}") - return + def error(self, m: str) -> None: + print(Fore.RED + Style.BRIGHT + "[-]" + self.reset + " " + m) - token = res["token"] - f = open("/tmp/wa", "w") - f.write(token) - f.close() + def input(self, m: str) -> str: + return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ") -def logout() -> None: - token = get_token() - res = req.get(join("admin/logout"), headers={ - "Authorization": token - }).json() - if res["error"] != "": - print(f"[-] Error logging out: {res['error']}") - return + def password(self, m: str) -> str: + return getpass(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ") - remove("/tmp/wa") - print("[+] Logged out") -def add_post() -> None: - token = get_token() - title = input("[>] Post title: ") - author = input("[>] Post author: ") - content_file = input("[>] Post content file: ") - public = input("[>] Should post be public? (y/n): ") +# API interface for the admin endpoints +class AdminAPI: + def __init__(self, url: str, password: str) -> None: + self.languages: List[str] = [ + "en", + "tr", + ] # languages supported by multilang fields + self.password = password + self.api_url = url - try: - f = open(content_file, "r") - content = f.read() - f.close() - except: - print("[-] Content file not found") - return + def _title_to_id(self, title: str) -> str: + return title.lower().replace(" ", "_") - res = req.put(join("admin/blog/add"), json={ - "title": title, - "author": author, - "content": content, - "public": 1 if public == "y" else 0 - }, headers={ - "Authorization": token - }).json() + def _check_multilang_field(self, ml: Dict[str, str]) -> bool: + for l in self.languages: + if l in ml and ml[l] != "": + return True + return False - if res["error"] != "": - print(f"[-] Error adding post: {res['error']}") - return + def _api_url_join(self, path: str) -> str: + api_has_slash = self.api_url.endswith("/") + path_has_slash = path.startswith("/") - print("[+] Post has been added") + if api_has_slash or path_has_slash: + return self.api_url + path + elif api_has_slash and path_has_slash: + return self.api_url + path[1:] + else: + return self.api_url + "/" + path -def remove_post() -> None: - token = get_token() - id = input("[>] Post ID: ") - res = req.delete(join("admin/blog/remove")+f"?id={id}", headers={ - "Authorization": token - }).json() + def _to_json(self, res: req.Response) -> dict: + if res.status_code == 403: + raise Exception("Authentication failed") - if res["error"] != "": - print(f"[-] Error removing post: {res['error']}") - return + json = res.json() - print("[-] Post has been removed") + if json["error"] != "": + raise Exception("API error: %s" % json["error"]) -def add_service() -> None: - token = get_token() - name = input("[>] Serivce name: ") - desc = input("[>] Serivce desc: ") - link = input("[>] Serivce URL: ") + return json - res = req.put(join("admin/service/add"), json={ - "name": name, - "desc": desc, - "url": link - }, headers={ - "Authorization": token - }).json() + def PUT(self, url: str, data: dict) -> req.Response: + return self._to_json( + req.put( + self._api_url_join(url), + json=data, + headers={"Authorization": self.password}, + ) + ) - if res["error"] != "": - print(f"[-] Error adding service: {res['error']}") - return + def DELETE(self, url: str) -> req.Response: + return self._to_json( + req.delete( + self._api_url_join(url), headers={"Authorization": self.password} + ) + ) - print("[+] Service has been added") + def GET(self, url: str) -> req.Response: + return self._to_json( + req.get(self._api_url_join(url), headers={"Authorization": self.password}) + ) -def remove_service() -> None: - token = get_token() - name = input("[>] Service name: ") - res = req.delete(join("admin/service/remove")+f"?name={name}", headers={ - "Authorization": token - }).json() + def add_service(self, service: Dict[str, str]): + if not "name" in service or service["name"] == "": + raise Exception('Service structure is missing required "name" field') - if res["error"] != "": - print(f"[-] Error removing service: {res['error']}") - return + if not "desc" in service: + raise Exception('Service structure is missing required "desc" field') - print("[+] Serivce has been removed") + if ( + (not "clear" in service or service["clear"] == "") + and (not "onion" in service or service["onion"] == "") + and (not "i2p" in service or service["i2p"] == "") + ): + raise Exception( + 'Service structure is missing "clear", "onion" and "i2p" field, at least one needed' + ) -cmds = { - "login": login, - "logout": logout, - "add_post": add_post, - "remove_post": remove_post, - "add_service": add_service, - "remove_service": remove_service, -} + if not self._check_multilang_field(service["desc"]): + raise Exception( + 'Service structure field "desc" needs at least one supported language entry' + ) -def main(): - global URL - URL = getenv("API") - if URL == None or URL == "": - print("[-] API enviroment variable not set") - exit(1) + self.PUT("/v1/admin/service/add", service) - if len(argv) != 2: - print(f"[-] Usage: admin_script ") - print(f"[+] Run \"admin_script help\" to get all commands") - exit(1) + def del_service(self, service: str) -> None: + if service == "": + raise Exception("Service name cannot be empty") - if argv[1] == "help": - print("Avaliable commands:") - for k in cmds.keys(): - print(f" {k}") - exit() + self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service)) - for k in cmds.keys(): - if k != argv[1]: - continue - try: - cmds[k]() - except KeyboardInterrupt: - pass - exit() + def check_services(self) -> None: + self.GET("/v1/admin/service/check") + + def add_news(self, news: Dict[str, str]): + if not "id" in news or news["id"] == "": + raise Exception('News structure is missing required "id" field') + + if not "author" in news or news["author"] == "": + raise Exception('News structure is missing required "author" field') + + if not "title" in news: + raise Exception('News structure is missing required "title" field') + + if not "content" in news: + raise Exception('News structure is missing required "content" field') + + if not self._check_multilang_field(news["title"]): + raise Exception( + 'News structure field "title" needs at least one supported language entry' + ) + + if not self._check_multilang_field(news["content"]): + raise Exception( + 'News structure field "content" needs at least one supported language entry' + ) + + self.PUT("/v1/admin/news/add", news) + + def del_news(self, news: str) -> None: + if news == "": + raise Exception("News ID cannot be empty") + + self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(news)) + + def logs(self) -> List[Dict[str, Any]]: + return self.GET("/v1/admin/logs") + + +# local helper functions used by the script +def __format_time(ts: int) -> str: + return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y") + + +def __load_json_file(file: str) -> Dict[str, Any]: + with open(file, "r") as f: + data = loads(f.read()) + return data + + +def __dump_json_file(data: Dict[str, Any], file: str) -> None: + with open(file, "w") as f: + data = dumps(data, indent=2) + f.write(data) + + +# command handlers +def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None: + match cmd: + case "add_service": + data: Dict[str, str] = {} + data["desc"] = {} + + data["name"] = log.input("Serivce name") + for l in api.languages: + data["desc"][l] = log.input("Serivce desc (%s)" % l) + data["check_url"] = log.input("Serivce status check URL") + data["clear"] = log.input("Serivce clearnet URL") + data["onion"] = log.input("Serivce onion URL") + data["i2p"] = log.input("Serivce I2P URL") + + api.add_service(data) + log.info("Service has been added") + + case "del_service": + api.del_service(self.log.input("Serivce name")) + log.info("Service has been deleted") + + case "check_services": + api.check_services() + log.info("Requested status check for all the services") + + case "add_news": + news: Dict[str, str] = {} + news["title"] = {} + news["content"] = {} + + data["id"] = log.input("News ID") + for l in api.languages: + data["title"][l] = log.input("News title (%s)" % l) + data["author"] = log.input("News author") + for l in api.languages: + data["content"][l] = log.input("News content (%s)" % l) + + api.add_news(data) + log.info("News has been added") + + case "del_news": + api.del_news(log.input("News ID")) + log.info("News has been deleted") + + case "logs": + logs = api.logs() + + if None == logs["result"] or len(logs["result"]) == 0: + return log.info("No available logs") + + for l in logs["result"]: + log.info( + "Time: %s | Action: %s" % (__format_time(l["time"]), l["action"]) + ) + + +def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> None: + match cmd: + case "add_service": + data = __load_json_file(file) + api.add_service(data) + log.info("Service has been added") + + case "del_service": + data = __load_json_file(file) + api.del_service(data["name"]) + log.info("Service has been deleted") + + case "check_services": + api.check_services() + log.info("Requested status check for all the services") + + case "add_news": + data = __load_json_file(file) + api.add_news(data) + log.info("News has been added") + + case "del_news": + data = __load_json_file(file) + api.del_news(data["id"]) + log.info("News has been deleted") + + case "logs": + logs = api.logs() + + if None == logs["result"] or len(logs["result"]) == 0: + return log.info("No available logs") + + __dump_json_file(logs["result"], file) + log.info("Logs has been saved") - print("[-] Command not found") if __name__ == "__main__": - main() + log = Log() + + if len(argv) < 2 or len(argv) > 3: + log.error("Usage: %s [command] " % argv[0]) + log.info("Here is a list of available commands:") + print("\tadd_service") + print("\tdel_service") + print("\tcheck_services") + print("\tadd_news") + print("\tdel_news") + print("\tlogs") + exit(1) + + url = getenv(API_URL_ENV) + + if url == None: + log.error( + "Please specify the API URL using %s environment variable" % API_URL_ENV + ) + exit(1) + + try: + password = log.password("Please enter the admin password") + api = AdminAPI(url, password) + + if len(argv) == 2: + __handle_command(log, api, argv[1]) + elif len(argv) == 3: + __handle_command_with_file(log, api, argv[1], argv[2]) + + except KeyboardInterrupt: + print() + log.error("Command cancelled") + exit(1) + + except Exception as e: + log.error("Command failed: %s" % e) + exit(1) diff --git a/admin/install.sh b/admin/install.sh deleted file mode 100755 index 350cd87..0000000 --- a/admin/install.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -echo -n "Enter API URL: " -read url -cat > /usr/bin/admin_script << EOF -#!/bin/sh -API=$url python3 $(pwd)/admin.py \$1 -EOF -chmod +x /usr/bin/admin_script diff --git a/admin/tests/test_news.json b/admin/tests/test_news.json new file mode 100644 index 0000000..45ee94a --- /dev/null +++ b/admin/tests/test_news.json @@ -0,0 +1,12 @@ +{ + "id": "test_news", + "title": { + "en": "Very important news", + "tr": "Çok önemli haber" + }, + "author": "ngn", + "content": { + "en": "Just letting you know that I'm testing the API", + "tr": "Sadece API'ı test ettiğimi bilmenizi istedim" + } +} diff --git a/admin/tests/test_service.json b/admin/tests/test_service.json new file mode 100644 index 0000000..68b7cae --- /dev/null +++ b/admin/tests/test_service.json @@ -0,0 +1,9 @@ +{ + "name": "Test Service", + "desc": { + "en": "Service used for testing the API", + "tr": "API'ı test etmek için kullanılan servis" + }, + "check_url": "http://localhost:7001", + "clear": "http://localhost:7001" +} diff --git a/api/.gitignore b/api/.gitignore index 2d6f3a5..6f62924 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,2 +1,2 @@ -server +*.elf *.db diff --git a/api/Dockerfile b/api/Dockerfile index 0836046..bcb112a 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,17 +1,19 @@ FROM golang:1.23.4 -WORKDIR /app +WORKDIR /api COPY *.go ./ COPY *.mod ./ COPY *.sum ./ COPY Makefile ./ +COPY util ./util +COPY views ./views COPY routes ./routes COPY config ./config +COPY status ./status COPY database ./database -COPY util ./util EXPOSE 7001 RUN make -ENTRYPOINT ["/app/server"] +ENTRYPOINT ["/api/api.elf"] diff --git a/api/Makefile b/api/Makefile index f6908ec..5a9f3f2 100644 --- a/api/Makefile +++ b/api/Makefile @@ -1,10 +1,12 @@ -all: server +GOSRCS = $(wildcard *.go) $(wildcard */*.go) -server: *.go routes/*.go database/*.go util/*.go config/*.go - go build -o $@ . +all: api.elf -test: - API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server +api.elf: $(GOSRCS) + go build -o $@ + +run: + API_DEBUG=true API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./api.elf format: gofmt -s -w . diff --git a/api/config/config.go b/api/config/config.go index 29d5d60..52270fb 100644 --- a/api/config/config.go +++ b/api/config/config.go @@ -2,59 +2,112 @@ package config import ( "fmt" + "net/url" "os" - "strings" - - "github.com/ngn13/website/api/util" ) -type Option struct { - Name string - Value string - Required bool +type Type struct { + Options []Option + Count int } -func (o *Option) Env() string { - return strings.ToUpper(fmt.Sprintf("API_%s", o.Name)) -} - -var options []Option = []Option{ - {Name: "password", Value: "", Required: true}, - {Name: "frontend_url", Value: "http://localhost:5173/", Required: true}, -} - -func Load() bool { - var val string - - for i := range options { - if val = os.Getenv(options[i].Env()); val == "" { +func (c *Type) Find(name string, typ uint8) (*Option, error) { + for i := 0; i < c.Count; i++ { + if c.Options[i].Name != name { continue } - options[i].Value = val - options[i].Required = false - } - - for i := range options { - if options[i].Required && options[i].Value == "" { - util.Fail("please specify the required config option \"%s\" (\"%s\")", options[i].Name, options[i].Env()) - return false + if c.Options[i].Type != typ { + return nil, fmt.Errorf("bad option type") } - if options[i].Required && options[i].Value != "" { - util.Fail("using the default value \"%s\" for required config option \"%s\" (\"%s\")", options[i].Value, options[i].Name, options[i].Env()) - } + return &c.Options[i], nil } - return true + return nil, fmt.Errorf("option not found") } -func Get(name string) string { - for i := range options { - if options[i].Name != name { - continue - } - return options[i].Value +func (c *Type) Load() (err error) { + var ( + env_val string + env_name string + opt *Option + exists bool + ) + + // default options + c.Options = []Option{ + {Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes? + {Name: "index", Value: "true", Type: OPTION_TYPE_BOOL, Required: false}, // should display the index page (view/index.md)? + + {Name: "api_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // API URL for the website + {Name: "frontend_url", Value: "http://localhost:5173/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website + + {Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password + {Name: "host", Value: "0.0.0.0:7001", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on + {Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP + {Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval + {Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check + {Name: "limit", Value: "5s", Type: OPTION_TYPE_STR, Required: false}, // if the service responds slower than this limit, it will be marked as "slow" } - return "" + c.Count = len(c.Options) + + for i := 0; i < c.Count; i++ { + opt = &c.Options[i] + + env_name = opt.Env() + + if env_val, exists = os.LookupEnv(env_name); exists { + opt.Value = env_val + } + + if opt.Value == "" && opt.Required { + return fmt.Errorf("please specify a value for the config option \"%s\" (\"%s\")", opt.Name, env_name) + } + + if err = opt.Load(); err != nil { + return fmt.Errorf("failed to load option \"%s\" (\"%s\"): %s", opt.Name, env_name, err.Error()) + } + } + + return nil +} + +func (c *Type) GetStr(name string) string { + var ( + opt *Option + err error + ) + + if opt, err = c.Find(name, OPTION_TYPE_STR); err != nil { + return "" + } + + return opt.TypeValue.Str +} + +func (c *Type) GetBool(name string) bool { + var ( + opt *Option + err error + ) + + if opt, err = c.Find(name, OPTION_TYPE_BOOL); err != nil { + return false + } + + return opt.TypeValue.Bool +} + +func (c *Type) GetURL(name string) *url.URL { + var ( + opt *Option + err error + ) + + if opt, err = c.Find(name, OPTION_TYPE_URL); err != nil { + return nil + } + + return opt.TypeValue.URL } diff --git a/api/config/option.go b/api/config/option.go new file mode 100644 index 0000000..ae667bd --- /dev/null +++ b/api/config/option.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "net/url" + "strings" +) + +const ( + OPTION_TYPE_STR = 0 + OPTION_TYPE_BOOL = 1 + OPTION_TYPE_URL = 2 +) + +type Option struct { + Name string + Value string + Required bool + Type uint8 + TypeValue struct { + URL *url.URL + Str string + Bool bool + } +} + +func (o *Option) Env() string { + return strings.ToUpper(fmt.Sprintf("API_%s", o.Name)) +} + +func (o *Option) Load() (err error) { + err = nil + + switch o.Type { + case OPTION_TYPE_STR: + o.TypeValue.Str = o.Value + + case OPTION_TYPE_BOOL: + o.TypeValue.Bool = "1" == o.Value || "true" == strings.ToLower(o.Value) + + case OPTION_TYPE_URL: + o.TypeValue.URL, err = url.Parse(o.Value) + + default: + return fmt.Errorf("invalid option type") + } + + return err +} diff --git a/api/database/admin.go b/api/database/admin.go new file mode 100644 index 0000000..1da515f --- /dev/null +++ b/api/database/admin.go @@ -0,0 +1,62 @@ +package database + +import ( + "database/sql" + "fmt" + + "github.com/ngn13/website/api/util" +) + +type AdminLog struct { + Action string `json:"action"` // action that was performed (service removal, service addition etc.) + Time int64 `json:"time"` // time when the action was performed +} + +func (l *AdminLog) Scan(rows *sql.Rows) (err error) { + if rows != nil { + return rows.Scan(&l.Action, &l.Time) + } + + return fmt.Errorf("no row/rows specified") +} + +func (db *Type) AdminLogNext(l *AdminLog) bool { + var err error + + if nil == db.rows { + if db.rows, err = db.sql.Query("SELECT * FROM admin_log"); err != nil { + util.Fail("failed to query admin_log table: %s", err.Error()) + goto fail + } + } + + if !db.rows.Next() { + goto fail + } + + if err = l.Scan(db.rows); err != nil { + util.Fail("failed to scan the admin_log table: %s", err.Error()) + goto fail + } + + return true + +fail: + if db.rows != nil { + db.rows.Close() + } + db.rows = nil + + return false +} + +func (db *Type) AdminLogAdd(l *AdminLog) error { + _, err := db.sql.Exec( + `INSERT INTO admin_log( + action, time + ) values(?, ?)`, + &l.Action, &l.Time, + ) + + return err +} diff --git a/api/database/database.go b/api/database/database.go index 2adb27c..d9cb1de 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -1,44 +1,66 @@ package database import ( + "fmt" + "database/sql" + _ "github.com/mattn/go-sqlite3" ) -func Setup(db *sql.DB) error { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS posts( - id TEXT NOT NULL UNIQUE, - title TEXT NOT NULL, - author TEXT NOT NULL, - date TEXT NOT NULL, - content TEXT NOT NULL, - public INTEGER NOT NULL, - vote INTEGER NOT NULL - ); - `) - - if err != nil { - return err - } - - _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS services( - name TEXT NOT NULL UNIQUE, - desc TEXT NOT NULL, - url TEXT NOT NULL - ); - `) - - if err != nil { - return err - } - - _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS votes( - hash TEXT NOT NULL UNIQUE, - is_upvote INTEGER NOT NULL - ); - `) - - return err +type Type struct { + sql *sql.DB + rows *sql.Rows +} + +func (db *Type) Load() (err error) { + if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil { + return fmt.Errorf("cannot access the database: %s", err.Error()) + } + + // see database/service.go + _, err = db.sql.Exec(` + CREATE TABLE IF NOT EXISTS services( + name TEXT NOT NULL UNIQUE, + desc TEXT NOT NULL, + check_time INTEGER NOT NULL, + check_res INTEGER NOT NULL, + check_url TEXT NOT NULL, + clear TEXT, + onion TEXT, + i2p TEXT + ); + `) + + if err != nil { + return fmt.Errorf("failed to create the services table: %s", err.Error()) + } + + // see database/news.go + _, err = db.sql.Exec(` + CREATE TABLE IF NOT EXISTS news( + id TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + author TEXT NOT NULL, + time INTEGER NOT NULL, + content TEXT NOT NULL + ); + `) + + if err != nil { + return fmt.Errorf("failed to create the news table: %s", err.Error()) + } + + // see database/admin.go + _, err = db.sql.Exec(` + CREATE TABLE IF NOT EXISTS admin_log( + action TEXT NOT NULL, + time INTEGER NOT NULL + ); + `) + + if err != nil { + return fmt.Errorf("failed to create the admin_log table: %s", err.Error()) + } + + return nil } diff --git a/api/database/multilang.go b/api/database/multilang.go new file mode 100644 index 0000000..628157b --- /dev/null +++ b/api/database/multilang.go @@ -0,0 +1,58 @@ +package database + +import ( + "encoding/json" + "reflect" + "strings" + "unicode" +) + +type Multilang struct { + En string `json:"en"` // english + Tr string `json:"tr"` // turkish +} + +func (ml *Multilang) Supports(lang string) bool { + ml_ref := reflect.ValueOf(ml).Elem() + + for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ { + if name := reflect.Indirect(ml_ref).Field(i).Type().Name(); strings.ToLower(name) == lang { + return true + } + } + + return false +} + +func (ml *Multilang) Get(lang string) string { + r := []rune(lang) + r[0] = unicode.ToUpper(r[0]) + l := string(r) + + ml_ref := reflect.ValueOf(ml) + return reflect.Indirect(ml_ref).FieldByName(l).String() +} + +func (ml *Multilang) Empty() bool { + ml_ref := reflect.ValueOf(ml) + + for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ { + if field := reflect.Indirect(ml_ref).Field(i); field.String() != "" { + return false + } + } + + return true +} + +func (ml *Multilang) Dump() (string, error) { + if data, err := json.Marshal(ml); err != nil { + return "", err + } else { + return string(data), nil + } +} + +func (ml *Multilang) Load(s string) error { + return json.Unmarshal([]byte(s), ml) +} diff --git a/api/database/news.go b/api/database/news.go new file mode 100644 index 0000000..a66c359 --- /dev/null +++ b/api/database/news.go @@ -0,0 +1,116 @@ +package database + +import ( + "database/sql" + + "github.com/ngn13/website/api/util" +) + +type News struct { + ID string `json:"id"` // ID of the news + title string `json:"-"` // title of the news (string) + Title Multilang `json:"title"` // title of the news + Author string `json:"author"` // author of the news + Time uint64 `json:"time"` // when the new was published + content string `json:"-"` // content of the news (string) + Content Multilang `json:"content"` // content of the news +} + +func (n *News) Supports(lang string) bool { + return n.Content.Supports(lang) && n.Title.Supports(lang) +} + +func (n *News) Load() (err error) { + if err = n.Title.Load(n.title); err != nil { + return err + } + + if err = n.Content.Load(n.content); err != nil { + return err + } + + return nil +} + +func (n *News) Dump() (err error) { + if n.title, err = n.Title.Dump(); err != nil { + return err + } + + if n.content, err = n.Content.Dump(); err != nil { + return err + } + + return nil +} + +func (n *News) Scan(rows *sql.Rows) (err error) { + err = rows.Scan( + &n.ID, &n.title, &n.Author, + &n.Time, &n.content) + + if err != nil { + return err + } + + return n.Load() +} + +func (n *News) IsValid() bool { + return n.Author != "" && n.ID != "" && !n.Title.Empty() && !n.Content.Empty() +} + +func (db *Type) NewsNext(n *News) bool { + var err error + + if nil == db.rows { + if db.rows, err = db.sql.Query("SELECT * FROM news"); err != nil { + util.Fail("failed to query news table: %s", err.Error()) + goto fail + } + } + + if !db.rows.Next() { + goto fail + } + + if err = n.Scan(db.rows); err != nil { + util.Fail("failed to scan the news table: %s", err.Error()) + goto fail + } + + return true + +fail: + if db.rows != nil { + db.rows.Close() + } + db.rows = nil + + return false +} + +func (db *Type) NewsRemove(id string) error { + _, err := db.sql.Exec( + "DELETE FROM news WHERE id = ?", + id, + ) + + return err +} + +func (db *Type) NewsAdd(n *News) (err error) { + if err = n.Dump(); err != nil { + return err + } + + _, err = db.sql.Exec( + `INSERT OR REPLACE INTO news( + id, title, author, time, content + ) values(?, ?, ?, ?, ?)`, + n.ID, n.title, + n.Author, n.Time, n.content, + ) + + return err +} diff --git a/api/database/post.go b/api/database/post.go deleted file mode 100644 index 479277b..0000000 --- a/api/database/post.go +++ /dev/null @@ -1,71 +0,0 @@ -package database - -import ( - "database/sql" - - "github.com/ngn13/website/api/util" -) - -type Post struct { - ID string `json:"id"` - Title string `json:"title"` - Author string `json:"author"` - Date string `json:"date"` - Content string `json:"content"` - Public int `json:"public"` - Vote int `json:"vote"` -} - -func (p *Post) Load(rows *sql.Rows) error { - return rows.Scan(&p.ID, &p.Title, &p.Author, &p.Date, &p.Content, &p.Public, &p.Vote) -} - -func (p *Post) Get(db *sql.DB, id string) (bool, error) { - var ( - success bool - rows *sql.Rows - err error - ) - - if rows, err = db.Query("SELECT * FROM posts WHERE id = ?", id); err != nil { - return false, err - } - defer rows.Close() - - if success = rows.Next(); !success { - return false, nil - } - - if err = p.Load(rows); err != nil { - return false, err - } - - return true, nil -} - -func (p *Post) Remove(db *sql.DB) error { - _, err := db.Exec("DELETE FROM posts WHERE id = ?", p.ID) - return err -} - -func (p *Post) Save(db *sql.DB) error { - p.ID = util.TitleToID(p.Title) - - _, err := db.Exec( - "INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)", - p.ID, p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote, - ) - - return err -} - -func (p *Post) Update(db *sql.DB) error { - p.ID = util.TitleToID(p.Title) - - _, err := db.Exec( - "UPDATE posts SET title = ?, author = ?, date = ?, content = ?, public = ?, vote = ? WHERE id = ?", - p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote, p.ID, - ) - - return err -} diff --git a/api/database/service.go b/api/database/service.go index 3b14c71..34d3b8f 100644 --- a/api/database/service.go +++ b/api/database/service.go @@ -2,54 +2,127 @@ package database import ( "database/sql" + "fmt" + + "github.com/ngn13/website/api/util" ) type Service struct { - Name string `json:"name"` - Desc string `json:"desc"` - Url string `json:"url"` + Name string `json:"name"` // name of the service + desc string `json:"-"` // description of the service (string) + Desc Multilang `json:"desc"` // description of the service + CheckTime uint64 `json:"check_time"` // last status check time + CheckRes uint8 `json:"check_res"` // result of the status check + CheckURL string `json:"check_url"` // URL used for status check + Clear string `json:"clear"` // Clearnet (cringe) URL for the service + Onion string `json:"onion"` // Onion (TOR) URL for the service + I2P string `json:"i2p"` // I2P URL for the service } -func (s *Service) Load(rows *sql.Rows) error { - return rows.Scan(&s.Name, &s.Desc, &s.Url) +func (s *Service) Load() error { + return s.Desc.Load(s.desc) } -func (s *Service) Get(db *sql.DB, name string) (bool, error) { +func (s *Service) Dump() (err error) { + s.desc, err = s.Desc.Dump() + return +} + +func (s *Service) Scan(rows *sql.Rows, row *sql.Row) (err error) { + if rows != nil { + err = rows.Scan( + &s.Name, &s.desc, + &s.CheckTime, &s.CheckRes, &s.CheckURL, + &s.Clear, &s.Onion, &s.I2P) + } else if row != nil { + err = row.Scan( + &s.Name, &s.desc, + &s.CheckTime, &s.CheckRes, &s.CheckURL, + &s.Clear, &s.Onion, &s.I2P) + } else { + return fmt.Errorf("no row/rows specified") + } + + if err != nil { + return err + } + + return s.Load() +} + +func (s *Service) IsValid() bool { + return s.Name != "" && (s.Clear != "" || s.Onion != "" || s.I2P != "") && !s.Desc.Empty() +} + +func (db *Type) ServiceNext(s *Service) bool { + var err error + + if nil == db.rows { + if db.rows, err = db.sql.Query("SELECT * FROM services"); err != nil { + util.Fail("failed to query services table: %s", err.Error()) + goto fail + } + } + + if !db.rows.Next() { + goto fail + } + + if err = s.Scan(db.rows, nil); err != nil { + util.Fail("failed to scan the services table: %s", err.Error()) + goto fail + } + + return true + +fail: + if db.rows != nil { + db.rows.Close() + } + db.rows = nil + + return false +} + +func (db *Type) ServiceFind(name string) (*Service, error) { var ( - success bool - rows *sql.Rows - err error + row *sql.Row + s Service + err error ) - if rows, err = db.Query("SELECT * FROM services WHERE name = ?", name); err != nil { - return false, err - } - defer rows.Close() - - if success = rows.Next(); !success { - return false, nil + if row = db.sql.QueryRow("SELECT * FROM services WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows { + return nil, nil } - if err = s.Load(rows); err != nil { - return false, err + if err = s.Scan(nil, row); err != nil { + return nil, err } - return true, nil + return &s, nil } -func (s *Service) Remove(db *sql.DB) error { - _, err := db.Exec( +func (db *Type) ServiceRemove(name string) error { + _, err := db.sql.Exec( "DELETE FROM services WHERE name = ?", - s.Name, + name, ) return err } -func (s *Service) Save(db *sql.DB) error { - _, err := db.Exec( - "INSERT INTO services(name, desc, url) values(?, ?, ?)", - s.Name, s.Desc, s.Url, +func (db *Type) ServiceUpdate(s *Service) (err error) { + if err = s.Dump(); err != nil { + return err + } + + _, err = db.sql.Exec( + `INSERT OR REPLACE INTO services( + name, desc, check_time, check_res, check_url, clear, onion, i2p + ) values(?, ?, ?, ?, ?, ?, ?, ?)`, + s.Name, s.desc, + s.CheckTime, s.CheckRes, s.CheckURL, + s.Clear, s.Onion, s.I2P, ) return err diff --git a/api/database/vote.go b/api/database/vote.go deleted file mode 100644 index 109cc27..0000000 --- a/api/database/vote.go +++ /dev/null @@ -1,49 +0,0 @@ -package database - -import "database/sql" - -type Vote struct { - Hash string - IsUpvote bool -} - -func (v *Vote) Load(rows *sql.Rows) error { - return rows.Scan(&v.Hash, &v.IsUpvote) -} - -func (v *Vote) Get(db *sql.DB, hash string) (bool, error) { - var ( - success bool - rows *sql.Rows - err error - ) - - if rows, err = db.Query("SELECT * FROM votes WHERE hash = ?", hash); err != nil { - return false, err - } - defer rows.Close() - - if success = rows.Next(); !success { - return false, nil - } - - if err = v.Load(rows); err != nil { - return false, err - } - - return true, nil -} - -func (v *Vote) Update(db *sql.DB) error { - _, err := db.Exec("UPDATE votes SET is_upvote = ? WHERE hash = ?", v.IsUpvote, v.Hash) - return err -} - -func (v *Vote) Save(db *sql.DB) error { - _, err := db.Exec( - "INSERT INTO votes(hash, is_upvote) values(?, ?)", - v.Hash, v.IsUpvote, - ) - - return err -} diff --git a/api/go.mod b/api/go.mod index 74e6d81..b664cd7 100644 --- a/api/go.mod +++ b/api/go.mod @@ -2,11 +2,7 @@ module github.com/ngn13/website/api go 1.21.3 -require ( - github.com/gofiber/fiber/v2 v2.52.5 - github.com/gorilla/feeds v1.2.0 - github.com/mattn/go-sqlite3 v1.14.24 -) +require github.com/gofiber/fiber/v2 v2.52.5 require ( github.com/andybalholm/brotli v1.0.5 // indirect @@ -15,7 +11,9 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/api/go.sum b/api/go.sum index e42c05e..d1f72e1 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,14 +4,8 @@ github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yG github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= -github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -23,8 +17,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= diff --git a/api/main.go b/api/main.go index 4751ce6..bd92130 100644 --- a/api/main.go +++ b/api/main.go @@ -1,102 +1,124 @@ package main +/* + + * website/api | API server for my personal website + * written by ngn (https://ngn.tf) (2025) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + + */ + import ( - "database/sql" + "net/http" "github.com/gofiber/fiber/v2" "github.com/ngn13/website/api/config" "github.com/ngn13/website/api/database" "github.com/ngn13/website/api/routes" + "github.com/ngn13/website/api/status" "github.com/ngn13/website/api/util" ) -var db *sql.DB - func main() { var ( - app *fiber.App - //db *sql.DB + app *fiber.App + stat status.Type + + conf config.Type + db database.Type + err error ) - if !config.Load() { - util.Fail("failed to load the configuration") + if err = conf.Load(); err != nil { + util.Fail("failed to load the configuration: %s", err.Error()) return } - if db, err = sql.Open("sqlite3", "data.db"); err != nil { - util.Fail("cannot access the database: %s", err.Error()) + if !conf.GetBool("debug") { + util.Debg = func(m string, v ...any) {} + } + + if err = db.Load(); err != nil { + util.Fail("failed to load the database: %s", err.Error()) return } - defer db.Close() - if err = database.Setup(db); err != nil { - util.Fail("cannot setup the database: %s", err.Error()) + if err = stat.Setup(&conf, &db); err != nil { + util.Fail("failed to setup the status checker: %s", err.Error()) return } app = fiber.New(fiber.Config{ + AppName: "ngn's website", DisableStartupMessage: true, + ServerHeader: "", }) app.Use("*", func(c *fiber.Ctx) error { + // CORS stuff c.Set("Access-Control-Allow-Origin", "*") c.Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") - c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") + c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") // POST can be sent from HTML forms, so I prefer PUT for API endpoints + c.Locals("status", &stat) + c.Locals("config", &conf) c.Locals("database", &db) return c.Next() }) // index route - app.Get("/", func(c *fiber.Ctx) error { - return c.Send([]byte("o/")) - }) + app.Get("/", routes.GET_Index) - // blog routes - blog_routes := app.Group("/blog") + // version groups + v1 := app.Group("v1") - // blog feed routes - blog_routes.Get("/feed.*", routes.GET_Feed) + // v1 user routes + v1.Get("/services", routes.GET_Services) + v1.Get("/news/:lang", routes.GET_News) - // blog post routes - blog_routes.Get("/sum", routes.GET_PostSum) - blog_routes.Get("/get", routes.GET_Post) - - // blog post vote routes - blog_routes.Get("/vote/set", routes.GET_VoteSet) - blog_routes.Get("/vote/get", routes.GET_VoteGet) - - // service routes - service_routes := app.Group("services") - service_routes.Get("/all", routes.GET_Services) - - // admin routes - admin_routes := app.Group("admin") - admin_routes.Use("*", routes.AuthMiddleware) - - // admin auth routes - admin_routes.Get("/login", routes.GET_Login) - admin_routes.Get("/logout", routes.GET_Logout) - - // admin service managment routes - admin_routes.Put("/service/add", routes.PUT_AddService) - admin_routes.Delete("/service/remove", routes.DEL_RemoveService) - - // admin blog managment routes - admin_routes.Put("/blog/add", routes.PUT_AddPost) - admin_routes.Delete("/blog/remove", routes.DEL_RemovePost) + // v1 admin routes + v1.Use("/admin", routes.AuthMiddleware) + v1.Get("/admin/logs", routes.GET_AdminLogs) + v1.Get("/admin/service/check", routes.GET_CheckService) + v1.Put("/admin/service/add", routes.PUT_AddService) + v1.Delete("/admin/service/del", routes.DEL_DelService) + v1.Put("/admin/news/add", routes.PUT_AddNews) + v1.Delete("/admin/news/del", routes.DEL_DelNews) // 404 route app.All("*", func(c *fiber.Ctx) error { - return util.ErrNotFound(c) + return util.JSON(c, http.StatusNotFound, fiber.Map{ + "error": "Endpoint not found", + }) }) - util.Info("starting web server at port 7001") - - if err = app.Listen("0.0.0.0:7001"); err != nil { - util.Fail("error starting the webserver: %s", err.Error()) + // start the status checker + if err = stat.Run(); err != nil { + util.Fail("failed to start the status checker: %s", err.Error()) + return } + + // start the app + util.Info("starting web server on %s", conf.GetStr("host")) + + if err = app.Listen(conf.GetStr("host")); err != nil { + util.Fail("failed to start the web server: %s", err.Error()) + } + + stat.Stop() } diff --git a/api/routes/admin.go b/api/routes/admin.go index c0f0451..649e92c 100644 --- a/api/routes/admin.go +++ b/api/routes/admin.go @@ -1,183 +1,154 @@ package routes import ( - "database/sql" - "net/http" - "strings" + "fmt" "time" "github.com/gofiber/fiber/v2" - "github.com/mattn/go-sqlite3" "github.com/ngn13/website/api/config" "github.com/ngn13/website/api/database" + "github.com/ngn13/website/api/status" "github.com/ngn13/website/api/util" ) -var Token string = util.CreateToken() +func admin_log(c *fiber.Ctx, m string) error { + return c.Locals("database").(*database.Type).AdminLogAdd(&database.AdminLog{ + Action: m, // action that the admin peformed + Time: time.Now().Unix(), // current time + }) +} func AuthMiddleware(c *fiber.Ctx) error { - if c.Path() == "/admin/login" { - return c.Next() - } + conf := c.Locals("config").(*config.Type) - if c.Get("Authorization") != Token { + if c.Get("Authorization") != conf.GetStr("password") { return util.ErrAuth(c) } return c.Next() } -func GET_Login(c *fiber.Ctx) error { - if c.Query("pass") != config.Get("password") { - return util.ErrAuth(c) - } - - util.Info("new login from %s", util.GetIP(c)) - - return c.Status(http.StatusOK).JSON(fiber.Map{ - "error": "", - "token": Token, - }) -} - -func GET_Logout(c *fiber.Ctx) error { - Token = util.CreateToken() - - util.Info("logout from %s", util.GetIP(c)) - - return c.Status(http.StatusOK).JSON(fiber.Map{ - "error": "", - }) -} - -func DEL_RemoveService(c *fiber.Ctx) error { +func GET_AdminLogs(c *fiber.Ctx) error { var ( - db *sql.DB - service database.Service - name string - found bool - err error + list []database.AdminLog + log database.AdminLog ) - db = *(c.Locals("database").(**sql.DB)) - name = c.Query("name") + db := c.Locals("database").(*database.Type) - if name == "" { - util.ErrBadData(c) + for db.AdminLogNext(&log) { + list = append(list, log) } - if found, err = service.Get(db, name); err != nil { - util.Fail("error while searching for a service (\"%s\"): %s", name, err.Error()) - return util.ErrServer(c) + return util.JSON(c, 200, fiber.Map{ + "result": list, + }) +} + +func DEL_DelService(c *fiber.Ctx) error { + var ( + name string + err error + ) + + db := c.Locals("database").(*database.Type) + + if name = c.Query("name"); name == "" { + util.ErrBadReq(c) } - if !found { - return util.ErrEntryNotExists(c) + if err = admin_log(c, fmt.Sprintf("Removed service \"%s\"", name)); err != nil { + return util.ErrInternal(c, err) } - if err = service.Remove(db); err != nil { - util.Fail("error while removing a service (\"%s\"): %s", service.Name, err.Error()) - return util.ErrServer(c) + if err = db.ServiceRemove(name); err != nil { + return util.ErrInternal(c, err) } - return util.NoError(c) + return util.JSON(c, 200, nil) } func PUT_AddService(c *fiber.Ctx) error { var ( service database.Service - db *sql.DB - found bool err error ) - db = *(c.Locals("database").(**sql.DB)) + db := c.Locals("database").(*database.Type) if c.BodyParser(&service) != nil { return util.ErrBadJSON(c) } - if service.Name == "" || service.Desc == "" || service.Url == "" { - return util.ErrBadData(c) + if !service.IsValid() { + return util.ErrBadReq(c) } - if found, err = service.Get(db, service.Name); err != nil { - util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error()) - return util.ErrServer(c) + if err = admin_log(c, fmt.Sprintf("Added service \"%s\"", service.Name)); err != nil { + return util.ErrInternal(c, err) } - if found { - return util.ErrEntryExists(c) + if err = db.ServiceUpdate(&service); err != nil { + return util.ErrInternal(c, err) } - if err = service.Save(db); err != nil { - util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error()) - return util.ErrServer(c) - } + // force a status check so we can get the status of the new service + c.Locals("status").(*status.Type).Check() - return util.NoError(c) + return util.JSON(c, 200, nil) } -func DEL_RemovePost(c *fiber.Ctx) error { +func GET_CheckService(c *fiber.Ctx) error { + c.Locals("status").(*status.Type).Check() + return util.JSON(c, 200, nil) +} + +func DEL_DelNews(c *fiber.Ctx) error { var ( - db *sql.DB - id string - found bool - err error - post database.Post + id string + err error ) - db = *(c.Locals("database").(**sql.DB)) + db := c.Locals("database").(*database.Type) if id = c.Query("id"); id == "" { - return util.ErrBadData(c) + util.ErrBadReq(c) } - if found, err = post.Get(db, id); err != nil { - util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error()) - return util.ErrServer(c) + if err = admin_log(c, fmt.Sprintf("Removed news \"%s\"", id)); err != nil { + return util.ErrInternal(c, err) } - if !found { - return util.ErrEntryNotExists(c) + if err = db.NewsRemove(id); err != nil { + return util.ErrInternal(c, err) } - if err = post.Remove(db); err != nil { - util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error()) - return util.ErrServer(c) - } - - return util.NoError(c) + return util.JSON(c, 200, nil) } -func PUT_AddPost(c *fiber.Ctx) error { +func PUT_AddNews(c *fiber.Ctx) error { var ( - db *sql.DB - post database.Post + news database.News err error ) - db = *(c.Locals("database").(**sql.DB)) - post.Public = 1 + db := c.Locals("database").(*database.Type) - if c.BodyParser(&post) != nil { + if c.BodyParser(&news) != nil { return util.ErrBadJSON(c) } - if post.Title == "" || post.Author == "" || post.Content == "" { - return util.ErrBadData(c) + if !news.IsValid() { + return util.ErrBadReq(c) } - post.Date = time.Now().Format("02/01/06") - - if err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) { - return util.ErrEntryExists(c) + if err = admin_log(c, fmt.Sprintf("Added news \"%s\"", news.ID)); err != nil { + return util.ErrInternal(c, err) } - if err != nil { - util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error()) - return util.ErrServer(c) + if err = db.NewsAdd(&news); err != nil { + return util.ErrInternal(c, err) } - return util.NoError(c) + return util.JSON(c, 200, nil) } diff --git a/api/routes/blog.go b/api/routes/blog.go deleted file mode 100644 index c4e988f..0000000 --- a/api/routes/blog.go +++ /dev/null @@ -1,203 +0,0 @@ -package routes - -import ( - "database/sql" - "fmt" - "net/url" - "path" - "strings" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gorilla/feeds" - "github.com/ngn13/website/api/config" - "github.com/ngn13/website/api/database" - "github.com/ngn13/website/api/util" -) - -func GET_Post(c *fiber.Ctx) error { - var ( - post database.Post - id string - db *sql.DB - found bool - err error - ) - - db = *(c.Locals("database").(**sql.DB)) - - if id = c.Query("id"); id == "" { - return util.ErrBadData(c) - } - - if found, err = post.Get(db, id); err != nil { - util.Fail("error while search for a post (\"%s\"): %s", id, err.Error()) - return util.ErrServer(c) - } - - if !found { - return util.ErrEntryNotExists(c) - } - - return c.JSON(fiber.Map{ - "error": "", - "result": post, - }) -} - -func GET_PostSum(c *fiber.Ctx) error { - var ( - posts []database.Post - rows *sql.Rows - db *sql.DB - err error - ) - - db = *(c.Locals("database").(**sql.DB)) - - if rows, err = db.Query("SELECT * FROM posts"); err != nil { - util.Fail("cannot load posts: %s", err.Error()) - return util.ErrServer(c) - } - defer rows.Close() - - for rows.Next() { - var post database.Post - - if err = post.Load(rows); err != nil { - util.Fail("error while loading post: %s", err.Error()) - return util.ErrServer(c) - } - - if post.Public == 0 { - continue - } - - if len(post.Content) > 255 { - post.Content = post.Content[0:250] - } - - posts = append(posts, post) - } - - return c.JSON(fiber.Map{ - "error": "", - "result": posts, - }) -} - -func getFeed(db *sql.DB) (*feeds.Feed, error) { - var ( - posts []database.Post - err error - ) - - rows, err := db.Query("SELECT * FROM posts") - if err != nil { - return nil, err - } - - for rows.Next() { - var post database.Post - - if err = post.Load(rows); err != nil { - return nil, err - } - - if post.Public == 0 { - continue - } - - posts = append(posts, post) - } - rows.Close() - - blogurl, err := url.JoinPath( - config.Get("frontend_url"), "/blog", - ) - - if err != nil { - return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error()) - } - - feed := &feeds.Feed{ - Title: "[ngn.tf] | blog", - Link: &feeds.Link{Href: blogurl}, - Description: "ngn's personal blog", - Author: &feeds.Author{Name: "ngn", Email: "ngn@ngn.tf"}, - Created: time.Now(), - } - - feed.Items = []*feeds.Item{} - for _, p := range posts { - purl, err := url.JoinPath(blogurl, p.ID) - if err != nil { - return nil, fmt.Errorf("failed to create URL for '%s': %s\n", p.ID, err.Error()) - } - - parsed, err := time.Parse("02/01/06", p.Date) - if err != nil { - return nil, fmt.Errorf("failed to parse time for '%s': %s\n", p.ID, err.Error()) - } - - feed.Items = append(feed.Items, &feeds.Item{ - Id: p.ID, - Title: p.Title, - Link: &feeds.Link{Href: purl}, - Author: &feeds.Author{Name: p.Author}, - Created: parsed, - }) - } - - return feed, nil -} - -func GET_Feed(c *fiber.Ctx) error { - var ( - db *sql.DB - err error - feed *feeds.Feed - name []string - res string - ext string - ) - - db = *(c.Locals("database").(**sql.DB)) - - if name = strings.Split(path.Base(c.Path()), "."); len(name) != 2 { - return util.ErrNotFound(c) - } - ext = name[1] - - if feed, err = getFeed(db); err != nil { - util.Fail("cannot obtain the feed: %s", err.Error()) - return util.ErrServer(c) - } - - switch ext { - case "atom": - res, err = feed.ToAtom() - c.Set("Content-Type", "application/atom+xml") - break - - case "json": - res, err = feed.ToJSON() - c.Set("Content-Type", "application/feed+json") - break - - case "rss": - res, err = feed.ToRss() - c.Set("Content-Type", "application/rss+xml") - break - - default: - return util.ErrNotFound(c) - } - - if err != nil { - util.Fail("cannot obtain the feed as the specified format: %s", err.Error()) - return util.ErrServer(c) - } - - return c.Send([]byte(res)) -} diff --git a/api/routes/index.go b/api/routes/index.go new file mode 100644 index 0000000..970b2f2 --- /dev/null +++ b/api/routes/index.go @@ -0,0 +1,32 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/config" + "github.com/ngn13/website/api/util" +) + +func GET_Index(c *fiber.Ctx) error { + var ( + md []byte + err error + ) + + conf := c.Locals("config").(*config.Type) + + if !conf.GetBool("index") { + return util.ErrNotFound(c) + } + + frontend := conf.GetURL("frontend_url") + api := conf.GetURL("api_url") + + if md, err = util.Render("views/index.md", fiber.Map{ + "frontend": frontend, + "api": api, + }); err != nil { + return util.ErrInternal(c, err) + } + + return util.Markdown(c, md) +} diff --git a/api/routes/news.go b/api/routes/news.go new file mode 100644 index 0000000..39d5a79 --- /dev/null +++ b/api/routes/news.go @@ -0,0 +1,47 @@ +package routes + +import ( + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/config" + "github.com/ngn13/website/api/database" + "github.com/ngn13/website/api/util" +) + +func GET_News(c *fiber.Ctx) error { + var ( + news []database.News + n database.News + feed []byte + err error + ) + + db := c.Locals("database").(*database.Type) + conf := c.Locals("config").(*config.Type) + frontend := conf.GetURL("frontend_url") + lang := c.Params("lang") + + if lang == "" || len(lang) != 2 { + return util.ErrBadReq(c) + } + + lang = strings.ToLower(lang) + + for db.NewsNext(&n) { + if n.Supports(lang) { + news = append(news, n) + } + } + + if feed, err = util.Render("views/news.xml", fiber.Map{ + "frontend": frontend, + "lang": lang, + "news": news, + }); err != nil { + return util.ErrInternal(c, err) + } + + c.Set("Content-Type", "application/atom+xml; charset=utf-8") + return c.Send(feed) +} diff --git a/api/routes/services.go b/api/routes/services.go index 0ded696..c84efa1 100644 --- a/api/routes/services.go +++ b/api/routes/services.go @@ -1,8 +1,6 @@ package routes import ( - "database/sql" - "github.com/gofiber/fiber/v2" "github.com/ngn13/website/api/database" "github.com/ngn13/website/api/util" @@ -11,32 +9,29 @@ import ( func GET_Services(c *fiber.Ctx) error { var ( services []database.Service - rows *sql.Rows - db *sql.DB - err error + service database.Service ) - db = *(c.Locals("database").(**sql.DB)) + db := c.Locals("database").(*database.Type) + name := c.Query("name") - if rows, err = db.Query("SELECT * FROM services"); err != nil { - util.Fail("cannot load services: %s", err.Error()) - return util.ErrServer(c) - } - defer rows.Close() - - for rows.Next() { - var service database.Service - - if err = service.Load(rows); err != nil { - util.Fail("error while loading service: %s", err.Error()) - return util.ErrServer(c) + if name != "" { + if s, err := db.ServiceFind(name); err != nil { + return util.ErrInternal(c, err) + } else if s != nil { + return util.JSON(c, 200, fiber.Map{ + "result": s, + }) } + return util.ErrNotExist(c) + } + + for db.ServiceNext(&service) { services = append(services, service) } - return c.JSON(fiber.Map{ - "error": "", + return util.JSON(c, 200, fiber.Map{ "result": services, }) } diff --git a/api/routes/vote.go b/api/routes/vote.go deleted file mode 100644 index be718a0..0000000 --- a/api/routes/vote.go +++ /dev/null @@ -1,139 +0,0 @@ -package routes - -import ( - "database/sql" - - "github.com/gofiber/fiber/v2" - "github.com/ngn13/website/api/database" - "github.com/ngn13/website/api/util" -) - -func getVoteHash(id string, ip string) string { - return util.GetSHA512(id + "_" + ip) -} - -func GET_VoteGet(c *fiber.Ctx) error { - var ( - db *sql.DB - id string - hash string - vote database.Vote - found bool - err error - ) - - db = *(c.Locals("database").(**sql.DB)) - - if id = c.Query("id"); id == "" { - return util.ErrBadData(c) - } - - hash = getVoteHash(id, util.GetIP(c)) - - if found, err = vote.Get(db, hash); err != nil { - util.Fail("error while searchig for a vote (\"%s\"): %s", hash, err.Error()) - return util.ErrServer(c) - } - - if !found { - return util.ErrEntryNotExists(c) - } - - if vote.IsUpvote { - return c.JSON(fiber.Map{ - "error": "", - "result": "upvote", - }) - } - - return c.JSON(fiber.Map{ - "error": "", - "result": "downvote", - }) -} - -func GET_VoteSet(c *fiber.Ctx) error { - var ( - db *sql.DB - id string - is_upvote bool - hash string - vote database.Vote - post database.Post - found bool - err error - ) - - db = *(c.Locals("database").(**sql.DB)) - id = c.Query("id") - - if c.Query("to") == "" || id == "" { - return util.ErrBadData(c) - } - - if found, err = post.Get(db, id); err != nil { - util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error()) - return util.ErrServer(c) - } - - if !found { - return util.ErrEntryNotExists(c) - } - - is_upvote = c.Query("to") == "upvote" - hash = getVoteHash(id, util.GetIP(c)) - - if found, err = vote.Get(db, hash); err != nil { - util.Fail("error while searching for a vote (\"%s\"): %s", hash, err.Error()) - return util.ErrServer(c) - } - - if found { - if vote.IsUpvote == is_upvote { - return util.ErrEntryExists(c) - } - - if vote.IsUpvote && !is_upvote { - post.Vote -= 2 - } - - if !vote.IsUpvote && is_upvote { - post.Vote += 2 - } - - vote.IsUpvote = is_upvote - - if err = post.Update(db); err != nil { - util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error()) - return util.ErrServer(c) - } - - if err = vote.Update(db); err != nil { - util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error()) - return util.ErrServer(c) - } - - return util.NoError(c) - } - - vote.Hash = hash - vote.IsUpvote = is_upvote - - if is_upvote { - post.Vote++ - } else { - post.Vote-- - } - - if err = post.Update(db); err != nil { - util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error()) - return util.ErrServer(c) - } - - if err = vote.Save(db); err != nil { - util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error()) - return util.ErrServer(c) - } - - return util.NoError(c) -} diff --git a/api/status/service.go b/api/status/service.go new file mode 100644 index 0000000..26d25d0 --- /dev/null +++ b/api/status/service.go @@ -0,0 +1,105 @@ +package status + +import ( + "net/http" + "net/http/httptrace" + "net/url" + "time" + + "github.com/ngn13/website/api/database" + "github.com/ngn13/website/api/util" +) + +const ( + STATUS_RES_DOWN = 0 // service is down + STATUS_RES_OK = 1 // service is up + STATUS_RES_SLOW = 2 // service is up, but slow + STATUS_RES_NONE = 3 // service doesn't support status checking/status checking is disabled +) + +func (s *Type) check_http_service(service *database.Service) (r uint8, err error) { + var ( + req *http.Request + res *http.Response + + start time.Time + elapsed time.Duration + ) + + r = STATUS_RES_NONE + + if req, err = http.NewRequest("GET", service.CheckURL, nil); err != nil { + return + } + + trace := &httptrace.ClientTrace{ + GetConn: func(_ string) { start = time.Now() }, + GotFirstResponseByte: func() { elapsed = time.Since(start) }, + } + + http.DefaultClient.Timeout = s.timeout + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + res, err = http.DefaultClient.Do(req) + + if res != nil { + defer res.Body.Close() + } + + if err != nil { + util.Debg("marking service \"%s\" as down (%s)", service.Name, err.Error()) + err = nil + r = STATUS_RES_DOWN + } else if res.StatusCode != 200 { + util.Debg("marking service \"%s\" as down (status code %d)", service.Name, res.StatusCode) + r = STATUS_RES_DOWN + } else if elapsed.Microseconds() > s.limit.Microseconds() { + r = STATUS_RES_SLOW + } else { + r = STATUS_RES_OK + } + + return +} + +func (s *Type) check_service(service *database.Service) error { + var ( + res uint8 + url *url.URL + err error + ) + + if s.disabled || service.CheckURL == "" { + err = nil + goto fail + } + + if url, err = url.Parse(service.CheckURL); err != nil { + return err + } + + switch url.Scheme { + case "https": + if res, err = s.check_http_service(service); err != nil { + goto fail + } + + case "http": + if res, err = s.check_http_service(service); err != nil { + goto fail + } + + default: + // unsupported protocol + err = nil + goto fail + } + + service.CheckTime = uint64(time.Now().Unix()) + service.CheckRes = res + return nil + +fail: + service.CheckTime = 0 + service.CheckRes = STATUS_RES_NONE + return err +} diff --git a/api/status/status.go b/api/status/status.go new file mode 100644 index 0000000..8c2a74f --- /dev/null +++ b/api/status/status.go @@ -0,0 +1,139 @@ +package status + +import ( + "fmt" + "time" + + "github.com/ngn13/website/api/config" + "github.com/ngn13/website/api/database" + "github.com/ngn13/website/api/util" +) + +type Type struct { + conf *config.Type + db *database.Type + + ticker *time.Ticker + updateChan chan int + closeChan chan int + + disabled bool + timeout time.Duration + limit time.Duration +} + +func (s *Type) check() { + var ( + services []database.Service + service database.Service + err error + ) + + for s.db.ServiceNext(&service) { + services = append(services, service) + } + + for i := range services { + if err = s.check_service(&services[i]); err != nil { + util.Fail("failed to check the service status for \"%s\": %s", services[i].Name, err.Error()) + } + + if err = s.db.ServiceUpdate(&services[i]); err != nil { + util.Fail("failed to update service status for \"%s\": %s", services[i].Name, err.Error()) + } + } +} + +func (s *Type) loop() { + s.check() + + for { + select { + case <-s.closeChan: + close(s.updateChan) + s.ticker.Stop() + s.closeChan <- 0 + return + + case <-s.updateChan: + s.check() + + case <-s.ticker.C: + s.check() + } + } +} + +func (s *Type) Setup(conf *config.Type, db *database.Type) error { + var ( + dur time.Duration + iv, to, lm string + err error + ) + + iv = conf.GetStr("interval") + to = conf.GetStr("timeout") + lm = conf.GetStr("limit") + + if iv == "" || to == "" || lm == "" { + s.disabled = true + return nil + } + + if dur, err = util.GetDuration(iv); err != nil { + return err + } + + if s.timeout, err = util.GetDuration(iv); err != nil { + return err + } + + if s.limit, err = util.GetDuration(iv); err != nil { + return err + } + + s.conf = conf + s.db = db + + s.ticker = time.NewTicker(dur) + s.updateChan = make(chan int) + s.closeChan = make(chan int) + + s.disabled = false + + return nil +} + +func (s *Type) Run() error { + if s.ticker == nil || s.updateChan == nil || s.closeChan == nil { + return fmt.Errorf("you either didn't call Setup() or you called it and it failed") + } + + if s.disabled { + go s.check() + return nil + } + + go s.loop() + return nil +} + +func (s *Type) Check() { + if !s.disabled { + s.updateChan <- 0 + } +} + +func (s *Type) Stop() { + // tell loop() to stop + s.closeChan <- 0 + + // wait till loop() stops + for { + select { + case <-s.closeChan: + close(s.closeChan) + return + } + } +} diff --git a/api/util/log.go b/api/util/log.go index 3d540c7..d84645a 100644 --- a/api/util/log.go +++ b/api/util/log.go @@ -5,8 +5,17 @@ import ( "os" ) -var ( - Info = log.New(os.Stdout, "\033[34m[info]\033[0m ", log.Ltime|log.Lshortfile).Printf - Warn = log.New(os.Stderr, "\033[33m[warn]\033[0m ", log.Ltime|log.Lshortfile).Printf - Fail = log.New(os.Stderr, "\033[31m[fail]\033[0m ", log.Ltime|log.Lshortfile).Printf +const ( + COLOR_BLUE = "\033[34m" + COLOR_YELLOW = "\033[33m" + COLOR_RED = "\033[31m" + COLOR_CYAN = "\033[36m" + COLOR_RESET = "\033[0m" +) + +var ( + Debg = log.New(os.Stdout, COLOR_CYAN+"[debg]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf + Info = log.New(os.Stdout, COLOR_BLUE+"[info]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf + Warn = log.New(os.Stderr, COLOR_YELLOW+"[warn]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf + Fail = log.New(os.Stderr, COLOR_RED+"[fail]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf ) diff --git a/api/util/res.go b/api/util/res.go new file mode 100644 index 0000000..2166cf0 --- /dev/null +++ b/api/util/res.go @@ -0,0 +1,89 @@ +package util + +import ( + "net/http" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/config" + "github.com/russross/blackfriday/v2" +) + +func IP(c *fiber.Ctx) string { + conf := c.Locals("config").(*config.Type) + ip_header := conf.GetStr("ip_header") + + if ip_header != "" && c.Get(ip_header) != "" { + return strings.Clone(c.Get(ip_header)) + } + + return c.IP() +} + +func Markdown(c *fiber.Ctx, raw []byte) error { + exts := blackfriday.FencedCode + exts |= blackfriday.NoEmptyLineBeforeBlock + exts |= blackfriday.HardLineBreak + + c.Set("Content-Type", "text/html; charset=utf-8") + return c.Send(blackfriday.Run(raw, blackfriday.WithExtensions(exts))) +} + +func JSON(c *fiber.Ctx, code int, data fiber.Map) error { + if data == nil { + data = fiber.Map{} + data["error"] = "" + } else if _, ok := data["error"]; !ok { + data["error"] = "" + } + + if data["error"] == 200 { + Warn("200 response with an error at %s", c.Path()) + } + + return c.Status(code).JSON(data) +} + +func ErrInternal(c *fiber.Ctx, err error) error { + Warn("Internal server error at %s: %s", c.Path(), err.Error()) + + return JSON(c, http.StatusInternalServerError, fiber.Map{ + "error": "Server error", + }) +} + +func ErrExists(c *fiber.Ctx) error { + return JSON(c, http.StatusConflict, fiber.Map{ + "error": "Entry already exists", + }) +} + +func ErrNotExist(c *fiber.Ctx) error { + return JSON(c, http.StatusNotFound, fiber.Map{ + "error": "Entry does not exist", + }) +} + +func ErrBadReq(c *fiber.Ctx) error { + return JSON(c, http.StatusBadRequest, fiber.Map{ + "error": "Provided data is invalid", + }) +} + +func ErrNotFound(c *fiber.Ctx) error { + return JSON(c, http.StatusNotFound, fiber.Map{ + "error": "Endpoint not found", + }) +} + +func ErrBadJSON(c *fiber.Ctx) error { + return JSON(c, http.StatusBadRequest, fiber.Map{ + "error": "Invalid JSON data", + }) +} + +func ErrAuth(c *fiber.Ctx) error { + return JSON(c, http.StatusUnauthorized, fiber.Map{ + "error": "Authentication failed", + }) +} diff --git a/api/util/util.go b/api/util/util.go new file mode 100644 index 0000000..90602fb --- /dev/null +++ b/api/util/util.go @@ -0,0 +1,67 @@ +package util + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "fmt" + "os" + "strconv" + "strings" + "text/template" + "time" +) + +func Render(file string, data interface{}) ([]byte, error) { + var ( + rendered *bytes.Buffer + tmpl *template.Template + content []byte + err error + ) + + if content, err = os.ReadFile(file); err != nil { + return nil, err + } + + if tmpl, err = template.New("template").Parse(string(content)); err != nil { + return nil, err + } + + rendered = bytes.NewBuffer(nil) + err = tmpl.Execute(rendered, data) + + return rendered.Bytes(), err +} + +func GetDuration(d string) (time.Duration, error) { + var ( + d_num uint64 + err error + ) + + d_num_end := d[len(d)-1] + d_num_str := strings.TrimSuffix(d, string(d_num_end)) + + if d_num, err = strconv.ParseUint(d_num_str, 10, 64); err != nil { + return 0, err + } + + switch d_num_end { + case 's': + return time.Duration(d_num) * (time.Second), nil + + case 'm': + return time.Duration(d_num) * (time.Second * 60), nil + + case 'h': + return time.Duration(d_num) * ((time.Second * 60) * 60), nil + } + + return 0, fmt.Errorf("invalid time duration format") +} + +func GetSHA1(s string) string { + hasher := sha1.New() + return hex.EncodeToString(hasher.Sum([]byte(s))) +} diff --git a/api/util/utils.go b/api/util/utils.go deleted file mode 100644 index 61e1376..0000000 --- a/api/util/utils.go +++ /dev/null @@ -1,74 +0,0 @@ -package util - -import ( - "crypto/sha512" - "fmt" - "math/rand" - "net/http" - "strings" - - "github.com/gofiber/fiber/v2" -) - -func GetSHA512(s string) string { - hasher := sha512.New() - return fmt.Sprintf("%x", hasher.Sum([]byte(s))) -} - -func TitleToID(name string) string { - return strings.ToLower(strings.ReplaceAll(name, " ", "")) -} - -func CreateToken() string { - s := make([]byte, 32) - for i := 0; i < 32; i++ { - s[i] = byte(65 + rand.Intn(25)) - } - return string(s) -} - -func ErrorJSON(error string) fiber.Map { - return fiber.Map{ - "error": error, - } -} - -func GetIP(c *fiber.Ctx) string { - if c.Get("X-Real-IP") != "" { - return strings.Clone(c.Get("X-Real-IP")) - } - - return c.IP() -} - -func ErrServer(c *fiber.Ctx) error { - return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error")) -} - -func ErrEntryExists(c *fiber.Ctx) error { - return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists")) -} - -func ErrEntryNotExists(c *fiber.Ctx) error { - return c.Status(http.StatusNotFound).JSON(ErrorJSON("Entry does not exist")) -} - -func ErrBadData(c *fiber.Ctx) error { - return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid")) -} - -func ErrBadJSON(c *fiber.Ctx) error { - return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Bad JSON data")) -} - -func ErrAuth(c *fiber.Ctx) error { - return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed")) -} - -func ErrNotFound(c *fiber.Ctx) error { - return c.Status(http.StatusNotFound).JSON(ErrorJSON("Requested endpoint not found")) -} - -func NoError(c *fiber.Ctx) error { - return c.Status(http.StatusOK).JSON(ErrorJSON("")) -} diff --git a/api/views/index.md b/api/views/index.md new file mode 100644 index 0000000..2a97bbd --- /dev/null +++ b/api/views/index.md @@ -0,0 +1,161 @@ + + + + +# [{{.api.Host}}]({{.api.String}}) +This is the API for my personal website, [{{.frontend.Host}}]({{.frontend.String}}). + +It stores information about the self-hosted services I provide and it also allows me +to publish news and updates about these services using an Atom feed. It's written in +Go and uses SQLite for storage. Licensed under GNU GPL version 3. + +**Source code and the license is available at**: [https://github.com/ngn13/website](https://github.com/ngn13/website) +**You can report issues to**: [https://github.com/ngn13/website/issues](https://github.com/ngn13/website/issues) + +The rest of this document contains documentation for all the available API endpoints. + +## Version 1 Endpoints +Each version 1 endpoint, can be accessed using the /v1 route. + +All the endpoints return JSON formatted data. + +### Errors +If any error occurs, you will get a non-200 response. And the JSON data will have an +"error" key, which will contain information about the error that occured, in the +string format. This is the only JSON key that will be set in non-200 responses. + +### Results +If no error occurs, "error" key will be set to an emtpy string (""). If the endpoint +returns any data, this will be stored using the "result" key. The "result" have a +different expected type and a format for each endpoint. + +### Multilang +Some "result" formats may use a structure called "Multilang". This is a simple JSON +structure that includes one key for each supported language. The key is named after +the language it represents. Currently only supported languages are: +- English (`en`) +- Turkish (`tr`) + +So each multilang structure, will have **at least** one of these keys. + +Here is an example multilang structure: +``` +{ + "en": "Hello, world!", + "tr": "Merhaba, dünya!" +} +``` +If a "result" field is using a multilang structure, it will be specified as "Multilang" +in the rest of the documentation. + +### Administrator routes +The endpoints under the "/v1/admin" route, are administrator-only routes. To access +these routes you'll need to specfiy and password using the "Authorization" header. +If the password you specify, matches with the password specified using the +`API_PASSWORD` environment variable, you will be able to access the route. + +### GET /v1/services +Returns a list of available services. Each service has the following JSON format: +``` +{ + "name": "Test Service", + "desc": { + "en": "Service used for testing the API", + "tr": "API'ı test etmek için kullanılan servis" + }, + "check_time": 1735861944, + "check_res": 1, + "check_url": "http://localhost:7001", + "clear": "http://localhost:7001", + "onion": "", + "i2p": "" +} +``` +Where: +- `name`: Service name (string) +- `desc`: Service description (Multilang) +- `check_time`: Last time status check time for the service, set 0 if status checking is +not supported for this service/status checking is disabled (integer, UNIX timestamp) +- `check_res`: Last service status check result (integer) + * 0 if the service is down + * 1 if the service is up + * 2 if the service is up, but slow + * 3 if the service doesn't support status checking/status checking is disabled +- `check_url`: URL used for service's status check (string, empty if none) +- `clear`: Clearnet URL for the service (string, empty string if none) +- `onion`: Onion (TOR) URL for the service (string, empty string if none) +- `i2p`: I2P URL for the service (string, empty string if none) + +You can also get information about a specific service by specifying it's name using +a URL query named "name". + +### GET /v1/news/:language +Returns a Atom feed of news for the given language. Supports languages that are supported +by Multilang. + +### GET /v1/admin/logs +Returns a list of administrator logs. Each log has the following JSON format: +``` +{ + "action": "Added service \"Test Service\"", + "time": 1735861794 +} +``` +Where: +- `action`: Action that the administrator performed (string) +- `time`: Time when the administrator action was performed (integer, UNIX timestamp) + +Client can get the logs for only a single address, by setting the URL query "addr". + +### PUT /v1/admin/service/add +Creates a new service. The request body needs to contain JSON data, and it needs to +have the JSON format used to represent a service. See "/v1/services/all" route to +see this format. + +Returns no data on success. + +### DELETE /v1/admin/service/del +Deletes a service. The client needs to specify the name of the service to delete, by +setting the URL query "name". + +Returns no data on success. + +### GET /v1/admin/service/check +Forces a status check for all the services. + +Returns no data on success. + +### PUT /v1/admin/news/add +Creates a news post. The request body needs to contain JSON data, and it needs +to use the following JSON format: +``` +{ + "id": "test_news", + "title": { + "en": "Very important news", + "tr": "Çok önemli haber" + }, + "author": "ngn", + "content": { + "en": "Just letting you know that I'm testing the API", + "tr": "Sadece API'ı test ettiğimi bilmenizi istedim" + } +} +``` +Where: +- `id`: Unique ID for the news post (string) +- `title`: Title for the news post (Multilang) +- `author`: Author of the news post (string) +- `content`: Contents of the news post (Multilang) + +Returns no data on success. + +### DELETE /v1/admin/news/del +Deletes a news post. The client needs to specify the ID of the news post to delete, +by setting the URL query "id". + +Returns no data on success. diff --git a/api/views/news.xml b/api/views/news.xml new file mode 100644 index 0000000..b78643f --- /dev/null +++ b/api/views/news.xml @@ -0,0 +1,6 @@ + + {{.frontend.Host}} news + 2025-01-02T20:46:24Z + News and updates about my self-hosted services and projects + + From 337e56de78500fbb07252a1a12a1e634fe9cca30 Mon Sep 17 00:00:00 2001 From: ngn Date: Sat, 4 Jan 2025 19:59:44 +0300 Subject: [PATCH 02/12] finish up the atom news feed API --- admin/admin.py | 59 +- api/database/multilang.go | 2 +- api/database/news.go | 2 +- api/go.mod | 8 +- api/routes/admin.go | 2 + api/routes/news.go | 45 +- api/views/news.xml | 16 +- app/.prettierrc | 9 + app/package-lock.json | 3157 +++++++++++----------- app/package.json | 48 +- app/src/app.html | 20 +- app/src/lib/card.svelte | 77 +- app/src/lib/card_link.svelte | 99 +- app/src/lib/header.svelte | 53 +- app/src/lib/navbar.svelte | 60 +- app/src/lib/navbar_link.svelte | 61 +- app/src/lib/service.svelte | 106 +- app/src/lib/util.js | 16 + app/src/routes/+error.svelte | 10 +- app/src/routes/+layout.svelte | 2 +- app/src/routes/+page.svelte | 213 +- app/src/routes/blog/+page.js | 10 +- app/src/routes/blog/+page.svelte | 155 +- app/src/routes/blog/[id]/+page.server.js | 16 +- app/src/routes/blog/[id]/+page.svelte | 252 +- app/src/routes/donate/+page.svelte | 84 +- app/src/routes/services/+page.js | 46 +- app/src/routes/services/+page.svelte | 127 +- app/static/animations.css | 79 + app/static/global.css | 262 +- app/static/markdown.css | 152 +- app/svelte.config.js | 12 +- app/vite.config.js | 6 +- 33 files changed, 2633 insertions(+), 2633 deletions(-) create mode 100644 app/.prettierrc create mode 100644 app/src/lib/util.js create mode 100644 app/static/animations.css diff --git a/admin/admin.py b/admin/admin.py index 87f9ceb..f02ad51 100644 --- a/admin/admin.py +++ b/admin/admin.py @@ -65,8 +65,8 @@ class AdminAPI: return title.lower().replace(" ", "_") def _check_multilang_field(self, ml: Dict[str, str]) -> bool: - for l in self.languages: - if l in ml and ml[l] != "": + for lang in self.languages: + if lang in ml and ml[lang] != "": return True return False @@ -114,24 +114,26 @@ class AdminAPI: ) def add_service(self, service: Dict[str, str]): - if not "name" in service or service["name"] == "": + if "name" not in service or service["name"] == "": raise Exception('Service structure is missing required "name" field') - if not "desc" in service: + if "desc" not in service: raise Exception('Service structure is missing required "desc" field') if ( - (not "clear" in service or service["clear"] == "") - and (not "onion" in service or service["onion"] == "") - and (not "i2p" in service or service["i2p"] == "") + ("clear" not in service or service["clear"] == "") + and ("onion" not in service or service["onion"] == "") + and ("i2p" not in service or service["i2p"] == "") ): raise Exception( - 'Service structure is missing "clear", "onion" and "i2p" field, at least one needed' + 'Service structure is missing "clear", "onion" ' + + 'and "i2p" field, at least one needed' ) if not self._check_multilang_field(service["desc"]): raise Exception( - 'Service structure field "desc" needs at least one supported language entry' + 'Service structure field "desc" needs at least ' + + "one supported language entry" ) self.PUT("/v1/admin/service/add", service) @@ -146,26 +148,28 @@ class AdminAPI: self.GET("/v1/admin/service/check") def add_news(self, news: Dict[str, str]): - if not "id" in news or news["id"] == "": + if "id" not in news or news["id"] == "": raise Exception('News structure is missing required "id" field') - if not "author" in news or news["author"] == "": + if "author" not in news or news["author"] == "": raise Exception('News structure is missing required "author" field') - if not "title" in news: + if "title" not in news: raise Exception('News structure is missing required "title" field') - if not "content" in news: + if "content" not in news: raise Exception('News structure is missing required "content" field') if not self._check_multilang_field(news["title"]): raise Exception( - 'News structure field "title" needs at least one supported language entry' + 'News structure field "title" needs at least ' + + "one supported language entry" ) if not self._check_multilang_field(news["content"]): raise Exception( - 'News structure field "content" needs at least one supported language entry' + 'News structure field "content" needs at least ' + + "one supported language entry" ) self.PUT("/v1/admin/news/add", news) @@ -205,8 +209,8 @@ def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None: data["desc"] = {} data["name"] = log.input("Serivce name") - for l in api.languages: - data["desc"][l] = log.input("Serivce desc (%s)" % l) + for lang in api.languages: + data["desc"][lang] = log.input("Serivce desc (%s)" % lang) data["check_url"] = log.input("Serivce status check URL") data["clear"] = log.input("Serivce clearnet URL") data["onion"] = log.input("Serivce onion URL") @@ -216,7 +220,7 @@ def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None: log.info("Service has been added") case "del_service": - api.del_service(self.log.input("Serivce name")) + api.del_service(log.input("Serivce name")) log.info("Service has been deleted") case "check_services": @@ -229,11 +233,11 @@ def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None: news["content"] = {} data["id"] = log.input("News ID") - for l in api.languages: - data["title"][l] = log.input("News title (%s)" % l) + for lang in api.languages: + data["title"][lang] = log.input("News title (%s)" % lang) data["author"] = log.input("News author") - for l in api.languages: - data["content"][l] = log.input("News content (%s)" % l) + for lang in api.languages: + data["content"][lang] = log.input("News content (%s)" % lang) api.add_news(data) log.info("News has been added") @@ -245,12 +249,13 @@ def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None: case "logs": logs = api.logs() - if None == logs["result"] or len(logs["result"]) == 0: + if logs["result"] is None or len(logs["result"]) == 0: return log.info("No available logs") - for l in logs["result"]: + for log in logs["result"]: log.info( - "Time: %s | Action: %s" % (__format_time(l["time"]), l["action"]) + "Time: %s | Action: %s" + % (__format_time(log["time"]), log["action"]) ) @@ -283,7 +288,7 @@ def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> case "logs": logs = api.logs() - if None == logs["result"] or len(logs["result"]) == 0: + if logs["result"] is None or len(logs["result"]) == 0: return log.info("No available logs") __dump_json_file(logs["result"], file) @@ -306,7 +311,7 @@ if __name__ == "__main__": url = getenv(API_URL_ENV) - if url == None: + if url is None: log.error( "Please specify the API URL using %s environment variable" % API_URL_ENV ) diff --git a/api/database/multilang.go b/api/database/multilang.go index 628157b..6b4709d 100644 --- a/api/database/multilang.go +++ b/api/database/multilang.go @@ -16,7 +16,7 @@ func (ml *Multilang) Supports(lang string) bool { ml_ref := reflect.ValueOf(ml).Elem() for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ { - if name := reflect.Indirect(ml_ref).Field(i).Type().Name(); strings.ToLower(name) == lang { + if name := reflect.Indirect(ml_ref).Type().Field(i).Name; strings.ToLower(name) == lang { return true } } diff --git a/api/database/news.go b/api/database/news.go index a66c359..8385116 100644 --- a/api/database/news.go +++ b/api/database/news.go @@ -57,7 +57,7 @@ func (n *News) Scan(rows *sql.Rows) (err error) { } func (n *News) IsValid() bool { - return n.Author != "" && n.ID != "" && !n.Title.Empty() && !n.Content.Empty() + return n.Time != 0 && n.Author != "" && n.ID != "" && !n.Title.Empty() && !n.Content.Empty() } func (db *Type) NewsNext(n *News) bool { diff --git a/api/go.mod b/api/go.mod index b664cd7..3d596b5 100644 --- a/api/go.mod +++ b/api/go.mod @@ -2,7 +2,11 @@ module github.com/ngn13/website/api go 1.21.3 -require github.com/gofiber/fiber/v2 v2.52.5 +require ( + github.com/gofiber/fiber/v2 v2.52.5 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/russross/blackfriday/v2 v2.1.0 +) require ( github.com/andybalholm/brotli v1.0.5 // indirect @@ -11,9 +15,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/api/routes/admin.go b/api/routes/admin.go index 649e92c..c71ecdf 100644 --- a/api/routes/admin.go +++ b/api/routes/admin.go @@ -138,6 +138,8 @@ func PUT_AddNews(c *fiber.Ctx) error { return util.ErrBadJSON(c) } + news.Time = uint64(time.Now().Unix()) + if !news.IsValid() { return util.ErrBadReq(c) } diff --git a/api/routes/news.go b/api/routes/news.go index 39d5a79..e58e32b 100644 --- a/api/routes/news.go +++ b/api/routes/news.go @@ -1,7 +1,9 @@ package routes import ( + "sort" "strings" + "time" "github.com/gofiber/fiber/v2" "github.com/ngn13/website/api/config" @@ -9,12 +11,31 @@ import ( "github.com/ngn13/website/api/util" ) +// feed_entry is a temporary struct used to pass the news to the news.xml +type feed_entry struct { + Title string + Author string + Time time.Time + RFC3339 string + Content string +} + +// convert UNIX timestamp to RFC3339 (format used by atom feeds) +func (e *feed_entry) From(news *database.News, lang string) { + e.Title = news.Title.Get(lang) + e.Author = news.Author + e.Time = time.Unix(int64(news.Time), 0) + e.RFC3339 = e.Time.Format(time.RFC3339) + e.Content = news.Content.Get(lang) +} + func GET_News(c *fiber.Ctx) error { var ( - news []database.News - n database.News - feed []byte - err error + entries []feed_entry + news database.News + indx uint64 + feed []byte + err error ) db := c.Locals("database").(*database.Type) @@ -27,17 +48,25 @@ func GET_News(c *fiber.Ctx) error { } lang = strings.ToLower(lang) + indx = 0 - for db.NewsNext(&n) { - if n.Supports(lang) { - news = append(news, n) + for db.NewsNext(&news) { + if news.Supports(lang) { + entries = append(entries, feed_entry{}) + entries[indx].From(&news, lang) + indx++ } } + sort.Slice(entries, func(i, j int) bool { + return entries[i].Time.Before(entries[j].Time) + }) + if feed, err = util.Render("views/news.xml", fiber.Map{ "frontend": frontend, + "updated": time.Now().Format(time.RFC3339), + "entries": entries, "lang": lang, - "news": news, }); err != nil { return util.ErrInternal(c, err) } diff --git a/api/views/news.xml b/api/views/news.xml index b78643f..251fa1f 100644 --- a/api/views/news.xml +++ b/api/views/news.xml @@ -1,6 +1,16 @@ {{.frontend.Host}} news - 2025-01-02T20:46:24Z - News and updates about my self-hosted services and projects - + {{.updated}} + News and updates about my projects and self-hosted services + + {{ range .entries }} + + {{.Title}} + {{.RFC3339}} + + {{.Author}} + + {{.Content}} + + {{ end }} diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 0000000..b1c437c --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": false, + "tabWidth": 2, + "singleQuote": false, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/app/package-lock.json b/app/package-lock.json index f4d1e7d..a9a0132 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,1566 +1,1595 @@ { - "name": "website", - "version": "5.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "website", - "version": "5.0.0", - "dependencies": { - "@types/dompurify": "^3.2.0", - "dompurify": "^3.2.3", - "marked": "^15.0.4" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.3.1", - "@sveltejs/adapter-node": "^5.2.11", - "@sveltejs/kit": "^2.15.1", - "@sveltejs/vite-plugin-svelte": "^4.0.3", - "svelte": "^5.16.0", - "vite": "^5.4.11" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", - "dev": true - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", - "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", - "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", - "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", - "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", - "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", - "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", - "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", - "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", - "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", - "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", - "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", - "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", - "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", - "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", - "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", - "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sveltejs/adapter-auto": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", - "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", - "dev": true, - "dependencies": { - "import-meta-resolve": "^4.1.0" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/adapter-node": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz", - "integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==", - "dev": true, - "dependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "rollup": "^4.9.5" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.4.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.1.tgz", - "integrity": "sha512-8t7D3hQHbUDMiaQ2RVnjJJ/+Ur4Fn/tkeySJCsHtX346Q9cp3LAnav8xXdfuqYNJwpUGX0x3BqF1uvbmXQw93A==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", - "devalue": "^5.1.0", - "esm-env": "^1.2.1", - "import-meta-resolve": "^4.1.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0", - "tiny-glob": "^0.2.9" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3 || ^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.3.tgz", - "integrity": "sha512-J7nC5gT5qpmvyD2pmzPUntLUgoinyEaNy9sTpGGE6N7pblggO0A1NyneJJvR2ELlzK6ti28aF2SLXG1yJdnJeA==", - "dev": true, - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", - "debug": "^4.3.7", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.12", - "vitefu": "^1.0.3" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.0.tgz", - "integrity": "sha512-hBxSYW/66989cq9dN248omD/ziskSdIV1NqfuueuAI1z6jGcg14k9Zd98pDIEnoA6wC9kWUGuQ6adzBbWwQyRg==", - "dev": true, - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", - "svelte": "^5.0.0-next.96 || ^5.0.0", - "vite": "^5.0.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/dompurify": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz", - "integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==", - "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", - "dependencies": { - "dompurify": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "optional": true - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-typescript": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", - "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", - "dev": true, - "peerDependencies": { - "acorn": ">=8.9.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "dev": true - }, - "node_modules/dompurify": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", - "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/esm-env": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", - "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", - "dev": true - }, - "node_modules/esrap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz", - "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-core-module": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", - "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/is-reference/node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/marked": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.4.tgz", - "integrity": "sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.45", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", - "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rollup": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", - "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.4", - "@rollup/rollup-android-arm64": "4.22.4", - "@rollup/rollup-darwin-arm64": "4.22.4", - "@rollup/rollup-darwin-x64": "4.22.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", - "@rollup/rollup-linux-arm-musleabihf": "4.22.4", - "@rollup/rollup-linux-arm64-gnu": "4.22.4", - "@rollup/rollup-linux-arm64-musl": "4.22.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", - "@rollup/rollup-linux-riscv64-gnu": "4.22.4", - "@rollup/rollup-linux-s390x-gnu": "4.22.4", - "@rollup/rollup-linux-x64-gnu": "4.22.4", - "@rollup/rollup-linux-x64-musl": "4.22.4", - "@rollup/rollup-win32-arm64-msvc": "4.22.4", - "@rollup/rollup-win32-ia32-msvc": "4.22.4", - "@rollup/rollup-win32-x64-msvc": "4.22.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.0.tgz", - "integrity": "sha512-Ygqsiac6UogVED2ruKclU+pOeMThxWtp9LG+li7BXeDKC2paVIsRTMkNmcON4Zejerd1s5sZHWx6ZtU85xklVg==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@types/estree": "^1.0.5", - "acorn": "^8.12.1", - "acorn-typescript": "^1.4.13", - "aria-query": "^5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "esm-env": "^1.2.1", - "esrap": "^1.3.2", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz", - "integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==", - "dev": true, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "dev": true - } - } + "name": "website", + "version": "5.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website", + "version": "5.0.0", + "dependencies": { + "@types/dompurify": "^3.2.0", + "dompurify": "^3.2.3", + "marked": "^15.0.4" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.3.1", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.15.1", + "@sveltejs/vite-plugin-svelte": "^4.0.3", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.2", + "svelte": "^5.16.0", + "vite": "^5.4.11" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", + "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^4.1.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz", + "integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==", + "dev": true, + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.1.tgz", + "integrity": "sha512-8t7D3hQHbUDMiaQ2RVnjJJ/+Ur4Fn/tkeySJCsHtX346Q9cp3LAnav8xXdfuqYNJwpUGX0x3BqF1uvbmXQw93A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.1", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.3.tgz", + "integrity": "sha512-J7nC5gT5qpmvyD2pmzPUntLUgoinyEaNy9sTpGGE6N7pblggO0A1NyneJJvR2ELlzK6ti28aF2SLXG1yJdnJeA==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "debug": "^4.3.7", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.12", + "vitefu": "^1.0.3" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.0.tgz", + "integrity": "sha512-hBxSYW/66989cq9dN248omD/ziskSdIV1NqfuueuAI1z6jGcg14k9Zd98pDIEnoA6wC9kWUGuQ6adzBbWwQyRg==", + "dev": true, + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/dompurify": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz", + "integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==", + "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", + "dependencies": { + "dompurify": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "dev": true, + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true + }, + "node_modules/dompurify": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/esm-env": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz", + "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==", + "dev": true + }, + "node_modules/esrap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz", + "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-core-module": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-reference/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.4.tgz", + "integrity": "sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.2.tgz", + "integrity": "sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/resolve": { + "version": "1.22.9", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", + "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.0.tgz", + "integrity": "sha512-Ygqsiac6UogVED2ruKclU+pOeMThxWtp9LG+li7BXeDKC2paVIsRTMkNmcON4Zejerd1s5sZHWx6ZtU85xklVg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "acorn-typescript": "^1.4.13", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.3.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz", + "integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true + } + } } diff --git a/app/package.json b/app/package.json index 2667784..5fb0bfc 100644 --- a/app/package.json +++ b/app/package.json @@ -1,24 +1,28 @@ { - "name": "website", - "version": "5.0.0", - "private": true, - "scripts": { - "dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev", - "build": "vite build", - "preview": "vite preview --host" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.3.1", - "@sveltejs/adapter-node": "^5.2.11", - "@sveltejs/kit": "^2.15.1", - "@sveltejs/vite-plugin-svelte": "^4.0.3", - "svelte": "^5.16.0", - "vite": "^5.4.11" - }, - "type": "module", - "dependencies": { - "@types/dompurify": "^3.2.0", - "dompurify": "^3.2.3", - "marked": "^15.0.4" - } + "name": "website", + "version": "5.0.0", + "private": true, + "scripts": { + "dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev", + "build": "vite build", + "preview": "vite preview --host", + "lint": "prettier --check .", + "format": "prettier --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.3.1", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.15.1", + "@sveltejs/vite-plugin-svelte": "^4.0.3", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.2", + "svelte": "^5.16.0", + "vite": "^5.4.11" + }, + "type": "module", + "dependencies": { + "@types/dompurify": "^3.2.0", + "dompurify": "^3.2.3", + "marked": "^15.0.4" + } } diff --git a/app/src/app.html b/app/src/app.html index 56aa21c..b0fe481 100644 --- a/app/src/app.html +++ b/app/src/app.html @@ -1,12 +1,12 @@ - + - - - - - %sveltekit.head% - - -
%sveltekit.body%
- + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/app/src/lib/card.svelte b/app/src/lib/card.svelte index 5cfb435..e0c1954 100644 --- a/app/src/lib/card.svelte +++ b/app/src/lib/card.svelte @@ -1,52 +1,63 @@
root@ngn.tf:~# {current} -
+
diff --git a/app/src/lib/card_link.svelte b/app/src/lib/card_link.svelte index 2d94d07..8b79510 100644 --- a/app/src/lib/card_link.svelte +++ b/app/src/lib/card_link.svelte @@ -1,21 +1,24 @@ @@ -25,46 +28,54 @@
{current} -
+
diff --git a/app/src/lib/header.svelte b/app/src/lib/header.svelte index 4dba0d5..6522c7a 100644 --- a/app/src/lib/header.svelte +++ b/app/src/lib/header.svelte @@ -1,5 +1,5 @@
@@ -10,32 +10,31 @@ export let subtitle = ""
diff --git a/app/src/lib/navbar.svelte b/app/src/lib/navbar.svelte index 88785a0..3b6037b 100644 --- a/app/src/lib/navbar.svelte +++ b/app/src/lib/navbar.svelte @@ -1,51 +1,43 @@ -