diff --git a/.gitignore b/.gitignore index 6c4cd8f..866d243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ +data.db *.yaml +*.yml *.env + +# don't ignore example deployment stuff +!deploy/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..09e6e24 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +SERVERS = app api doc + +all: $(SERVERS) + for server in $^ ; do \ + make -C $$server ; \ + done + +format: + for server in $(SERVERS) ; do \ + make -C $$server format ; \ + done + +.PHONY: format diff --git a/README.md b/README.md index 992f8d7..6ea749e 100644 --- a/README.md +++ b/README.md @@ -14,80 +14,72 @@ and fonts from [NerdFonts](https://www.nerdfonts.com/) ### `api` Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web -framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used worked with express a lot in the past. However previously the I was using -[Gin](https://github.com/gin-gonic/gin) (see history section). +framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used +worked with express a lot in the past. However previously the I was using [Gin](https://github.com/gin-gonic/gin) +(see history section). -API stores all the data in a local sqlite(3) database. Go doesn't support sqlite3 out of the box so +API stores all the data in a local SQLite(3) database. Go doesn't support SQLite3 out of the box so I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3). +### `doc` +Contains the documentation server, written in C. It uses the [ctorm](https://github.com/ngn13/ctorm) web +framework, which is a framework that I myself wrote. Unlike the frontend application or the API server, it's not +accessable by public, the frontend application gets the documentation content from this server and renders it using +SSR. The reason I don't use the API for hosting the documentation content is that I want a separate server for hosting +static content, API is only for hosting dynamic stuff. + ### `admin` -The frontend application does not contain an admin interface, I do the administration stuff (such as -adding posts, adding services etc.) using the python script in this directory. This script can be -installed on to the PATH by running the `install.sh` script. After installation it can be used -by running `admin_script`. +The frontend application does not contain an admin interface, I do the administration stuff (such as adding news posts, +adding services etc.) using the python script in this directory. This script can be installed on to the PATH by running +the Makefile install script. After installation it can be used by running `admin_script`. ## Deployment -Easiest way to deploy is to use docker. I have created a `compose.yml` file so the API and the -frontend application can be deployed easily with just the `docker-compose up` command: -```yaml -version: "3" -services: - app: - build: - context: ./app - args: - API_URL: https://api.ngn.tf - ports: - - "127.0.0.1:7002:3000" - depends_on: - - api - - api: - build: - context: ./api - environment: - - API_PASSWORD="securepassword" - - API_FRONTEND_URL="https://ngn.tf" - ports: - - "127.0.0.1:7001:7001" - volumes: - - ./api/data.db:/app/data.db -``` +Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh` script in the [deploy](deploy/) directory +that can be used to startup all the docker containers. Configuration options are passed during build time for the frontend +application, and for others it's passed with environment variables. ## History Some nostalgic history/changelog stuff (just for the major version numbers): - **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page, -I never published any of the source code and I wiped the local copy on my USB drive in early 2022 +I never published any of the source code and I wiped the local copy on my USB drive in early 2022, I still +remember what it looked like though, it looked like I made entire website in microsoft paint... while blindfoled, +so yeah it was shit. - **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code -was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png) +was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png). -- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework) -my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version, -at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just -updated it everytime I wanted to add content? It was pretty much like a static website and was hosted -on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a -deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able +- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework) my website with one +of the fancy JS frameworks. I decided to go with Svelte. Not the kit version, at the time svelte did not support SSR. +I do not remember writting an API for it so I guess I just updated it everytime I wanted to add content? It was pretty +much like a static website and was hosted on `ngn13.fun` as at this point I had my own hosting. The source code for +this website was in a deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able to find it. I also do not remember how it looked like, sooo this version is pretty much lost :( -- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework -my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported -server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun` +- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework my website one more +time in NuxtJS. I had really "fun" time using vue stuff. As NuxtJS supported server-side code, this website had its own +built in API. This website was also hosted on `ngn13.fun`. This also the first version that lives on this git repository. -- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website -again, this time I was going with SvelteKit as I haven't had the greatest experience with NuxtJS. -SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna -brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API -with Go and Gin. I did not publish the source code for the API, its still on my local gitea -server tho. This website was hosted on `ngn13.fun` as well +- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website again, this time +I was going with SvelteKit as I haven't had the greatest experience with NuxtJS. SvelteKit was really fun to work with +and I got my new website done pretty quickly. (I don't wanna brag or something but I really imporeved the CSS/styling +stuff ya know). I also wrote a new API with Go and Gin. I did not publish the source code for the API, the code lived +on my local git server until I deleted it when I was done with 6.0. This website was hosted on `ngn13.fun` as well. -- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0, -the big changes are in the API. I rewrote the API with Fiber. This version was the first version which is hosted on -`ngn.tf` which is my new domain name btw +- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0, the big changes are in +the API. I rewrote the API with Fiber. This version was the first version hosted on `ngn.tf` which is my new domain name. -- **v5.0 (2024 october - ...)**: The current major version of my website, has small UI and API tweaks when -compared to 4.0 +- **v5.0 (2024 october - 2025 january)**: This version just had simple frontend UI changes compared to 4.0, at this +point I was thinking about doing a massive rework (which I did with 6.0), however I was working on some other shit at +the time, so I just did some small changes with the limited time I had for this project. + +- **v6.0 (2025 january - ...)**: The current major version of my website, frontend had a massive rework, API has been +cleaned up and extended to do status checking for the services I host. The `doc` server has been added to the mix +so I can host static documentation. The most important thing about this version is that it adds multi-language support, +so literally everything on the website (including the API and documentation content) is localized for both English +and Turkish, which was something I wanted to do for the longest time ever. + +Damn it has been 4 years since I wrote that shit HTML page huh? Time flies... ## Screenshots (from v4.0) ![](assets/4.0_index.png) 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..925a6e3 100644 --- a/admin/admin.py +++ b/admin/admin.py @@ -1,176 +1,404 @@ #!/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 = "" -def join(pth: str) -> str: - if URL == None: - return "" +# logger used by the script +class Log: + def __init__(self) -> None: + self.reset = Fore.RESET + Style.RESET_ALL - if URL.endswith("/"): - return URL+pth - return URL+"/"+pth + def info(self, m: str) -> None: + print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m) -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 error(self, m: str) -> None: + print(Fore.RED + 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 input(self, m: str) -> str: + return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ") - token = res["token"] - f = open("/tmp/wa", "w") - f.write(token) - f.close() + def password(self, m: str) -> str: + return getpass(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 - remove("/tmp/wa") - print("[+] Logged out") +# 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 -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): ") + def _title_to_id(self, title: str) -> str: + return title.lower().replace(" ", "_") - try: - f = open(content_file, "r") - content = f.read() - f.close() - except: - print("[-] Content file not found") - return + def _check_multilang_field(self, ml: Dict[str, str]) -> bool: + for lang in self.languages: + if lang in ml and ml[lang] != "": + return True + return False - 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 _api_url_join(self, path: str) -> str: + api_has_slash = self.api_url.endswith("/") + path_has_slash = path.startswith("/") - if res["error"] != "": - print(f"[-] Error adding post: {res['error']}") - return + 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 - print("[+] Post has been added") + def _to_json(self, res: req.Response) -> dict: + if res.status_code == 403: + raise Exception("Authentication failed") -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() + json = res.json() - if res["error"] != "": - print(f"[-] Error removing post: {res['error']}") - return + if json["error"] != "": + raise Exception("API error: %s" % json["error"]) - print("[-] Post has been removed") + return json -def add_service() -> None: - token = get_token() - name = input("[>] Serivce name: ") - desc = input("[>] Serivce desc: ") - link = input("[>] Serivce URL: ") + 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}, + ) + ) - res = req.put(join("admin/service/add"), json={ - "name": name, - "desc": desc, - "url": link - }, headers={ - "Authorization": token - }).json() + def DELETE(self, url: str) -> req.Response: + return self._to_json( + req.delete( + self._api_url_join(url), headers={"Authorization": self.password} + ) + ) - if res["error"] != "": - print(f"[-] Error adding service: {res['error']}") - return + def GET(self, url: str) -> req.Response: + return self._to_json( + req.get(self._api_url_join(url), headers={"Authorization": self.password}) + ) - print("[+] Service has been added") + def add_service(self, service: Dict[str, str]): + if "name" not in service or service["name"] == "": + raise Exception('Service structure is missing required "name" field') -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() + if "desc" not in service: + raise Exception('Service structure is missing required "desc" field') - if res["error"] != "": - print(f"[-] Error removing service: {res['error']}") - return + if ( + ("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' + ) - print("[+] Serivce has been removed") + if not self._check_multilang_field(service["desc"]): + raise Exception( + 'Service structure field "desc" needs at least ' + + "one supported language entry" + ) -cmds = { - "login": login, - "logout": logout, - "add_post": add_post, - "remove_post": remove_post, - "add_service": add_service, - "remove_service": remove_service, -} + self.PUT("/v1/admin/service/add", service) -def main(): - global URL - URL = getenv("API") - if URL == None or URL == "": - print("[-] API enviroment variable not set") - exit(1) + def del_service(self, name: str) -> None: + if name == "": + raise Exception("Service name cannot be empty") - if len(argv) != 2: - print(f"[-] Usage: admin_script ") - print(f"[+] Run \"admin_script help\" to get all commands") - exit(1) + self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(name)) - if argv[1] == "help": - print("Avaliable commands:") - for k in cmds.keys(): - print(f" {k}") - exit() + def add_project(self, project: Dict[str, str]): + if "name" not in project or project["name"] == "": + raise Exception('Project structure is missing required "name" field') + + if "desc" not in project: + raise Exception('Project structure is missing required "desc" field') + + if not self._check_multilang_field(project["desc"]): + raise Exception( + 'Project structure field "desc" needs at least ' + + "one supported language entry" + ) + + self.PUT("/v1/admin/project/add", project) + + def del_project(self, name: str) -> None: + if name == "": + raise Exception("Project name cannot be empty") + + self.DELETE("/v1/admin/project/del?name=%s" % quote_plus(name)) + + def check_services(self) -> None: + self.GET("/v1/admin/service/check") + + def add_news(self, news: Dict[str, str]): + if "id" not in news or news["id"] == "": + raise Exception('News structure is missing required "id" field') + + if "author" not in news or news["author"] == "": + raise Exception('News structure is missing required "author" field') + + if "title" not in news: + raise Exception('News structure is missing required "title" field') + + 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" + ) + + 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, id: str) -> None: + if id == "": + raise Exception("News ID cannot be empty") + + self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(id)) + + def logs(self) -> List[Dict[str, Any]]: + return self.GET("/v1/admin/logs") + + +class AdminScript: + def __init__(self): + self.log: Log = Log() + self.api: AdminAPI = None + self.commands = { + "add_service": self.add_service, + "del_service": self.del_service, + "add_project": self.add_project, + "del_project": self.del_project, + "add_news": self.add_news, + "del_news": self.del_news, + "check_services": self.check_services, + "logs": self.get_logs, + } + self.api_url_env = "API_URL" + + def __format_time(self, ts: int) -> str: + return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y") + + def __load_json_file(self, file: str) -> Dict[str, Any]: + with open(file, "r") as f: + data = loads(f.read()) + return data + + def __dump_json_file(self, data: Dict[str, Any], file: str) -> None: + with open(file, "w") as f: + data = dumps(data, indent=2) + f.write(data) + + def run(self) -> bool: + if len(argv) < 2 or len(argv) > 3: + self.log.error("Usage: %s [command] " % argv[0]) + self.log.info("Here is a list of available commands:") + + for command in self.commands.keys(): + print("\t%s" % command) + + return False + + url = getenv(self.api_url_env) + valid_cmd = False + + if url is None: + self.log.error( + "Please specify the API URL using %s environment variable" + % self.api_url_env + ) + return False + + for cmd in self.commands: + if argv[1] == cmd: + valid_cmd = True + break + + if not valid_cmd: + self.log.error( + "Invalid command, run the script with no commands to list the available commands" + ) + return False - for k in cmds.keys(): - if k != argv[1]: - continue try: - cmds[k]() - except KeyboardInterrupt: - pass - exit() + password = self.log.password("Please enter the admin password") + self.api = AdminAPI(url, password) + + if len(argv) == 2: + self.handle_command(argv[1]) + + elif len(argv) == 3: + self.handle_command(argv[1], argv[2]) + + except KeyboardInterrupt: + self.log.error("Command cancelled") + return False + + except Exception as e: + self.log.error("Command failed: %s" % e) + return False + + # service commands + def add_service(self, data: Dict[str, Any] = None) -> None: + if data is None: + data: Dict[str, str] = {} + data["desc"] = {} + + data["name"] = self.log.input("Serivce name") + + for lang in self.api.languages: + data["desc"][lang] = self.log.input("Serivce desc (%s)" % lang) + + data["check_url"] = self.log.input("Serivce status check URL") + data["clear"] = self.log.input("Serivce clearnet URL") + data["onion"] = self.log.input("Serivce onion URL") + data["i2p"] = self.log.input("Serivce I2P URL") + + self.api.add_service(data) + self.log.info("Service has been added") + + def del_service(self, data: Dict[str, Any] = None) -> None: + if data is None: + data: Dict[str, str] = {} + data["name"] = self.log.input("Service name") + + self.api.del_service(data["name"]) + self.log.info("Service has been deleted") + + # project commands + def add_project(self, data: Dict[str, Any] = None) -> None: + if data is None: + data: Dict[str, str] = {} + data["desc"] = {} + + data["name"] = self.log.input("Project name") + + for lang in self.api.languages: + data["desc"][lang] = self.log.input("Project desc (%s)" % lang) + + data["url"] = self.log.input("Project URL") + data["license"] = self.log.input("Project license") + + self.api.add_project(data) + self.log.info("Project has been added") + + def del_project(self, data: Dict[str, Any] = None) -> None: + if data is None: + data: Dict[str, str] = {} + data["name"] = self.log.input("Project name") + + self.api.del_project(data["name"]) + self.log.info("Project has been deleted") + + # news command + def add_news(self, data: Dict[str, Any] = None) -> None: + if data is None: + news: Dict[str, str] = {} + news["title"] = {} + news["content"] = {} + + data["id"] = self.log.input("News ID") + + for lang in self.api.languages: + data["title"][lang] = self.log.input("News title (%s)" % lang) + + data["author"] = self.log.input("News author") + + for lang in self.api.languages: + data["content"][lang] = self.log.input("News content (%s)" % lang) + + self.api.add_news(data) + self.log.info("News has been added") + + def del_news(self, data: Dict[str, Any] = None) -> None: + if data is None: + data: Dict[str, str] = {} + data["id"] = self.log.input("News ID") + + self.api.del_project(data["id"]) + self.log.info("News has been deleted") + + def check_services(self, data: Dict[str, Any] = None) -> None: + self.api.check_services() + self.log.info("Requested status check for all the services") + + def get_logs(self, data: Dict[str, Any] = None) -> None: + logs = self.api.logs() + + if logs["result"] is None or len(logs["result"]) == 0: + return self.log.info("No available logs") + + for log in logs["result"]: + self.log.info( + "Time: %s | Action: %s" + % (self.__format_time(log["time"]), log["action"]) + ) + + def handle_command(self, cmd: str, file: str = None) -> bool: + for command in self.commands.keys(): + if command != cmd: + continue + + data = None + + try: + if file != "" and file is not None: + data = self.__load_json_file(file) + + self.commands[cmd](data) + return True + + except Exception as e: + self.log.error("Command failed: %s" % e) + return False + + self.log.error("Invalid command: %s", cmd) + return False - print("[-] Command not found") if __name__ == "__main__": - main() + script = AdminScript() + exit(script.run() if 1 else 0) 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_project.json b/admin/tests/test_project.json new file mode 100644 index 0000000..bc7d519 --- /dev/null +++ b/admin/tests/test_project.json @@ -0,0 +1,9 @@ +{ + "name": "test", + "desc": { + "en": "A non-existent project used to test the API", + "tr": "API'ı test etmek için kullanılan varolmayan bir proje" + }, + "url": "https://github.com/ngn13/test", + "license": "GPL-3.0" +} 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..84e53d2 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,17 +1,26 @@ FROM golang:1.23.4 -WORKDIR /app +WORKDIR /api + +RUN useradd runner -r -u 1001 -d /api +RUN chown -R runner:runner /api +USER runner -COPY *.go ./ COPY *.mod ./ COPY *.sum ./ + +RUN go mod download + +COPY *.go ./ COPY Makefile ./ -COPY routes ./routes COPY config ./config COPY database ./database +COPY routes ./routes +COPY sql ./sql +COPY status ./status COPY util ./util +COPY views ./views -EXPOSE 7001 RUN make -ENTRYPOINT ["/app/server"] +ENTRYPOINT ["/api/api.elf"] diff --git a/api/Makefile b/api/Makefile index f6908ec..e496f4e 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: + WEBSITE_DEBUG=true WEBSITE_PASSWORD=test ./api.elf format: gofmt -s -w . diff --git a/api/config/config.go b/api/config/config.go index 29d5d60..5319703 100644 --- a/api/config/config.go +++ b/api/config/config.go @@ -2,59 +2,108 @@ 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: "app_url", Value: "http://localhost:7001/", 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:7002", 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..cae1f4a --- /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("WEBSITE_%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_log.go b/api/database/admin_log.go new file mode 100644 index 0000000..c80f99b --- /dev/null +++ b/api/database/admin_log.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 " + TABLE_ADMIN_LOG); err != nil { + util.Fail("failed to query 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 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 "+TABLE_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..f35f32f 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -1,44 +1,64 @@ package database import ( + "fmt" + "os" + "path" + "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 - ); - `) +const ( + SQL_PATH = "sql" - if err != nil { - return err - } + TABLE_ADMIN_LOG = "admin_log" // stores administrator logs + TABLE_METRICS = "metrics" // stores API usage metrcis + TABLE_NEWS = "news" // stores news posts + TABLE_SERVICES = "services" // stores services + TABLE_PROJECTS = "projects" // stores projects +) - _, 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 +var tables []string = []string{ + TABLE_ADMIN_LOG, TABLE_METRICS, TABLE_NEWS, + TABLE_SERVICES, TABLE_PROJECTS, +} + +type Type struct { + sql *sql.DB + rows *sql.Rows +} + +func (db *Type) create_table(table string) error { + var ( + err error + query []byte + ) + + query_path := path.Join(SQL_PATH, table+".sql") + + if query, err = os.ReadFile(query_path); err != nil { + return fmt.Errorf("failed to read %s for table %s: %", query_path, table, err.Error()) + } + + if _, err = db.sql.Exec(string(query)); err != nil { + return fmt.Errorf("failed to create the %s table: %s", table, err.Error()) + } + + return nil +} + +func (db *Type) Load() (err error) { + if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil { + return fmt.Errorf("failed access the database: %s", err.Error()) + } + + for _, table := range tables { + if err = db.create_table(table); err != nil { + return err + } + } + + return nil } diff --git a/api/database/metrics.go b/api/database/metrics.go new file mode 100644 index 0000000..22ae9b5 --- /dev/null +++ b/api/database/metrics.go @@ -0,0 +1,57 @@ +package database + +import ( + "database/sql" + + "github.com/ngn13/website/api/util" +) + +func (db *Type) MetricsGet(key string) (uint64, error) { + var ( + row *sql.Row + count uint64 + err error + ) + + if row = db.sql.QueryRow("SELECT value FROM "+TABLE_METRICS+" WHERE key = ?", key); row == nil { + return 0, nil + } + + if err = row.Scan(&count); err != nil && err != sql.ErrNoRows { + util.Fail("failed to scan the table: %s", err.Error()) + return 0, err + } + + if err == sql.ErrNoRows { + return 0, nil + } + + return count, nil +} + +func (db *Type) MetricsSet(key string, value uint64) error { + var ( + err error + res sql.Result + ) + + if res, err = db.sql.Exec("UPDATE "+TABLE_METRICS+" SET value = ? WHERE key = ?", value, key); err != nil && err != sql.ErrNoRows { + util.Fail("failed to query table: %s", err.Error()) + return err + } + + if effected, err := res.RowsAffected(); err != nil { + return err + } else if effected < 1 { + _, err = db.sql.Exec( + "INSERT INTO "+TABLE_METRICS+`( + key, value + ) values(?, ?)`, + key, value, + ) + + return err + } + + return nil +} diff --git a/api/database/multilang.go b/api/database/multilang.go new file mode 100644 index 0000000..6b4709d --- /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).Type().Field(i).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..10f766f --- /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.Time != 0 && 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 " + TABLE_NEWS); err != nil { + util.Fail("failed to query 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 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 "+TABLE_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 "+TABLE_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/project.go b/api/database/project.go new file mode 100644 index 0000000..0949a8a --- /dev/null +++ b/api/database/project.go @@ -0,0 +1,92 @@ +package database + +import ( + "database/sql" + + "github.com/ngn13/website/api/util" +) + +type Project struct { + Name string `json:"name"` // name of the project + desc string `json:"-"` // description of the project (string) + Desc Multilang `json:"desc"` // description of the project + URL string `json:"url"` // URL of the project's homepage/source + License string `json:"license"` // name of project's license +} + +func (p *Project) Load() error { + return p.Desc.Load(p.desc) +} + +func (p *Project) Dump() (err error) { + p.desc, err = p.Desc.Dump() + return +} + +func (p *Project) Scan(rows *sql.Rows) (err error) { + if err = rows.Scan( + &p.Name, &p.desc, + &p.URL, &p.License); err != nil { + return err + } + + return p.Load() +} + +func (p *Project) IsValid() bool { + return p.Name != "" && p.URL != "" && !p.Desc.Empty() +} + +func (db *Type) ProjectNext(p *Project) bool { + var err error + + if nil == db.rows { + if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_PROJECTS); err != nil { + util.Fail("failed to query table: %s", err.Error()) + goto fail + } + } + + if !db.rows.Next() { + goto fail + } + + if err = p.Scan(db.rows); err != nil { + util.Fail("failed to scan the table: %s", err.Error()) + goto fail + } + + return true + +fail: + if db.rows != nil { + db.rows.Close() + } + db.rows = nil + + return false +} + +func (db *Type) ProjectRemove(name string) error { + _, err := db.sql.Exec( + "DELETE FROM "+TABLE_PROJECTS+" WHERE name = ?", + name, + ) + + return err +} + +func (db *Type) ProjectAdd(p *Project) (err error) { + if err = p.Dump(); err != nil { + return err + } + + _, err = db.sql.Exec( + "INSERT OR REPLACE INTO "+TABLE_PROJECTS+`( + name, desc, url, license + ) values(?, ?, ?, ?)`, + p.Name, p.desc, p.URL, p.License, + ) + + return err +} diff --git a/api/database/service.go b/api/database/service.go index 3b14c71..596bc10 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 " + TABLE_SERVICES); err != nil { + util.Fail("failed to query 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 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 "+TABLE_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( - "DELETE FROM services WHERE name = ?", - s.Name, +func (db *Type) ServiceRemove(name string) error { + _, err := db.sql.Exec( + "DELETE FROM "+TABLE_SERVICES+" WHERE 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 "+TABLE_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..0e9af9d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,7 +4,6 @@ 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 ) diff --git a/api/go.sum b/api/go.sum index e42c05e..2885b92 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,6 @@ 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/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..0d4af96 100644 --- a/api/main.go +++ b/api/main.go @@ -1,102 +1,131 @@ 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("/projects", routes.GET_Projects) + v1.Get("/metrics", routes.GET_Metrics) + v1.Get("/news/:lang", routes.GET_News) - // blog post routes - blog_routes.Get("/sum", routes.GET_PostSum) - blog_routes.Get("/get", routes.GET_Post) + // v1 admin routes + v1.Use("/admin", routes.AuthMiddleware) + v1.Get("/admin/logs", routes.GET_AdminLogs) - // blog post vote routes - blog_routes.Get("/vote/set", routes.GET_VoteSet) - blog_routes.Get("/vote/get", routes.GET_VoteGet) + v1.Get("/admin/service/check", routes.GET_CheckService) + v1.Put("/admin/service/add", routes.PUT_AddService) + v1.Delete("/admin/service/del", routes.DEL_DelService) - // service routes - service_routes := app.Group("services") - service_routes.Get("/all", routes.GET_Services) + v1.Put("/admin/project/add", routes.PUT_AddProject) + v1.Delete("/admin/project/del", routes.DEL_DelProject) - // 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.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..142b516 100644 --- a/api/routes/admin.go +++ b/api/routes/admin.go @@ -1,183 +1,206 @@ 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 { - var ( - db *sql.DB - id string - found bool - err error - post database.Post - ) - - 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 searching for a post (\"%s\"): %s", id, err.Error()) - return util.ErrServer(c) - } - - if !found { - return util.ErrEntryNotExists(c) - } - - 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) +func GET_CheckService(c *fiber.Ctx) error { + c.Locals("status").(*status.Type).Check() + return util.JSON(c, 200, nil) } -func PUT_AddPost(c *fiber.Ctx) error { +func PUT_AddProject(c *fiber.Ctx) error { var ( - db *sql.DB - post database.Post - err error + project database.Project + 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(&project) != nil { return util.ErrBadJSON(c) } - if post.Title == "" || post.Author == "" || post.Content == "" { - return util.ErrBadData(c) + if !project.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 project \"%s\"", project.Name)); 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.ProjectAdd(&project); err != nil { + return util.ErrInternal(c, err) } - return util.NoError(c) + return util.JSON(c, 200, nil) +} + +func DEL_DelProject(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 err = admin_log(c, fmt.Sprintf("Removed project \"%s\"", name)); err != nil { + return util.ErrInternal(c, err) + } + + if err = db.ProjectRemove(name); err != nil { + return util.ErrInternal(c, err) + } + + return util.JSON(c, 200, nil) +} + +func DEL_DelNews(c *fiber.Ctx) error { + var ( + id string + err error + ) + + db := c.Locals("database").(*database.Type) + + if id = c.Query("id"); id == "" { + util.ErrBadReq(c) + } + + if err = admin_log(c, fmt.Sprintf("Removed news \"%s\"", id)); err != nil { + return util.ErrInternal(c, err) + } + + if err = db.NewsRemove(id); err != nil { + return util.ErrInternal(c, err) + } + + return util.JSON(c, 200, nil) +} + +func PUT_AddNews(c *fiber.Ctx) error { + var ( + news database.News + err error + ) + + db := c.Locals("database").(*database.Type) + + if c.BodyParser(&news) != nil { + return util.ErrBadJSON(c) + } + + news.Time = uint64(time.Now().Unix()) + + if !news.IsValid() { + return util.ErrBadReq(c) + } + + if err = admin_log(c, fmt.Sprintf("Added news \"%s\"", news.ID)); err != nil { + return util.ErrInternal(c, err) + } + + if err = db.NewsAdd(&news); err != nil { + return util.ErrInternal(c, err) + } + + 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..1d60bc7 --- /dev/null +++ b/api/routes/index.go @@ -0,0 +1,14 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/config" +) + +func GET_Index(c *fiber.Ctx) error { + conf := c.Locals("config").(*config.Type) + app := conf.GetURL("app_url") + + // redirect to the API documentation + return c.Redirect(app.JoinPath("/doc/api").String()) +} diff --git a/api/routes/metrics.go b/api/routes/metrics.go new file mode 100644 index 0000000..728cab7 --- /dev/null +++ b/api/routes/metrics.go @@ -0,0 +1,67 @@ +package routes + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/database" + "github.com/ngn13/website/api/util" +) + +const VISITOR_CACHE_MAX = 30 // store 30 visitor data at most +var visitor_cache []string // in memory cache for the visitor addresses + +func GET_Metrics(c *fiber.Ctx) error { + var ( + err error + result map[string]uint64 = map[string]uint64{ + "total": 0, // total number of visitors + "since": 0, // metric collection start date (UNIX timestamp) + } + ) + + db := c.Locals("database").(*database.Type) + new_addr := util.GetSHA1(util.IP(c)) + is_in_cache := false + + for _, cache := range visitor_cache { + if new_addr == cache { + is_in_cache = true + break + } + } + + if result["total"], err = db.MetricsGet("visitor_count"); err != nil { + return util.ErrInternal(c, err) + } + + if !is_in_cache { + if len(visitor_cache) > VISITOR_CACHE_MAX { + util.Debg("visitor cache is full, removing the oldest entry") + visitor_cache = visitor_cache[1:] + } + + visitor_cache = append(visitor_cache, new_addr) + result["total"]++ + + if err = db.MetricsSet("visitor_count", result["total"]); err != nil { + return util.ErrInternal(c, err) + } + } + + if result["since"], err = db.MetricsGet("start_date"); err != nil { + return util.ErrInternal(c, err) + } + + if result["since"] == 0 { + result["since"] = uint64(time.Now().Truncate(24 * time.Hour).Unix()) + + if err = db.MetricsSet("start_date", result["since"]); err != nil { + return util.ErrInternal(c, err) + } + } + + return util.JSON(c, 200, fiber.Map{ + "result": result, + }) +} diff --git a/api/routes/news.go b/api/routes/news.go new file mode 100644 index 0000000..1e8cdca --- /dev/null +++ b/api/routes/news.go @@ -0,0 +1,78 @@ +package routes + +import ( + "sort" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/config" + "github.com/ngn13/website/api/database" + "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 ( + entries []feed_entry + news database.News + indx uint64 + feed []byte + err error + ) + + db := c.Locals("database").(*database.Type) + conf := c.Locals("config").(*config.Type) + app := conf.GetURL("app_url") + lang := c.Params("lang") + + if lang == "" || len(lang) != 2 { + return util.ErrBadReq(c) + } + + lang = strings.ToLower(lang) + indx = 0 + + 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{ + "updated": time.Now().Format(time.RFC3339), + "entries": entries, + "lang": lang, + "app": app, + }); err != nil { + return util.ErrInternal(c, err) + } + + c.Set("Content-Disposition", "attachment; filename=\"news.atom\"") + c.Set("Content-Type", "application/atom+xml; charset=utf-8") + + return c.Send(feed) +} diff --git a/api/routes/projects.go b/api/routes/projects.go new file mode 100644 index 0000000..af919ac --- /dev/null +++ b/api/routes/projects.go @@ -0,0 +1,24 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/database" + "github.com/ngn13/website/api/util" +) + +func GET_Projects(c *fiber.Ctx) error { + var ( + projects []database.Project + project database.Project + ) + + db := c.Locals("database").(*database.Type) + + for db.ProjectNext(&project) { + projects = append(projects, project) + } + + return util.JSON(c, 200, fiber.Map{ + "result": projects, + }) +} 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/sql/admin_log.sql b/api/sql/admin_log.sql new file mode 100644 index 0000000..6089dbb --- /dev/null +++ b/api/sql/admin_log.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS admin_log( + action TEXT NOT NULL, + time INTEGER NOT NULL +); diff --git a/api/sql/metrics.sql b/api/sql/metrics.sql new file mode 100644 index 0000000..e23a6ef --- /dev/null +++ b/api/sql/metrics.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS metrics( + key TEXT NOT NULL UNIQUE, + value INTEGER NOT NULL +); diff --git a/api/sql/news.sql b/api/sql/news.sql new file mode 100644 index 0000000..dd5a58d --- /dev/null +++ b/api/sql/news.sql @@ -0,0 +1,7 @@ +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 +); diff --git a/api/sql/projects.sql b/api/sql/projects.sql new file mode 100644 index 0000000..951dc1b --- /dev/null +++ b/api/sql/projects.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS projects( + name TEXT NOT NULL UNIQUE, + desc TEXT NOT NULL, + url TEXT NOT NULL, + license TEXT +); diff --git a/api/sql/services.sql b/api/sql/services.sql new file mode 100644 index 0000000..9823da0 --- /dev/null +++ b/api/sql/services.sql @@ -0,0 +1,10 @@ +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 +); 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..d9c63dd --- /dev/null +++ b/api/util/res.go @@ -0,0 +1,79 @@ +package util + +import ( + "net/http" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/ngn13/website/api/config" +) + +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 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/news.xml b/api/views/news.xml new file mode 100644 index 0000000..a5a0a28 --- /dev/null +++ b/api/views/news.xml @@ -0,0 +1,16 @@ + + {{.app.Host}} news + {{.updated}} + News and updates about my projects and self-hosted services + + {{ range .entries }} + + {{.Title}} + {{.RFC3339}} + + {{.Author}} + + {{.Content}} + + {{ end }} + diff --git a/app/.dockerignore b/app/.dockerignore index b02546f..a136c83 100644 --- a/app/.dockerignore +++ b/app/.dockerignore @@ -1,4 +1,4 @@ node_modules .svelte-kit -build public +build 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/Dockerfile b/app/Dockerfile index f757326..9d5af1a 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,23 +1,39 @@ -FROM node:23.5.0 as build +# build the application with node +FROM node:23.5.0 AS build + +ARG WEBSITE_REPORT_URL +ARG WEBSITE_SOURCE_URL +ARG WEBSITE_APP_URL +ARG WEBSITE_API_URL +ARG WEBSITE_DOC_URL + +ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL +ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL +ENV WEBSITE_APP_URL=$WEBSITE_APP_URL +ENV WEBSITE_API_URL=$WEBSITE_API_URL +ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL WORKDIR /app COPY . /app -ARG API_URL -ENV VITE_API_URL_DEV $API_URL - RUN npm install && npm run build -FROM oven/bun:1.1.20 as main +# run it with bun (a lot faster) +FROM oven/bun:latest AS main WORKDIR /app -COPY --from=build /app/build ./build -COPY --from=build /app/package.json ./package.json +COPY --from=build /app/build ./build +COPY --from=build /app/package.json ./package.json COPY --from=build /app/package-lock.json ./package-lock.json -EXPOSE 4173 +RUN useradd runner -r -u 1001 -d /app +RUN chown -R runner:runner /app +USER runner RUN bun install +EXPOSE 7001 + +ENV PORT=7001 CMD ["bun", "build/index.js"] diff --git a/app/Makefile b/app/Makefile new file mode 100644 index 0000000..8ff4f7a --- /dev/null +++ b/app/Makefile @@ -0,0 +1,10 @@ +all: + npm run build + +format: + npm run format + +run: + npm run dev + +.PHONY: format diff --git a/app/package-lock.json b/app/package-lock.json index f4d1e7d..2a50ba2 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,1566 +1,2244 @@ { - "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": "6.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website", + "version": "6.0", + "dependencies": { + "@types/dompurify": "^3.2.0", + "dompurify": "^3.2.3", + "marked": "^15.0.6", + "svelte-i18n": "^4.0.1" + }, + "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==", + "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/@formatjs/ecma402-abstract": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", + "integrity": "sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.5.10", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", + "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.8.tgz", + "integrity": "sha512-hZlLNI3+Lev8IAXuwehLoN7QTKqbx3XXwFW1jh0AdIA9XJdzn9Uzr+2LLBspPm/PX0+NLIfykj/8IKxQqHUcUQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/icu-skeleton-parser": "1.8.12", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz", + "integrity": "sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "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==", + "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==", + "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==", + "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==" + }, + "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==", + "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/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/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==", + "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==", + "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==", + "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==", + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "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/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "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/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "license": "MIT" + }, + "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==", + "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==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "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==" + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz", + "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "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==", + "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==", + "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/intl-messageformat": { + "version": "10.7.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz", + "integrity": "sha512-IB2N1tmI24k2EFH3PWjU7ivJsnWyLwOWOva0jnXFa29WzB6fb0JZ5EMQGu+XN5lDtjHYFo0/UooP67zBwUg7rQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/icu-messageformat-parser": "2.9.8", + "tslib": "2" + } + }, + "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-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "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==", + "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==" + }, + "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==", + "license": "MIT" + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "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==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz", + "integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "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==", + "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==", + "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/svelte-i18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz", + "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", + "license": "MIT", + "dependencies": { + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^10.5.3", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-i18n": "dist/cli.js" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "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==", + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "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==" + } + } } diff --git a/app/package.json b/app/package.json index 2667784..65e2ae0 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": "6.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "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": { + "dompurify": "^3.2.3", + "marked": "^15.0.6", + "svelte-i18n": "^4.0.1" + } } 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/api.js b/app/src/lib/api.js new file mode 100644 index 0000000..1de72b9 --- /dev/null +++ b/app/src/lib/api.js @@ -0,0 +1,37 @@ +import { urljoin } from "$lib/util.js"; + +const api_version = "v1"; +const api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version); + +function api_urljoin(path = null, query = {}) { + return urljoin(api_url, path, query); +} + +function api_check_err(json) { + if (!("error" in json)) throw new Error('API response is missing the "error" key'); + + if (json["error"] != "") throw new Error(`API returned an error: ${json["error"]}`); + + if (!("result" in json)) throw new Error('API response is missing the "result" key'); +} + +async function api_http_get(fetch, url) { + const res = await fetch(url); + const json = await res.json(); + api_check_err(json); + return json["result"]; +} + +async function api_get_metrics(fetch) { + return await api_http_get(fetch, api_urljoin("/metrics")); +} + +async function api_get_services(fetch) { + return await api_http_get(fetch, api_urljoin("/services")); +} + +async function api_get_projects(fetch) { + return await api_http_get(fetch, api_urljoin("/projects")); +} + +export { api_version, api_urljoin, api_get_metrics, api_get_services, api_get_projects }; diff --git a/app/src/lib/card.svelte b/app/src/lib/card.svelte index 5cfb435..d76bd2a 100644 --- a/app/src/lib/card.svelte +++ b/app/src/lib/card.svelte @@ -1,52 +1,50 @@ -
-
- root@ngn.tf:~# {current} -
-
+
+

{title}

+
-
+ diff --git a/app/src/lib/card_link.svelte b/app/src/lib/card_link.svelte deleted file mode 100644 index 2d94d07..0000000 --- a/app/src/lib/card_link.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - - - -
- {current} -
-
- -
-
- - diff --git a/app/src/lib/doc.js b/app/src/lib/doc.js new file mode 100644 index 0000000..c7dc91b --- /dev/null +++ b/app/src/lib/doc.js @@ -0,0 +1,28 @@ +import { urljoin } from "$lib/util.js"; + +function doc_urljoin(path = null, query = {}) { + return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query); +} + +function doc_check_err(json) { + if ("error" in json) throw new Error(`Documentation server returned an error: ${json["error"]}`); +} + +async function doc_http_get(fetch, url) { + const res = await fetch(url); + const json = await res.json(); + doc_check_err(json); + return json; +} + +async function doc_get_list(fetch) { + return await doc_http_get(fetch, doc_urljoin("/list")); +} + +async function doc_get(fetch, name) { + let url = doc_urljoin("/get"); + url = urljoin(url, name); + return await doc_http_get(fetch, url); +} + +export { doc_urljoin, doc_get, doc_get_list }; diff --git a/app/src/lib/error.svelte b/app/src/lib/error.svelte new file mode 100644 index 0000000..728589d --- /dev/null +++ b/app/src/lib/error.svelte @@ -0,0 +1,62 @@ + + +
+

{$_("error.title")}

+ + {#if error === ""} + Unknown error + {:else} + {error} + {/if} + + + {$_("error.report")} + + +
+ + diff --git a/app/src/lib/footer.svelte b/app/src/lib/footer.svelte new file mode 100644 index 0000000..a2412c4 --- /dev/null +++ b/app/src/lib/footer.svelte @@ -0,0 +1,89 @@ + + +
+
+ + + {$_("footer.powered")} + +
+
+ + {$_("footer.number", { + values: { + total: data.total, + since: date_from_ts(data.since), + }, + })} + {#if data.number % 1000 == 0} + ({$_("footer.wow")}) + {/if} + + + {$_("footer.version", { values: { api_version: "v1", frontend_version: pkg.version } })} + +
+
+ + diff --git a/app/src/lib/head.svelte b/app/src/lib/head.svelte new file mode 100644 index 0000000..674a557 --- /dev/null +++ b/app/src/lib/head.svelte @@ -0,0 +1,22 @@ + + + + [ngn.tf] | {title} + + + + + + + + diff --git a/app/src/lib/header.svelte b/app/src/lib/header.svelte index 4dba0d5..15eadd1 100644 --- a/app/src/lib/header.svelte +++ b/app/src/lib/header.svelte @@ -1,41 +1,80 @@
-

- -

-

{subtitle}

+
+

{title.toLowerCase()}

+

_

+
+
diff --git a/app/src/lib/icon.svelte b/app/src/lib/icon.svelte new file mode 100644 index 0000000..d0a6189 --- /dev/null +++ b/app/src/lib/icon.svelte @@ -0,0 +1,6 @@ + + + diff --git a/app/src/lib/link.svelte b/app/src/lib/link.svelte new file mode 100644 index 0000000..751c86e --- /dev/null +++ b/app/src/lib/link.svelte @@ -0,0 +1,37 @@ + + +{#if icon != ""} + +{/if} +{#if highlight} + + + +{:else} + + + +{/if} + + diff --git a/app/src/lib/locale.js b/app/src/lib/locale.js new file mode 100644 index 0000000..d2890e7 --- /dev/null +++ b/app/src/lib/locale.js @@ -0,0 +1,66 @@ +import { init, locale, register, waitLocale } from "svelte-i18n"; +import { browser } from "$app/environment"; +import { get, writable } from "svelte/store"; + +const locale_default = "en"; +let locale_index = writable(0); +let locale_list = []; + +function locale_setup() { + // english + register("en", () => import("../locales/en.json")); + locale_list.push({ code: "en", name: "English", icon: "🇬🇧" }); + + // turkish + register("tr", () => import("../locales/tr.json")); + locale_list.push({ code: "tr", name: "Turkish", icon: "🇹🇷" }); + + init({ + fallbackLocale: locale_default, + initialLocale: get(locale), + }); +} + +function locale_from_browser() { + if (browser) return window.navigator.language.slice(0, 2).toLowerCase(); + else return locale_default; +} + +function locale_select(l = null) { + if (l === null) { + if (browser && null !== (l = localStorage.getItem("locale"))) locale_select(l); + else locale_select(locale_from_browser()); + return; + } + + l = l.slice(0, 2); + + for (let i = 0; i < locale_list.length; i++) { + if (l !== locale_list[i].code) continue; + + if (browser) localStorage.setItem("locale", l); + + locale.set(l); + locale_index.set(i); + + return; + } + + locale.set(locale_default); + locale_index.set(0); +} + +async function locale_wait() { + await waitLocale(); +} + +export { + locale, + locale_list, + locale_index, + locale_default, + locale_setup, + locale_wait, + locale_select, + locale_from_browser, +}; diff --git a/app/src/lib/navbar.svelte b/app/src/lib/navbar.svelte index 88785a0..08550e4 100644 --- a/app/src/lib/navbar.svelte +++ b/app/src/lib/navbar.svelte @@ -1,51 +1,42 @@ -