From defc34558de82c91bad9f0b83f2865fc62b7f550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= Date: Fri, 12 Apr 2024 16:32:09 +0200 Subject: [PATCH] Introducing The Minekube Blog (#49) --- .web/docs/.vitepress/config.ts | 12 +- .../.vitepress/theme/components/MeetTeam.vue | 4 +- .../theme/components/positions/JoinUs.vue | 125 +++++++++++++++ .../components/positions/PositionItem.vue | 35 +++++ .../components/positions/ThreeColumns.vue | 30 ++++ .../theme/components/positions/types.ts | 7 + .../theme/components/posts/Article.vue | 79 ++++++++++ .../theme/components/posts/Author.vue | 41 +++++ .../theme/components/posts/Date.vue | 18 +++ .../theme/components/posts/Home.vue | 78 ++++++++++ .../theme/components/posts/Layout.vue | 59 ++++++++ .../theme/components/posts/NotFound.vue | 3 + .../theme/components/posts/genFeed.ts | 59 ++++++++ .../theme/components/posts/posts.data.ts | 50 ++++++ .web/docs/.vitepress/theme/index.ts | 4 + .web/docs/blog/gate-lite.md | 143 ++++++++++++++++++ .web/docs/blog/index.md | 6 + .web/docs/team.md | 2 +- .web/package.json | 1 + .web/yarn.lock | 28 ++++ 20 files changed, 776 insertions(+), 8 deletions(-) create mode 100644 .web/docs/.vitepress/theme/components/positions/JoinUs.vue create mode 100644 .web/docs/.vitepress/theme/components/positions/PositionItem.vue create mode 100644 .web/docs/.vitepress/theme/components/positions/ThreeColumns.vue create mode 100644 .web/docs/.vitepress/theme/components/positions/types.ts create mode 100644 .web/docs/.vitepress/theme/components/posts/Article.vue create mode 100644 .web/docs/.vitepress/theme/components/posts/Author.vue create mode 100644 .web/docs/.vitepress/theme/components/posts/Date.vue create mode 100644 .web/docs/.vitepress/theme/components/posts/Home.vue create mode 100644 .web/docs/.vitepress/theme/components/posts/Layout.vue create mode 100644 .web/docs/.vitepress/theme/components/posts/NotFound.vue create mode 100644 .web/docs/.vitepress/theme/components/posts/genFeed.ts create mode 100644 .web/docs/.vitepress/theme/components/posts/posts.data.ts create mode 100644 .web/docs/blog/gate-lite.md create mode 100644 .web/docs/blog/index.md diff --git a/.web/docs/.vitepress/config.ts b/.web/docs/.vitepress/config.ts index 77173d9..f12a472 100644 --- a/.web/docs/.vitepress/config.ts +++ b/.web/docs/.vitepress/config.ts @@ -2,8 +2,9 @@ import {defineConfig} from 'vitepress' import {discordLink, editLink, gitHubLink, projects} from '../shared' import {additionalTitle, commitRef} from "../shared/cloudflare"; +import {genFeed} from "./theme/components/posts/genFeed"; -const ogUrl = 'https://connect.minekube.com' +export const ogUrl = 'https://connect.minekube.com' const ogImage = `${ogUrl}/og-image.png` const ogTitle = 'Minekube Connect' const ogDescription = 'The Ingress Tunnel for Minecraft Servers' @@ -46,6 +47,8 @@ export default defineConfig({ reactivityTransform: true }, + buildEnd: genFeed, + themeConfig: { logo: '/minekube-logo.png', @@ -85,10 +88,9 @@ export default defineConfig({ nav: [ {text: 'Quick Start', link: '/guide/quick-start'}, - {text: 'Connectors', link: '/guide/connectors/'}, - {text: 'Downloads', link: '/guide/connectors/plugin#downloading-the-connect-plugin'}, - {text: 'Pricing', link: '/plans'}, - {text: 'Changelog', link: '/guide/changelog'}, + {text: 'Connectors', link: '/guide/connectors/', activeMatch: '^/guide/connectors/'}, + {text: 'Blog', link: '/blog/', activeMatch: '^/blog/'}, + {text: 'Plans', link: '/plans'}, ...projects, ], diff --git a/.web/docs/.vitepress/theme/components/MeetTeam.vue b/.web/docs/.vitepress/theme/components/MeetTeam.vue index 5e1d055..d2727c3 100644 --- a/.web/docs/.vitepress/theme/components/MeetTeam.vue +++ b/.web/docs/.vitepress/theme/components/MeetTeam.vue @@ -15,7 +15,7 @@ const teamSvg = ` ` -const core = [ +const core: DefaultTheme.TeamMember[] = [ { avatar: 'https://www.github.com/minekube.png', name: 'Join the Core Team!', @@ -39,7 +39,7 @@ const core = [ {icon: 'github', link: 'https://github.com/robinbraemer'}, ], }, -] satisfies DefaultTheme.TeamMember[] +] diff --git a/.web/docs/.vitepress/theme/components/positions/JoinUs.vue b/.web/docs/.vitepress/theme/components/positions/JoinUs.vue new file mode 100644 index 0000000..4508bfc --- /dev/null +++ b/.web/docs/.vitepress/theme/components/positions/JoinUs.vue @@ -0,0 +1,125 @@ + + + \ No newline at end of file diff --git a/.web/docs/.vitepress/theme/components/positions/PositionItem.vue b/.web/docs/.vitepress/theme/components/positions/PositionItem.vue new file mode 100644 index 0000000..a4023fe --- /dev/null +++ b/.web/docs/.vitepress/theme/components/positions/PositionItem.vue @@ -0,0 +1,35 @@ + + + diff --git a/.web/docs/.vitepress/theme/components/positions/ThreeColumns.vue b/.web/docs/.vitepress/theme/components/positions/ThreeColumns.vue new file mode 100644 index 0000000..835c78f --- /dev/null +++ b/.web/docs/.vitepress/theme/components/positions/ThreeColumns.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/.web/docs/.vitepress/theme/components/positions/types.ts b/.web/docs/.vitepress/theme/components/positions/types.ts new file mode 100644 index 0000000..680406c --- /dev/null +++ b/.web/docs/.vitepress/theme/components/positions/types.ts @@ -0,0 +1,7 @@ +export type Position = { + role: string; + description: string; + salary?: string; + location: string; + href?: string; +} \ No newline at end of file diff --git a/.web/docs/.vitepress/theme/components/posts/Article.vue b/.web/docs/.vitepress/theme/components/posts/Article.vue new file mode 100644 index 0000000..4d22cb3 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/Article.vue @@ -0,0 +1,79 @@ + + + diff --git a/.web/docs/.vitepress/theme/components/posts/Author.vue b/.web/docs/.vitepress/theme/components/posts/Author.vue new file mode 100644 index 0000000..d5ece38 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/Author.vue @@ -0,0 +1,41 @@ + + + diff --git a/.web/docs/.vitepress/theme/components/posts/Date.vue b/.web/docs/.vitepress/theme/components/posts/Date.vue new file mode 100644 index 0000000..e602f7a --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/Date.vue @@ -0,0 +1,18 @@ + + + diff --git a/.web/docs/.vitepress/theme/components/posts/Home.vue b/.web/docs/.vitepress/theme/components/posts/Home.vue new file mode 100644 index 0000000..18495f2 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/Home.vue @@ -0,0 +1,78 @@ + + + diff --git a/.web/docs/.vitepress/theme/components/posts/Layout.vue b/.web/docs/.vitepress/theme/components/posts/Layout.vue new file mode 100644 index 0000000..69a6240 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/Layout.vue @@ -0,0 +1,59 @@ + + + diff --git a/.web/docs/.vitepress/theme/components/posts/NotFound.vue b/.web/docs/.vitepress/theme/components/posts/NotFound.vue new file mode 100644 index 0000000..a1d7496 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/NotFound.vue @@ -0,0 +1,3 @@ + diff --git a/.web/docs/.vitepress/theme/components/posts/genFeed.ts b/.web/docs/.vitepress/theme/components/posts/genFeed.ts new file mode 100644 index 0000000..3dce066 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/genFeed.ts @@ -0,0 +1,59 @@ +import path from 'path' +import { writeFileSync } from 'fs' +import { Feed } from 'feed' +import { createContentLoader, type SiteConfig } from 'vitepress' +import {Post} from "./posts.data"; +import {ogUrl} from "../../../config"; + +const baseUrl = ogUrl + +export async function genFeed(config: SiteConfig) { + const feed = new Feed({ + title: 'The Minekube Blog', + description: 'The official blog for the Minekube platform', + id: baseUrl, + link: baseUrl, + language: 'en', + image: `${baseUrl}/minekube-logo.png`, + favicon: `${baseUrl}/favicon.png`, + copyright: + 'Copyright (c) 2021-present, Yuxi (Evan) You and blog contributors' + }) + + const posts = (await createContentLoader('blog/*.md', { + excerpt: true, + render: true, + transform(raw): Post[] { + return raw.filter(({ url }) => !url.endsWith('/')) // Exclude 'index.md' + } + }).load()) + console.log(posts) + + posts.sort( + (a, b) => + +new Date(b.frontmatter.date as string) - + +new Date(a.frontmatter.date as string) + ) + + for (const { url, excerpt, frontmatter, html } of posts) { + console.log(html) + feed.addItem({ + title: frontmatter.title, + id: `${baseUrl}${url}`, + link: `${baseUrl}${url}`, + description: excerpt, + content: html?.replaceAll('​', ''), + author: [ + { + name: frontmatter.author, + link: frontmatter.twitter + ? `https://twitter.com/${frontmatter.twitter}` + : undefined + } + ], + date: frontmatter.date + }) + } + + writeFileSync(path.join(config.outDir, 'feed.rss'), feed.rss2()) +} diff --git a/.web/docs/.vitepress/theme/components/posts/posts.data.ts b/.web/docs/.vitepress/theme/components/posts/posts.data.ts new file mode 100644 index 0000000..fc0c959 --- /dev/null +++ b/.web/docs/.vitepress/theme/components/posts/posts.data.ts @@ -0,0 +1,50 @@ +import { createContentLoader } from 'vitepress' + +export interface Post { + title: string + url: string + imageUrl: string + date: { + time: number + string: string + } + excerpt: string | undefined, + category: string, + author: { + name: string, + role: string, + href: string + imageUrl: string + } +} + +declare const data: Post[] +export { data } + +export default createContentLoader('blog/*.md', { + excerpt: true, + transform(raw): Post[] { + return raw + .filter(({ url }) => !url.endsWith('/')) // Exclude 'index.md' + .map(({ url, frontmatter, excerpt }) => ({ + ...frontmatter, + url, + excerpt, + date: formatDate(frontmatter.date), + })) + .sort((a, b) => b.date.time - a.date.time) + } +}) + +function formatDate(raw: string): Post['date'] { + const date = new Date(raw) + date.setUTCHours(12) + return { + time: +date, + string: date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) + } +} \ No newline at end of file diff --git a/.web/docs/.vitepress/theme/index.ts b/.web/docs/.vitepress/theme/index.ts index 03e350a..66d6929 100644 --- a/.web/docs/.vitepress/theme/index.ts +++ b/.web/docs/.vitepress/theme/index.ts @@ -6,6 +6,8 @@ import VPBadge from 'vitepress/dist/client/theme-default/components/VPBadge.vue' import MeetTeam from "./components/MeetTeam.vue"; import Layout from "./components/Layout.vue"; import PlansLanding from "./components/plans/PlansLanding.vue"; +import PostLayout from "./components/posts/Layout.vue"; +import PostHome from "./components/posts/Home.vue"; export default { extends: DefaultTheme, @@ -15,5 +17,7 @@ export default { app.component('VPBadge', VPBadge) app.component('MeetTeam', MeetTeam) app.component('PlansLanding', PlansLanding) + app.component('PostHome', PostHome) + app.component('Post', PostLayout) } } satisfies Theme diff --git a/.web/docs/blog/gate-lite.md b/.web/docs/blog/gate-lite.md new file mode 100644 index 0000000..fe130f1 --- /dev/null +++ b/.web/docs/blog/gate-lite.md @@ -0,0 +1,143 @@ +--- +layout: Post +title: 'How Gate Lite adds an extra layer of security to your Minecraft server or network' +category: Engineering +date: 2024-04-13 +imageUrl: 'https://images.playground.com/6f527647f6ed480b92df1213fceeaaaf.jpeg' +author: + name: Benjamin (NixNux123) + role: Moderator + href: 'https://github.com/NixNux123' + imageUrl: 'https://github.com/NixNux123.png' +--- + +Compared to classic Gate, Velocity or BungeeCord - Gate Lite, a mode of the [Minekube Gate proxy](https://gate.minekube.com/), acts as a lightweight reverse proxy for Minecraft Java. Unlike Gate, Velocity, or Bungeecord, it doesn't offer features like combining multiple Minecraft servers to a network or implementing network-wide features. Gate Lite sits in front of these proxies and simply forwards incoming connections to them. In addition, it offers features like an offline MOTD, several security enhancements, and the ability to handle multiple Minecraft networks behind one port. + +## Introduction + +Gate is an excellent alternative to Velocity, and the Connect network is actually based on it. Trust us, you won't regret it! You'll love the benefits of using Gate and Gate Lite, as it can help protect your server or network. + +#### And how does the routing work? + +Gate Lite detects the hostname you join with and uses it to determine the appropriate route. This allows you to use multiple domains for different backends while only requiring one port. + +Gate Lite searches the list of routes from top to bottom. It also supports wildcards with `*` (which matches any number of characters) and `?` (which matches any single character). Multiple hostnames per backend are also allowed with `[ "abc.example.com", "def.example.com" ]`. + +Gate Lite ensures that the configured backend is online before forwarding the connection. This approach guarantees that the user's request is always directed to an available backend. In case multiple backends are configured for the route, Gate Lite randomly selects one and checks if it is online. If the selected backend is offline, Gate Lite repeats this process until an online backend is found or every backend has been tested. + +**Example of Gate Lite Routing:** + +![grafik](https://gate.minekube.com/assets/lite-mermaid-diagram-LR.nwReuafr.svg) + +The configuration for this might look something like this: +```yaml +config: + lite: + enabled: true + routes: + - host: abc.example.com + backend: 10.0.0.3:25568 + - host: '*.example.com' + backend: 10.0.0.1:25567 + - host: [ example.com, localhost ] + backend: [ 10.0.0.2:25566 ] +``` + +#### Technical explanation + +When Minecraft Java sends its [handshake packet](https://wiki.vg/Protocol#Handshaking), it includes the server address, protocol version, and port. Gate Lite is able to decode this packet and extract the server address field, which it then uses to locate the corresponding route in the configuration. This process is made simple and efficient thanks to Gate Lite's architecture. + +Gate Lite's unique approach is that it ensures that the connection speaks the Minecraft protocol before forwarding it. This adds an extra layer of security, as it prevents malicious traffic from reaching the backend server. + +In addition, Gate Lite is significantly faster than other proxies when piping the connection through. This is because it operates on the application layer of the network, and it only forwards connections that adhere to the Minecraft protocol. This results in a more efficient and streamlined process for the users. Gate Lite reduces the number of TCP connections required for ping requests by caching the motd and server icon. By caching the motd and server icon, only one TCP connection to the backend is needed every X minutes to provide the motd for thousands of ping requests. This approach optimizes the performance of the backend by reducing the load on it. + +As I mentioned earlier, this technique can help prevent [layer 7](https://en.wikipedia.org/wiki/OSI_model) flooding attacks, such as motd-spam. + +[Learn more about Gate Lite](https://gate.minekube.com/guide/lite.html) + +## How Gate Lite protects you from unknown hostnames + +**To achieve this, it is crucial to avoid having a route with `*` in your configuration.** + +Rest assured that Gate Lite prevents connections from unconfigured hostnames or domains, and only allows connections if a suitable route is found in the configuration. Gate Lite ensures that only connections from explicitly allowed hostnames and domains are allowed, protecting you from offensive ones. Don't give the competition a chance to make you look bad with strange domains 😉. + +## What can Gate Lite do against DoS and DDoS attacks? + +Gate Lite can do several things to reduce the impact of an attack. In addition to the extremely low resource usage, which I will discuss later, Gate Lite offers the following features: + +### Rate Limits + +Gate provides two rate limits, a connection rate limit and a login rate limit. + +**Connection limit:** The connection limit is triggered for every new connection, regardless of whether a route is found. When the rate limit is reached, the `/24 subnet` of the IP is blocked for a short time. +The login limit is the same as the connection limit, but it is placed just before the player authenticates to Mojang to prevent flooding the Mojang API. + +**In Lite mode, only the Connection limit works since authentication is handled by the backend.** + +### Caching + +Gate Lite caches the MOTD and server icon of the backends for a configured time. When you request a MOTD, Gate Lite will respond from its cache, which reduces the load on the backend. This feature helps prevent attacks that aim to make your server unavailable through mass MOTD requests. + +**Technical explanation:** +This results in a more efficient and streamlined process for the users. Gate Lite reduces the number of TCP connections required for ping requests by caching the motd and server icon. By caching the motd and server icon, only one TCP connection to the backend is needed every X minutes to provide the motd for thousands of ping requests. This approach optimizes the performance of the backend by reducing the load on it. +As I mentioned earlier, this technique can help prevent [layer 7](https://en.wikipedia.org/wiki/OSI_model) flooding attacks, such as motd-spam. + +### Limited Layer 3 and 4 protection + +Gate Lite operates on the [application layer](https://en.wikipedia.org/wiki/OSI_model) of the network, providing protection for the backend server against attacks on the [network and transport layers](https://en.wikipedia.org/wiki/OSI_model). Gate Lite operates on the application layer of the network, providing protection for the backend server against attacks on the network and transport layers. This ensures that your server is secure and your data is safe. + +Attacks against Minecraft servers and networks are most commonly aimed at layers 3 and 4, as they are relatively simple to execute and can be used against almost any other internet service. These attacks aim to consume the server's bandwidth. However, if the server running Gate Lite has sufficient bandwidth and a functional layer 3 and 4 DDoS protection, it becomes challenging to attack the backend server. More information will be provided later. + +**To ensure security, we recommend that access to the backend server be limited to Gate Lite only.** + +**Technical explanation:** +Gate Lite operates on [layer 7 of the OSI model](https://en.wikipedia.org/wiki/OSI_model) and routes connections based on the hostname or domain in the [handshake packet](https://wiki.vg/Protocol#Handshaking). If the incoming connection is only on layer 4 and the handshake packet doesn't exist, Gate Lite will drop the connection. If the incoming connection is only on layer 4 and the handshake packet doesn't exist, Gate Lite will drop the connection. However, this ensures that only valid connections reach the backend, improving security and efficiency. + +As I mentioned earlier, the most common attacks are aimed at layers 3 and 4, with the goal of overwhelming the victim's bandwidth. However, if the server running Gate Lite has functional layer 3 and 4 attack protection, it can reduce incoming bandwidth and prevent it from filling up the server's available bandwidth. This means that your backend servers will be well protected against such attacks, even if they lack adequate protection. Gate Lite is an excellent choice for servers with limited resources. You can rest assured that it will perform effectively, even on less powerful servers. We will provide more information on this soon. + +#### DDoS protection services + +To effectively prevent network capacity attacks on your server, it is essential to have a reliable DDoS protection service. Fortunately, there are many providers to choose from. In this section, we will provide a brief comparison of some well-known providers, including Minekube Connect. + +Please keep in mind that this is a basic comparison and for more technical details, we recommend visiting the provider's websites. + +![table](https://hackmd.io/_uploads/S1-qtcRkR.png) + +## How powerful are Gate and Gate Lite? + +Gate is written in Go, a modern programming language designed for cloud development. It requires only a few megabytes of RAM and some CPU to run. Gate Lite requires even fewer resources because it only pipes bytes. + +**Technical explanation:** +Gate is a great choice for those who want to optimize their resource usage. Thanks to its Go-based architecture, Go compiles programs into executable files, similar to C or Rust, which can be run directly. This means that Gate uses fewer resources than Java, which compiles its programs to run on the [Java Virtual Machine (JVM)](https://en.wikipedia.org/wiki/Java_virtual_machine). While the JVM makes it easier to develop software that can run on any machine that supports it, it also requires more resources. By choosing Gate, you can ensure that your software runs efficiently without sacrificing functionality. + +### Efficiency during an attack + +Minekube's Connect Network is based on a modified version of Gate. During a layer 7 bot attack against one of the endpoints, the memory and CPU usage wasn't much higher than normal: + +![grafik](https://hackmd.io/_uploads/S1EDkcFJA.png) + +### Efficiency compared to other Minecraft proxies + +Compared to other proxies such as Velocity, which are already well optimized, Gate uses fewer resources even with many players. This not only reduces server costs but also improves the user experience by eliminating lag during scaling. + +![grafik](https://hackmd.io/_uploads/S1Zs19tyA.png) + +### Gate and Gate Lite works almost anywhere + +Gate and Gate Lite can run on a wide range of systems due to their low resource usage. They are available as an executable file, Docker image, and Kubernetes-ready, making them suitable for small VPS instances as well as large servers that support thousands of players. + +[Learn more about running Gate](https://gate.minekube.com/guide/quick-start.html) + +## How to get the benefits of Gate Lite without having to worry about it + +If you're not interested in hosting Gate Lite yourself or lack experience, don't worry! You can use Minekube Connect instead. Connect offers all the benefits of Gate Lite, plus DDoS protection, and it's completely free. Integrating Connect into your existing server/network is straightforward and easy. + +### But what is Connect? + +Connect is a global edge network behind which anyone can connect their Minecraft server or network for free. Every server gets its own anycast domain. Connect uses tunnels to connect players to each server. There is no need for port forwarding. + +**Check out [Minekube Connect](https://connect.minekube.com/) today and get all the advantages!** + +--- + +**If you have any further questions about Gate, Gate Lite or Connect, feel free to join the [Minekube Discord](https://minekube.com/discord)!** \ No newline at end of file diff --git a/.web/docs/blog/index.md b/.web/docs/blog/index.md new file mode 100644 index 0000000..ba66e13 --- /dev/null +++ b/.web/docs/blog/index.md @@ -0,0 +1,6 @@ +--- +layout: PostHome +title: From the Minekube Blog +subtext: Read the latest news and updates from the Minekube team. +--- + diff --git a/.web/docs/team.md b/.web/docs/team.md index 23f5584..4f5afbd 100644 --- a/.web/docs/team.md +++ b/.web/docs/team.md @@ -1,7 +1,7 @@ --- layout: page title: Join the Team -description: The development of Vite is guided by an international team. +description: The development of Connect is guided by an international team. --- diff --git a/.web/package.json b/.web/package.json index af9c564..1f08910 100644 --- a/.web/package.json +++ b/.web/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "autoprefixer": "^10.4.13", + "feed": "^4.2.2", "postcss": "^8.4.35", "tailwindcss": "^3.2.4", "vitepress": "^1.0.0-rc.44", diff --git a/.web/yarn.lock b/.web/yarn.lock index 0f5a565..457c43f 100644 --- a/.web/yarn.lock +++ b/.web/yarn.lock @@ -1262,6 +1262,7 @@ __metadata: resolution: "connect-minekube-docs@workspace:." dependencies: autoprefixer: "npm:^10.4.13" + feed: "npm:^4.2.2" postcss: "npm:^8.4.35" posthog-js: "npm:^1.93.3" tailwindcss: "npm:^3.2.4" @@ -1508,6 +1509,15 @@ __metadata: languageName: node linkType: hard +"feed@npm:^4.2.2": + version: 4.2.2 + resolution: "feed@npm:4.2.2" + dependencies: + xml-js: "npm:^1.6.11" + checksum: 10c0/c0849bde569da94493224525db00614fd1855a5d7c2e990f6e8637bd0298e85c3d329efe476cba77e711e438c3fb48af60cd5ef0c409da5bcd1f479790b0a372 + languageName: node + linkType: hard + "fflate@npm:^0.4.1": version: 0.4.8 resolution: "fflate@npm:0.4.8" @@ -2482,6 +2492,13 @@ __metadata: languageName: node linkType: hard +"sax@npm:^1.2.4": + version: 1.3.0 + resolution: "sax@npm:1.3.0" + checksum: 10c0/599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea + languageName: node + linkType: hard + "semver@npm:^7.3.5": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -2939,6 +2956,17 @@ __metadata: languageName: node linkType: hard +"xml-js@npm:^1.6.11": + version: 1.6.11 + resolution: "xml-js@npm:1.6.11" + dependencies: + sax: "npm:^1.2.4" + bin: + xml-js: ./bin/cli.js + checksum: 10c0/c83631057f10bf90ea785cee434a8a1a0030c7314fe737ad9bf568a281083b565b28b14c9e9ba82f11fc9dc582a3a907904956af60beb725be1c9ad4b030bc5a + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0"