Compare commits

...

19 commits

Author SHA1 Message Date
dependabot[bot]
b4b13728b0
build(deps-dev): bump eslint and @types/eslint in /frontend
Bumps [eslint](https://github.com/eslint/eslint) and [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint). These dependencies needed to be updated together.

Updates `eslint` from 9.7.0 to 9.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.7.0...v9.10.0)

Updates `@types/eslint` from 8.56.10 to 9.6.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-12 15:36:40 +00:00
ditatombot[bot]
d78eea3da0
Merge pull request #103 from ditatompel/dependabot/npm_and_yarn/frontend/sveltejs/kit-2.5.26
Merge pull request #103

This merge action was created automatically.

Reviewed-by: ditatompel <ditatompel@users.noreply.github.com>
2024-09-12 15:35:33 +00:00
ditatombot[bot]
1c67b09229
Merge pull request #107 from ditatompel/dependabot/npm_and_yarn/frontend/vite-5.4.4
Merge pull request #107

This merge action was created automatically.

Reviewed-by: ditatompel <ditatompel@users.noreply.github.com>
2024-09-12 15:33:33 +00:00
dependabot[bot]
89b90775ee
build(deps-dev): bump vite from 5.4.2 to 5.4.4 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.2 to 5.4.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.4/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-12 15:29:38 +00:00
ditatombot[bot]
5119ee75b1
Merge pull request #100 from ditatompel/dependabot/npm_and_yarn/frontend/svelte-check-4.0.1
Merge pull request #100

This merge action was created automatically.

Reviewed-by: ditatompel <ditatompel@users.noreply.github.com>
2024-09-12 15:28:22 +00:00
ditatombot[bot]
71c8737cff
Merge pull request #99 from ditatompel/dependabot/npm_and_yarn/frontend/postcss-8.4.45
Merge pull request #99

This merge action was created automatically.

Reviewed-by: ditatompel <ditatompel@users.noreply.github.com>
2024-09-12 15:24:03 +00:00
b60a67c8cb
chore: Run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
Run and use your own node! YOU MUST run and use your own node!
2024-09-12 06:02:02 +07:00
9bd609e4dd
chore: Remove dev SQL statement 2024-09-12 05:27:54 +07:00
cdb0816bc3
chore(ui): Replace IPs comma separator with space in table
Also replace IPs comma with comma and space in logs detail
2024-09-12 03:24:18 +07:00
fc172a0bd0
chore(ui): Moving logs link under uptime 2024-09-12 03:16:45 +07:00
7553ad8b45
feat: Display node IP addresses #84 2024-09-12 03:11:17 +07:00
f6b048b017
feat: Record node ip addresses #84
For future use investigations about "suspicious" nodes. #105
2024-09-12 01:13:30 +07:00
0e3dc04af8
fix: Formatting IPv6 display #84
Wraps IPv6 host in square brackets, returns as-is for domain names
or IPv4 addresses.
2024-09-09 19:50:30 +07:00
61cc98e378
feat!: Added IPv6 only information to the table
The IP address information of the remote node is replaced with
information on whether the remote node only supports IPv6 or not.
2024-09-09 19:17:39 +07:00
c3f837e122
feat: Check IP-stack info everytime prober send report #84
This commit add IsIPv6Only function inside `internal/ip` package
and moving `geo` package from `internal/geo` to `internal/ip/geo`.

Although it increases server resource usage, checking hostname to IP is
required every time the prober sends a report so that the `ipv6_only`
record in the database is not up-to-date. Previously, this feature did
not exist.
2024-09-09 18:21:03 +07:00
dependabot[bot]
a7bf9470fb
build(deps-dev): bump @sveltejs/kit from 2.5.25 to 2.5.26 in /frontend
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.5.25 to 2.5.26.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.5.26/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 05:49:15 +00:00
dependabot[bot]
3b9212618b
build(deps-dev): bump svelte-check from 3.8.6 to 4.0.1 in /frontend
Bumps [svelte-check](https://github.com/sveltejs/language-tools) from 3.8.6 to 4.0.1.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-3.8.6...svelte-check-4.0.1)

---
updated-dependencies:
- dependency-name: svelte-check
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 05:48:54 +00:00
dependabot[bot]
6365b8a3c8
build(deps-dev): bump postcss from 8.4.43 to 8.4.45 in /frontend
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.43 to 8.4.45.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.43...8.4.45)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 05:48:49 +00:00
518d4b4335
feat: Added IPv6 nodes support (alpha) #84
This commit accept IPv6 nodes submission.

When user submit new public node, the server will check IP addresses
from given hostname. If host IP addresses doesn't have IPv4, it will
be recorded as "IPv6 only" node.

Probers that support IPv6 may add `IPV6_CAPABLE=true` to the `.env`
file.

Please note that this feature still experimental and may not being
merged to the main branch.
2024-09-06 00:08:59 +07:00
21 changed files with 375 additions and 353 deletions

View file

@ -8,6 +8,7 @@ SERVER_ENDPOINT="http://127.0.0.1:18901"
API_KEY= API_KEY=
ACCEPT_TOR=false ACCEPT_TOR=false
TOR_SOCKS="127.0.0.1:9050" TOR_SOCKS="127.0.0.1:9050"
IPV6_CAPABLE=false
# Server Config # Server Config
# ############# # #############

View file

@ -38,14 +38,14 @@ To build the executable binaries, you need:
- MySQL/MariaDB - MySQL/MariaDB
- [GeoIP Database][geoip_doc] (optional). Place it to `./assets/geoip`, - [GeoIP Database][geoip_doc] (optional). Place it to `./assets/geoip`,
see [./internal/geo/ip.go](./internal/geo/ip.go). see [./internal/ip/geo/geoip.go](./internal/ip/geo/geoip.go).
## Installation ## Installation
### For initial server setup: ### For initial server setup:
1. Download [GeoIP Database][geoip_doc] and place it to `./assets/geoip`. 1. Download [GeoIP Database][geoip_doc] and place it to `./assets/geoip`.
(see [./internal/geo/ip.go](./internal/geo/ip.go)). (see [./internal/ip/geo/geoip.go](./internal/ip/geo/geoip.go)).
2. Pepare your MySQL/MariaDB. 2. Pepare your MySQL/MariaDB.
3. Copy `.env.example` to `.env` and edit it to match with server environment. 3. Copy `.env.example` to `.env` and edit it to match with server environment.
4. Build the binary with `make server` (or `make build` to build both 4. Build the binary with `make server` (or `make build` to build both

View file

@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/ditatompel/xmr-remote-nodes/internal/config" "github.com/ditatompel/xmr-remote-nodes/internal/config"
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
"github.com/ditatompel/xmr-remote-nodes/internal/monero" "github.com/ditatompel/xmr-remote-nodes/internal/monero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -36,20 +37,22 @@ func (err errProber) Error() string {
} }
type proberClient struct { type proberClient struct {
endpoint string // server endpoint endpoint string // server endpoint
apiKey string // prober api key apiKey string // prober api key
acceptTor bool // accept tor acceptTor bool // accept tor
torSOCKS string // IP:Port of tor socks torSOCKS string // IP:Port of tor socks
message string // message to include when reporting back to server acceptIPv6 bool // accept ipv6
message string // message to include when reporting back to server
} }
func newProber() *proberClient { func newProber() *proberClient {
cfg := config.AppCfg() cfg := config.AppCfg()
return &proberClient{ return &proberClient{
endpoint: cfg.ServerEndpoint, endpoint: cfg.ServerEndpoint,
apiKey: cfg.APIKey, apiKey: cfg.APIKey,
acceptTor: cfg.AcceptTor, acceptTor: cfg.AcceptTor,
torSOCKS: cfg.TorSOCKS, torSOCKS: cfg.TorSOCKS,
acceptIPv6: cfg.IPv6Capable,
} }
} }
@ -85,6 +88,10 @@ func (p *proberClient) SetAcceptTor(acceptTor bool) {
p.acceptTor = acceptTor p.acceptTor = acceptTor
} }
func (p *proberClient) SetAcceptIPv6(acceptIPv6 bool) {
p.acceptIPv6 = acceptIPv6
}
// Fetch a new job from the server, fetches node info, and sends it to the server // Fetch a new job from the server, fetches node info, and sends it to the server
func (p *proberClient) Run() error { func (p *proberClient) Run() error {
if err := p.validateConfig(); err != nil { if err := p.validateConfig(); err != nil {
@ -121,20 +128,26 @@ func (p *proberClient) validateConfig() error {
// Get monero node info to fetch from the server // Get monero node info to fetch from the server
func (p *proberClient) fetchJob() (monero.Node, error) { func (p *proberClient) fetchJob() (monero.Node, error) {
queryParams := "" acceptTor := 0
if p.acceptTor { if p.acceptTor {
queryParams = "?accept_tor=1" acceptTor = 1
}
acceptIPv6 := 0
if p.acceptIPv6 {
acceptIPv6 = 1
} }
var node monero.Node var node monero.Node
uri := fmt.Sprintf("%s/api/v1/job%s", p.endpoint, queryParams) uri := fmt.Sprintf("%s/api/v1/job?accept_tor=%d&accept_ipv6=%d", p.endpoint, acceptTor, acceptIPv6)
slog.Info(fmt.Sprintf("[PROBE] Getting node from %s", uri)) slog.Info(fmt.Sprintf("[PROBE] Getting node from %s", uri))
req, err := http.NewRequest(http.MethodGet, uri, nil) req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil { if err != nil {
return node, err return node, err
} }
req.Header.Add(monero.ProberAPIKey, p.apiKey) req.Header.Add(monero.ProberAPIKey, p.apiKey)
req.Header.Set("User-Agent", RPCUserAgent) req.Header.Set("User-Agent", RPCUserAgent)
@ -322,6 +335,13 @@ func (p *proberClient) fetchFee(client http.Client, endpoint string) (uint, erro
} }
func (p *proberClient) reportResult(node monero.Node, tookTime float64) error { func (p *proberClient) reportResult(node monero.Node, tookTime float64) error {
if !node.IsTor {
if hostIps, err := net.LookupIP(node.Hostname); err == nil {
node.IPv6Only = ip.IsIPv6Only(hostIps)
node.IPAddresses = ip.SliceToString(hostIps)
}
}
jsonData, err := json.Marshal(monero.ProbeReport{ jsonData, err := json.Marshal(monero.ProbeReport{
TookTime: tookTime, TookTime: tookTime,
Message: p.message, Message: p.message,

View file

@ -1,35 +1,35 @@
{ {
"name": "xmr-nodes-frontend", "name": "xmr-nodes-frontend",
"version": "v0.0.6", "version": "v0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "xmr-nodes-frontend", "name": "xmr-nodes-frontend",
"version": "v0.0.6", "version": "v0.1.0",
"devDependencies": { "devDependencies": {
"@floating-ui/dom": "^1.6.10", "@floating-ui/dom": "^1.6.10",
"@skeletonlabs/skeleton": "^2.10.2", "@skeletonlabs/skeleton": "^2.10.2",
"@skeletonlabs/tw-plugin": "^0.4.0", "@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-static": "^3.0.4", "@sveltejs/adapter-static": "^3.0.4",
"@sveltejs/kit": "^2.5.25", "@sveltejs/kit": "^2.5.26",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/forms": "^0.5.8", "@tailwindcss/forms": "^0.5.8",
"@types/eslint": "^8.56.0", "@types/eslint": "^9.6.1",
"@vincjo/datatables": "^1.14.10", "@vincjo/datatables": "^1.14.10",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"eslint": "^9.7.0", "eslint": "^9.10.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0", "eslint-plugin-svelte": "^2.43.0",
"postcss": "^8.4.43", "postcss": "^8.4.45",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.6",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"svelte-check": "^3.8.6", "svelte-check": "^4.0.1",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.2" "vite": "^5.4.4"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -462,9 +462,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.17.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
"integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint/object-schema": "^2.1.4", "@eslint/object-schema": "^2.1.4",
@ -499,9 +499,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.7.0", "version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.7.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==", "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -516,6 +516,18 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"dev": true,
"dependencies": {
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz",
@ -949,9 +961,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.5.25", "version": "2.5.26",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.25.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.26.tgz",
"integrity": "sha512-5hBSEN8XEjDZ5+2bHkFh8Z0QyOk0C187cyb12aANe1c8aeKbfu5ZD5XaC2vEH4h0alJFDXPdUkXQBmeeXeMr1A==", "integrity": "sha512-8l1JTIM2L+bS8ebq1E+nGjv/YSKSnD9Q19bYIUkc41vaEG2JjVUx6ikvPIJv2hkQAuqJLzoPrXlKk4KcyWOv3Q==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@ -1038,9 +1050,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "8.56.10", "version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/estree": "*", "@types/estree": "*",
@ -1059,12 +1071,6 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true "dev": true
}, },
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true
},
"node_modules/@vincjo/datatables": { "node_modules/@vincjo/datatables": {
"version": "1.14.10", "version": "1.14.10",
"resolved": "https://registry.npmjs.org/@vincjo/datatables/-/datatables-1.14.10.tgz", "resolved": "https://registry.npmjs.org/@vincjo/datatables/-/datatables-1.14.10.tgz",
@ -1293,15 +1299,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -1537,15 +1534,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/devalue": { "node_modules/devalue": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz",
@ -1582,12 +1570,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "dev": true
}, },
"node_modules/es6-promise": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
"dev": true
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@ -1648,16 +1630,17 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.7.0", "version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.7.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==", "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0", "@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.17.0", "@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.7.0", "@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0", "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@ -1680,7 +1663,6 @@
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
"is-path-inside": "^3.0.3", "is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@ -1696,6 +1678,14 @@
}, },
"funding": { "funding": {
"url": "https://eslint.org/donate" "url": "https://eslint.org/donate"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
} }
}, },
"node_modules/eslint-compat-utils": { "node_modules/eslint-compat-utils": {
@ -2004,12 +1994,6 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2033,27 +2017,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -2090,12 +2053,6 @@
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true "dev": true
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -2161,23 +2118,6 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -2447,15 +2387,6 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/mini-svg-data-uri": { "node_modules/mini-svg-data-uri": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
@ -2477,15 +2408,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -2495,18 +2417,6 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mri": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@ -2608,15 +2518,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -2685,15 +2586,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -2773,9 +2665,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.43", "version": "8.4.45",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
"integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==", "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -3065,19 +2957,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.21.0", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
@ -3148,18 +3027,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/sander": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
"integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==",
"dev": true,
"dependencies": {
"es6-promise": "^3.1.2",
"graceful-fs": "^4.1.3",
"mkdirp": "^0.5.1",
"rimraf": "^2.5.2"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.2", "version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
@ -3225,21 +3092,6 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^0.2.5",
"minimist": "^1.2.0",
"sander": "^0.5.0"
},
"bin": {
"sorcery": "bin/sorcery"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@ -3339,18 +3191,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"dependencies": {
"min-indent": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@ -3481,23 +3321,54 @@
} }
}, },
"node_modules/svelte-check": { "node_modules/svelte-check": {
"version": "3.8.6", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.6.tgz", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.1.tgz",
"integrity": "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==", "integrity": "sha512-AuWnCZdREoOzMhoptHPUUPYUxLNdXSkoZnPnlv19SZJJimRzLmjjZLKsOiRB4AnhgX+56/WSEdvkWXI/q2BSsA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "^0.3.17", "@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^3.4.1", "chokidar": "^3.4.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"sade": "^1.7.4", "sade": "^1.7.4"
"svelte-preprocess": "^5.1.3",
"typescript": "^5.0.3"
}, },
"bin": { "bin": {
"svelte-check": "bin/svelte-check" "svelte-check": "bin/svelte-check"
}, },
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": { "peerDependencies": {
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" "svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-check/node_modules/fdir": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
"integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
"dev": true,
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/svelte-check/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,
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/svelte-eslint-parser": { "node_modules/svelte-eslint-parser": {
@ -3584,68 +3455,6 @@
"svelte": "^3.19.0 || ^4.0.0" "svelte": "^3.19.0 || ^4.0.0"
} }
}, },
"node_modules/svelte-preprocess": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
"integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@types/pug": "^2.0.6",
"detect-indent": "^6.1.0",
"magic-string": "^0.30.5",
"sorcery": "^0.11.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">= 16.0.0"
},
"peerDependencies": {
"@babel/core": "^7.10.2",
"coffeescript": "^2.5.1",
"less": "^3.11.3 || ^4.0.0",
"postcss": "^7 || ^8",
"postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
"pug": "^3.0.0",
"sass": "^1.26.8",
"stylus": "^0.55.0",
"sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",
"svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0",
"typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"coffeescript": {
"optional": true
},
"less": {
"optional": true
},
"postcss": {
"optional": true
},
"postcss-load-config": {
"optional": true
},
"pug": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.10", "version": "3.4.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
@ -3877,13 +3686,13 @@
"dev": true "dev": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.2", "version": "5.4.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.4.tgz",
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "integrity": "sha512-RHFCkULitycHVTtelJ6jQLd+KSAAzOgEYorV32R2q++M6COBjKJR6BxqClwp5sf0XaBDjVMuJ9wnNfyAJwjMkA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"rollup": "^4.20.0" "rollup": "^4.20.0"
}, },
"bin": { "bin": {
@ -4067,12 +3876,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "1.10.2", "version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",

View file

@ -16,24 +16,24 @@
"@skeletonlabs/skeleton": "^2.10.2", "@skeletonlabs/skeleton": "^2.10.2",
"@skeletonlabs/tw-plugin": "^0.4.0", "@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-static": "^3.0.4", "@sveltejs/adapter-static": "^3.0.4",
"@sveltejs/kit": "^2.5.25", "@sveltejs/kit": "^2.5.26",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/forms": "^0.5.8", "@tailwindcss/forms": "^0.5.8",
"@types/eslint": "^8.56.0", "@types/eslint": "^9.6.1",
"@vincjo/datatables": "^1.14.10", "@vincjo/datatables": "^1.14.10",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"eslint": "^9.7.0", "eslint": "^9.10.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0", "eslint-plugin-svelte": "^2.43.0",
"postcss": "^8.4.43", "postcss": "^8.4.45",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.6",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"svelte-check": "^3.8.6", "svelte-check": "^4.0.1",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.2" "vite": "^5.4.4"
}, },
"type": "module" "type": "module"
} }

View file

@ -21,6 +21,7 @@ declare global {
is_tor: boolean; is_tor: boolean;
is_available: boolean; is_available: boolean;
nettype: string; nettype: string;
ip_addresses: string;
} }
interface ApiResponse { interface ApiResponse {

View file

@ -1,19 +1,23 @@
<script> <script>
import { getModalStore } from '@skeletonlabs/skeleton'; import { getModalStore } from '@skeletonlabs/skeleton';
import { formatHostname } from '$lib/utils/strings';
const modalStore = getModalStore(); const modalStore = getModalStore();
/** @type {string} */
export let ip;
/** @type {boolean} */
export let is_tor;
/** @type {string} */
export let hostname;
/** @type {number} */
export let port;
// if (is_tor) { /**
// hostname = hostname.substring(0, 8) + '[...].onion'; * @type {{
// } * is_tor: boolean,
* hostname: string,
* port: number,
* ipv6_only: boolean
* }}
*/
export let is_tor;
export let hostname;
export let port;
export let ipv6_only;
/** @type {string} */
export let ip_addresses;
/** /**
* @param {string} onionAddr * @param {string} onionAddr
@ -33,15 +37,20 @@
{#if is_tor} {#if is_tor}
<button <button
class="max-w-32 truncate text-orange-800 dark:text-orange-300" class="max-w-40 truncate text-orange-800 dark:text-orange-300"
on:click={() => modalAlert(hostname, port)} on:click={() => modalAlert(hostname, port)}
> >
👁 {hostname} 👁 {hostname}
</button><br />.onion:<span class="text-indigo-800 dark:text-indigo-400">{port}</span> </button><br />.onion:<span class="text-indigo-800 dark:text-indigo-400">{port}</span>
<span class="text-gray-700 dark:text-gray-400">(TOR)</span> <span class="text-gray-700 dark:text-gray-400">(TOR)</span>
{:else} {:else}
{hostname}:<span class="text-indigo-800 dark:text-indigo-400">{port}</span> {formatHostname(hostname)}:<span class="text-indigo-800 dark:text-indigo-400">{port}</span><br />
{#if ip !== ''} <div class="max-w-40 text-ellipsis overflow-x-auto md:overflow-hidden hover:overflow-visible">
<br /><span class="text-gray-700 dark:text-gray-400">{ip}</span> <span class="whitespace-break-spaces text-gray-700 dark:text-gray-400"
{/if} >{ip_addresses.replace(/,/g, ' ')}</span
>
{#if ipv6_only}
<span class="text-rose-800 dark:text-rose-400">(IPv6 only)</span>
{/if}
</div>
{/if} {/if}

View file

@ -1,3 +1,26 @@
/**
* Modifies the input string based on whether it is an IPv6 address.
* If the input is an IPv6 address, it wraps it in square brackets `[ ]`.
* Otherwise, it returns the input string as-is (for domain names or
* IPv4 addresses). AND I'M SORRY USING REGEX FOR THIS!
*
* @param {string} hostname
* @returns {string} - The modified string, IPv6 addresses wrapped in `[ ]`.
*/
export const formatHostname = (hostname) => {
// const ipv6Pattern = /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/; // full
// pattern for both full and compressed IPv6 addresses.
// source: https://regex101.com/library/cP9mH9?filterFlavors=dotnet&filterFlavors=javascript&orderBy=RELEVANCE&search=ip
// This may be incorrect, but let's assume it's correct. xD
const ipv6Pattern =
/^(([0-9A-Fa-f]{1,4}:){7})([0-9A-Fa-f]{1,4})$|(([0-9A-Fa-f]{1,4}:){1,6}:)(([0-9A-Fa-f]{1,4}:){0,4})([0-9A-Fa-f]{1,4})$/;
if (ipv6Pattern.test(hostname)) {
return `[${hostname}]`;
}
return hostname;
};
/** /**
* @param {number} bytes * @param {number} bytes
* @param {number} decimals * @param {number} decimals

View file

@ -4,7 +4,7 @@ export async function load() {
meta: { meta: {
title: 'Monero Remote Node', title: 'Monero Remote Node',
description: description:
'A website that helps you monitor your favourite Monero remote nodes, a device on the internet running the Monero software with copy of the Monero blockchain.', 'A website that helps you monitor your favourite Monero remote nodes, but YOU BETTER RUN AND USE YOUR OWN NODE.',
keywords: keywords:
'monero,monero,xmr,monero node,xmrnode,cryptocurrency,monero remote node,monero testnet,monero stagenet' 'monero,monero,xmr,monero node,xmrnode,cryptocurrency,monero remote node,monero testnet,monero stagenet'
}, },

View file

@ -42,7 +42,7 @@
<section id="form-add-monero-node"> <section id="form-add-monero-node">
<div class="section-container text-center"> <div class="section-container text-center">
<p>Enter your Monero node information below (IPv4 host only):</p> <p>Enter your Monero node information below (IPv6 host check is experimental):</p>
<form <form
class="mx-auto w-full max-w-3xl py-2" class="mx-auto w-full max-w-3xl py-2"

View file

@ -4,7 +4,7 @@ export async function load() {
// prettier-ignore // prettier-ignore
meta: { meta: {
title: 'Public Monero Remote Nodes List', title: 'Public Monero Remote Nodes List',
description: 'List of public Monero remote nodes that you can use with your favourite Monero wallet. You can filter by country, protocol, or CORS capable nodes.', description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!",
keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc' keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc'
} }
}; };

View file

@ -79,7 +79,7 @@
<section id="introduction "> <section id="introduction ">
<div class="section-container text-center !max-w-4xl"> <div class="section-container text-center !max-w-4xl">
<p>Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.</p> <p>Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.</p>
<p>Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the <strong>Monero community suggests to always run your own node</strong> to obtain the maximum possible privacy and to help decentralize the network.</p> <p>Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the <strong>Monero community suggests to <span class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p>
</div> </div>
</section> </section>
@ -211,12 +211,12 @@
<tr> <tr>
<td <td
><HostPortCell ><HostPortCell
ip={row.ip} ip_addresses={row.ip_addresses}
is_tor={row.is_tor} is_tor={row.is_tor}
hostname={row.hostname} hostname={row.hostname}
port={row.port} port={row.port}
ipv6_only={row.ipv6_only}
/> />
<a class="anchor" href="/remote-nodes/logs/?node_id={row.id}">[Logs]</a>
</td> </td>
<td><NetTypeCell nettype={row.nettype} height={row.height} /></td> <td><NetTypeCell nettype={row.nettype} height={row.height} /></td>
<td><ProtocolCell protocol={row.protocol} cors={row.cors} /></td> <td><ProtocolCell protocol={row.protocol} cors={row.cors} /></td>
@ -241,7 +241,13 @@
majority_fee={majorityFee[row.nettype]} majority_fee={majorityFee[row.nettype]}
/> />
</td> </td>
<td><UptimeCell uptime={row.uptime} /></td> <td
><UptimeCell uptime={row.uptime} /><br />
<a
class="anchor !text-purple-800 dark:!text-purple-400"
href="/remote-nodes/logs/?node_id={row.id}">[Logs]</a
>
</td>
<td> <td>
{format(row.last_checked * 1000, 'PP HH:mm')}<br /> {format(row.last_checked * 1000, 'PP HH:mm')}<br />
{formatDistance(row.last_checked * 1000, new Date(), { addSuffix: true })} {formatDistance(row.last_checked * 1000, new Date(), { addSuffix: true })}
@ -278,7 +284,12 @@
rel="noopener">still can return high fee only if you about to create a transactions</a rel="noopener">still can return high fee only if you about to create a transactions</a
>. >.
</li> </li>
<li><strong>The best and safest way is running your own node</strong>!</li> <li>
<strong
class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500"
>The best and safest way is running your own node!</strong
>
</li>
<li> <li>
Nodes with 0% uptime within 1 month with more than 300 check attempt will be removed. You Nodes with 0% uptime within 1 month with more than 300 check attempt will be removed. You
can always add your node again latter. can always add your node again latter.

View file

@ -3,7 +3,7 @@
import { format, formatDistance } from 'date-fns'; import { format, formatDistance } from 'date-fns';
import { loadData, loadNodeInfo } from './api-handler'; import { loadData, loadNodeInfo } from './api-handler';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { formatHashes, formatBytes } from '$lib/utils/strings'; import { formatHostname, formatHashes, formatBytes } from '$lib/utils/strings';
import { import {
DtSrRowsPerPage, DtSrRowsPerPage,
DtSrThSort, DtSrThSort,
@ -71,11 +71,11 @@
<tbody> <tbody>
<tr> <tr>
<td class="font-bold">Hostname:Port</td> <td class="font-bold">Hostname:Port</td>
<td>{nodeInfo?.hostname}:{nodeInfo?.port}</td> <td>{formatHostname(nodeInfo?.hostname)}:{nodeInfo?.port}</td>
</tr> </tr>
<tr> <tr>
<td class="font-bold">Public IP</td> <td class="font-bold">Public IP</td>
<td>{nodeInfo?.ip}</td> <td>{nodeInfo?.ip_addresses.replace(/,/g, ', ')}</td>
</tr> </tr>
<tr> <tr>
<td class="font-bold">Net Type</td> <td class="font-bold">Net Type</td>

View file

@ -24,6 +24,7 @@ type App struct {
APIKey string APIKey string
AcceptTor bool AcceptTor bool
TorSOCKS string TorSOCKS string
IPv6Capable bool
} }
func init() { func init() {
@ -65,4 +66,5 @@ func LoadApp() {
app.APIKey = os.Getenv("API_KEY") app.APIKey = os.Getenv("API_KEY")
app.AcceptTor, _ = strconv.ParseBool(os.Getenv("ACCEPT_TOR")) app.AcceptTor, _ = strconv.ParseBool(os.Getenv("ACCEPT_TOR"))
app.TorSOCKS = os.Getenv("TOR_SOCKS") app.TorSOCKS = os.Getenv("TOR_SOCKS")
app.IPv6Capable, _ = strconv.ParseBool(os.Getenv("IPV6_CAPABLE"))
} }

View file

@ -7,7 +7,7 @@ import (
type migrateFn func(*DB) error type migrateFn func(*DB) error
var dbMigrate = [...]migrateFn{v1, v2} var dbMigrate = [...]migrateFn{v1, v2, v3}
func MigrateDb(db *DB) error { func MigrateDb(db *DB) error {
version := getSchemaVersion(db) version := getSchemaVersion(db)
@ -256,3 +256,19 @@ func v2(db *DB) error {
return nil return nil
} }
func v3(db *DB) error {
slog.Debug("[DB] Migrating database schema version 3")
// table: tbl_node
slog.Debug("[DB] Adding additional columns to tbl_node")
_, err := db.Exec(`
ALTER TABLE tbl_node
ADD COLUMN ipv6_only TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER cors_capable,
ADD COLUMN ip_addresses TEXT NOT NULL DEFAULT '' AFTER cors_capable;`)
if err != nil {
return err
}
return nil
}

View file

@ -171,9 +171,10 @@ func Countries(c *fiber.Ctx) error {
// This handler should protected by `CheckProber` middleware. // This handler should protected by `CheckProber` middleware.
func GiveJob(c *fiber.Ctx) error { func GiveJob(c *fiber.Ctx) error {
acceptTor := c.QueryInt("accept_tor", 0) acceptTor := c.QueryInt("accept_tor", 0)
acceptIPv6 := c.QueryInt("accept_ipv6", 0)
moneroRepo := monero.New() moneroRepo := monero.New()
node, err := moneroRepo.GiveJob(acceptTor) node, err := moneroRepo.GiveJob(acceptTor, acceptIPv6)
if err != nil { if err != nil {
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"status": "error", "status": "error",

28
internal/ip/ip.go Normal file
View file

@ -0,0 +1,28 @@
// Package ip provides IP address related functions
package ip
import (
"net"
"strings"
)
// IsIPv6Only returns true if all given IPs are IPv6
func IsIPv6Only(ips []net.IP) bool {
for _, ip := range ips {
if ip.To4() != nil {
return false
}
}
return true
}
// SliceToString converts []net.IP to a string separated by comma.
// If the separator is empty, it defaults to ",".
func SliceToString(ips []net.IP) string {
r := make([]string, len(ips))
for i, j := range ips {
r[i] = j.String()
}
return strings.Join(r, ",")
}

86
internal/ip/ip_test.go Normal file
View file

@ -0,0 +1,86 @@
package ip
import (
"net"
"testing"
)
// Single test: go test ./internal/ip -bench TestIsIPv6Only -benchmem -run=^$ -v
func TestIsIPv6Only(t *testing.T) {
tests := []struct {
name string
ips []net.IP
want bool
}{
{
name: "IPv4",
ips: []net.IP{
net.ParseIP("1.1.1.1"),
},
want: false,
},
{
name: "IPv6",
ips: []net.IP{
net.ParseIP("2606:4700::6810:85e5"),
},
want: true,
},
{
name: "IPv6 and IPv4",
ips: []net.IP{
net.ParseIP("1.1.1.1"),
net.ParseIP("2606:4700::6810:84e5"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsIPv6Only(tt.ips); got != tt.want {
t.Errorf("IsIPv6Only() = %v, want %v", got, tt.want)
}
})
}
}
// Single test: go test ./internal/ip -bench TestSliceToString -benchmem -run=^$ -v
func TestSliceToString(t *testing.T) {
tests := []struct {
name string
ips []net.IP
want string
}{
{
name: "IPv4",
ips: []net.IP{
net.ParseIP("1.1.1.1"),
},
want: "1.1.1.1",
},
{
name: "IPv6",
ips: []net.IP{
net.ParseIP("2606:4700::6810:85e5"),
},
want: "2606:4700::6810:85e5",
},
{
name: "IPv6 and IPv4",
ips: []net.IP{
net.ParseIP("1.1.1.1"),
net.ParseIP("2606:4700::6810:85e5"),
},
want: "1.1.1.1,2606:4700::6810:85e5",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := SliceToString(tt.ips); got != tt.want {
t.Errorf("SliceToString() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -12,7 +12,7 @@ import (
"time" "time"
"github.com/ditatompel/xmr-remote-nodes/internal/database" "github.com/ditatompel/xmr-remote-nodes/internal/database"
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
"github.com/jmoiron/sqlx/types" "github.com/jmoiron/sqlx/types"
) )
@ -54,6 +54,8 @@ type Node struct {
FailedCount uint `json:"failed_count,omitempty" db:"failed_count"` FailedCount uint `json:"failed_count,omitempty" db:"failed_count"`
LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"` LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"`
CORSCapable bool `json:"cors" db:"cors_capable"` CORSCapable bool `json:"cors" db:"cors_capable"`
IPv6Only bool `json:"ipv6_only" db:"ipv6_only"`
IPAddresses string `json:"ip_addresses" db:"ip_addresses"`
} }
// Get node from database by id // Get node from database by id
@ -195,7 +197,10 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
if strings.HasSuffix(hostname, ".onion") { if strings.HasSuffix(hostname, ".onion") {
is_tor = true is_tor = true
} }
ip := ""
ipAddr := ""
ips := ""
ipv6_only := false
if !is_tor { if !is_tor {
hostIps, err := net.LookupIP(hostname) hostIps, err := net.LookupIP(hostname)
@ -203,10 +208,9 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
return err return err
} }
hostIp := hostIps[0].To4() ipv6_only = ip.IsIPv6Only(hostIps)
if hostIp == nil {
return errors.New("Host IP is not IPv4") hostIp := hostIps[0]
}
if hostIp.IsPrivate() { if hostIp.IsPrivate() {
return errors.New("IP address is private") return errors.New("IP address is private")
} }
@ -214,7 +218,8 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
return errors.New("IP address is loopback address") return errors.New("IP address is loopback address")
} }
ip = hostIp.String() ipAddr = hostIp.String()
ips = ip.SliceToString(hostIps)
} }
row, err := r.db.Query(` row, err := r.db.Query(`
@ -248,7 +253,9 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
lon, lon,
date_entered, date_entered,
last_checked, last_checked,
last_check_status last_check_status,
ip_addresses,
ipv6_only
) VALUES ( ) VALUES (
?, ?,
?, ?,
@ -260,6 +267,8 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
?, ?,
?, ?,
?, ?,
?,
?,
? ?
)`, )`,
protocol, protocol,
@ -267,12 +276,14 @@ func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
port, port,
is_tor, is_tor,
"", "",
ip, ipAddr,
0, 0,
0, 0,
time.Now().Unix(), time.Now().Unix(),
0, 0,
string(statusDb)) string(statusDb),
ips,
ipv6_only)
if err != nil { if err != nil {
return err return err
} }

View file

@ -10,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ditatompel/xmr-remote-nodes/internal/geo" "github.com/ditatompel/xmr-remote-nodes/internal/ip/geo"
) )
type QueryLogs struct { type QueryLogs struct {
@ -108,7 +108,7 @@ func (r *moneroRepo) Logs(q QueryLogs) (FetchLogs, error) {
} }
// GiveJob returns node that should be probed for the next time // GiveJob returns node that should be probed for the next time
func (r *moneroRepo) GiveJob(acceptTor int) (Node, error) { func (r *moneroRepo) GiveJob(acceptTor, acceptIPv6 int) (Node, error) {
args := []interface{}{} args := []interface{}{}
wq := []string{} wq := []string{}
where := "" where := ""
@ -117,6 +117,10 @@ func (r *moneroRepo) GiveJob(acceptTor int) (Node, error) {
wq = append(wq, "is_tor = ?") wq = append(wq, "is_tor = ?")
args = append(args, 0) args = append(args, 0)
} }
if acceptIPv6 != 1 {
wq = append(wq, "ipv6_only = ?")
args = append(args, 0)
}
if len(wq) > 0 { if len(wq) > 0 {
where = "WHERE " + strings.Join(wq, " AND ") where = "WHERE " + strings.Join(wq, " AND ")
@ -304,7 +308,9 @@ func (r *moneroRepo) ProcessJob(report ProbeReport, proberId int64) error {
city = ?, city = ?,
last_checked = ?, last_checked = ?,
last_check_status = ?, last_check_status = ?,
cors_capable = ? cors_capable = ?,
ip_addresses = ?,
ipv6_only = ?
WHERE WHERE
id = ?` id = ?`
_, err := r.db.Exec(update, _, err := r.db.Exec(update,
@ -326,6 +332,8 @@ func (r *moneroRepo) ProcessJob(report ProbeReport, proberId int64) error {
now.Unix(), now.Unix(),
statuses, statuses,
report.Node.CORSCapable, report.Node.CORSCapable,
report.Node.IPAddresses,
report.Node.IPv6Only,
report.Node.ID) report.Node.ID)
if err != nil { if err != nil {
slog.Warn(err.Error()) slog.Warn(err.Error())
@ -337,10 +345,12 @@ func (r *moneroRepo) ProcessJob(report ProbeReport, proberId int64) error {
is_available = ?, is_available = ?,
uptime = ?, uptime = ?,
last_checked = ?, last_checked = ?,
last_check_status = ? last_check_status = ?,
ip_addresses = ?,
ipv6_only = ?
WHERE WHERE
id = ?` id = ?`
if _, err := r.db.Exec(u, 0, report.Node.Uptime, now.Unix(), statuses, report.Node.ID); err != nil { if _, err := r.db.Exec(u, 0, report.Node.Uptime, now.Unix(), statuses, report.Node.IPAddresses, report.Node.IPv6Only, report.Node.ID); err != nil {
slog.Warn(err.Error()) slog.Warn(err.Error())
} }
} }