From b6983e68666e6768ef24a0011c2a07f3352448c9 Mon Sep 17 00:00:00 2001 From: da <> Date: Mon, 15 Jan 2024 22:06:46 +0100 Subject: [PATCH 001/378] add ntfy-client.service as user service --- .goreleaser.yml | 3 +++ client/user/ntfy-client.service | 10 +++++++ docs/subscribe/cli.md | 47 +++++++++------------------------ scripts/postinst.sh | 11 +++++++- 4 files changed, 35 insertions(+), 36 deletions(-) create mode 100644 client/user/ntfy-client.service diff --git a/.goreleaser.yml b/.goreleaser.yml index 062cce1f..469b7ee6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -90,6 +90,8 @@ nfpms: type: "config|noreplace" - src: client/ntfy-client.service dst: /lib/systemd/system/ntfy-client.service + - src: client/user/ntfy-client.service + dst: /lib/systemd/user/ntfy-client.service - dst: /var/cache/ntfy type: dir - dst: /var/cache/ntfy/attachments @@ -119,6 +121,7 @@ archives: - server/ntfy.service - client/client.yml - client/ntfy-client.service + - client/user/ntfy-client.service - id: ntfy_windows builds: diff --git a/client/user/ntfy-client.service b/client/user/ntfy-client.service new file mode 100644 index 00000000..0a9598ee --- /dev/null +++ b/client/user/ntfy-client.service @@ -0,0 +1,10 @@ +[Unit] +Description=ntfy client +After=network.target + +[Service] +ExecStart=/usr/bin/ntfy subscribe --config "%h/.config/ntfy/client.yml" --from-config +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 7f589d3c..ed8e2791 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -263,43 +263,20 @@ will be used, otherwise, the subscription settings will override the defaults. require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password. ### Using the systemd service -You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service)) -to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started) -if you install the deb/rpm package. To configure it, simply edit `/etc/ntfy/client.yml` and run `sudo systemctl restart ntfy-client`. +You can use the `ntfy-client` systemd services to subscribe to multiple topics just like in the example above. +You have the option of either enabling `ntfy-client` as a system service (see +[here](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service)) +or user service (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/user/ntfy-client.service)). +The services are automatically installed (but not started) if you install the deb/rpm/AUR package. +The system service ensures that ntfy is run at startup (useful for servers), +while the user service starts ntfy only after the user has logged in. The user service is recommended for personal machine use. + +To configure `ntfy-client` as a system service it, edit `/etc/ntfy/client.yml` and run `sudo systemctl restart ntfy-client`. + +To configure `ntfy-client` as a user service it, edit `~/.config/ntfy/client.yml` and run `systemctl --user restart ntfy-client` (without sudo). !!! info - The `ntfy-client.service` runs as user `ntfy`, meaning that typical Linux permission restrictions apply. See below - for how to fix this. - -If the service runs on your personal desktop machine, you may want to override the service user/group (`User=` and `Group=`), and -adjust the `DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` environment variables. This will allow you to run commands in your X session -as the primary machine user. - -You can either manually override these systemd service entries with `sudo systemctl edit ntfy-client`, and add this -(assuming your user is `phil`). Don't forget to run `sudo systemctl daemon-reload` and `sudo systemctl restart ntfy-client` -after editing the service file: - -=== "/etc/systemd/system/ntfy-client.service.d/override.conf" - ``` - [Service] - User=phil - Group=phil - Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" - ``` -Or you can run the following script that creates this override config for you: - -``` -sudo sh -c 'cat > /etc/systemd/system/ntfy-client.service.d/override.conf' </dev/null || true + systemctl --user daemon-reload >/dev/null || true if systemctl is-active -q ntfy.service; then echo "Restarting ntfy.service ..." if [ -x /usr/bin/deb-systemd-invoke ]; then @@ -33,12 +34,20 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then fi fi if systemctl is-active -q ntfy-client.service; then - echo "Restarting ntfy-client.service ..." + echo "Restarting ntfy-client.service (system) ..." if [ -x /usr/bin/deb-systemd-invoke ]; then deb-systemd-invoke try-restart ntfy-client.service >/dev/null || true else systemctl restart ntfy-client.service >/dev/null || true fi fi + if systemctl --user is-active -q ntfy-client.service; then + echo "Restarting ntfy-client.service (user)..." + if [ -x /usr/bin/deb-systemd-invoke ]; then + deb-systemd-invoke --user try-restart ntfy-client.service >/dev/null || true + else + systemctl --user restart ntfy-client.service >/dev/null || true + fi + fi fi fi From 5211d06f2cf6df049942edce655fed7fb9f94521 Mon Sep 17 00:00:00 2001 From: stendler Date: Wed, 29 May 2024 21:23:06 +0200 Subject: [PATCH 002/378] feat(server): add Cache and Firebase as keys to JSON publishing https://github.com/binwiederhier/ntfy/issues/1119 --- server/server.go | 6 ++++++ server/server_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++ server/types.go | 2 ++ 3 files changed, 53 insertions(+) diff --git a/server/server.go b/server/server.go index eb0fd120..eb4e5504 100644 --- a/server/server.go +++ b/server/server.go @@ -1862,6 +1862,12 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Call != "" { r.Header.Set("X-Call", m.Call) } + if m.Cache != "" { + r.Header.Set("X-Cache", m.Cache) + } + if m.Firebase != "" { + r.Header.Set("X-Firebase", m.Firebase) + } return next(w, r, v) } } diff --git a/server/server_test.go b/server/server_test.go index ef9157cb..460b6deb 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -84,6 +84,22 @@ func TestServer_PublishWithFirebase(t *testing.T) { require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) } +func TestServer_PublishWithoutFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + response := request(t, s, "PUT", "/mytopic", "my first message", map[string]string{ + "firebase": "no", + }) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 0, len(sender.Messages())) +} + func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *testing.T) { // This tests issue #641, which used to panic before the fix @@ -1669,6 +1685,35 @@ func TestServer_PublishAsJSON_WithActions(t *testing.T) { require.Equal(t, "target_temp_f=65", m.Actions[1].Body) } +func TestServer_PublishAsJSON_NoCache(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic","message": "this message is not cached","cache":"no"}` + response := request(t, s, "PUT", "/", body, nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "this message is not cached", msg.Message) + require.Equal(t, int64(0), msg.Expires) + + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Empty(t, messages) +} + +func TestServer_PublishAsJSON_WithoutFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + body := `{"topic":"mytopic","message": "my first message","firebase":"no"}` + response := request(t, s, "PUT", "/", body, nil) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 0, len(sender.Messages())) +} + func TestServer_PublishAsJSON_Invalid(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic":"mytopic",INVALID` diff --git a/server/types.go b/server/types.go index fb08fb05..b26df1a3 100644 --- a/server/types.go +++ b/server/types.go @@ -105,6 +105,8 @@ type publishMessage struct { Filename string `json:"filename"` Email string `json:"email"` Call string `json:"call"` + Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead) + Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead) Delay string `json:"delay"` } From 49a548252c449282d8788ebf5e448f5e67b2e531 Mon Sep 17 00:00:00 2001 From: Nogweii Date: Tue, 25 Jun 2024 22:58:36 -0700 Subject: [PATCH 003/378] teach `ntfy webpush` to write the keys to a file --- .gitignore | 1 + cmd/webpush.go | 23 ++++++++++++++++++++++- cmd/webpush_test.go | 7 +++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7cbb52ac..cf10bc33 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ node_modules/ __pycache__ web/dev-dist/ venv/ +cmd/key-file.yaml diff --git a/cmd/webpush.go b/cmd/webpush.go index ec66f083..bd44f5aa 100644 --- a/cmd/webpush.go +++ b/cmd/webpush.go @@ -4,9 +4,16 @@ package cmd import ( "fmt" + "os" "github.com/SherClockHolmes/webpush-go" "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +var flagsWebpush = append( + []cli.Flag{}, + altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"f"}, Usage: "write vapid keys to this file"}), ) func init() { @@ -26,6 +33,7 @@ var cmdWebPush = &cli.Command{ Usage: "Generate VAPID keys to enable browser background push notifications", UsageText: "ntfy webpush keys", Category: categoryServer, + Flags: flagsWebpush, }, }, } @@ -35,7 +43,19 @@ func generateWebPushKeys(c *cli.Context) error { if err != nil { return err } - _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: + + if keyFile := c.String("key-file"); keyFile != "" { + contents := fmt.Sprintf(`--- +web-push-public-key: %s +web-push-private-key: %s +`, publicKey, privateKey) + err = os.WriteFile(keyFile, []byte(contents), 0660) + if err != nil { + return err + } + _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys written to %s.`, keyFile) + } else { + _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: web-push-public-key: %s web-push-private-key: %s @@ -44,5 +64,6 @@ web-push-email-address: See https://ntfy.sh/docs/config/#web-push for details. `, publicKey, privateKey) + } return err } diff --git a/cmd/webpush_test.go b/cmd/webpush_test.go index 51926ca1..c2f19f6f 100644 --- a/cmd/webpush_test.go +++ b/cmd/webpush_test.go @@ -14,6 +14,13 @@ func TestCLI_WebPush_GenerateKeys(t *testing.T) { require.Contains(t, stderr.String(), "Web Push keys generated.") } +func TestCLI_WebPush_WriteKeysToFile(t *testing.T) { + app, _, _, stderr := newTestApp() + require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--key-file=key-file.yaml")) + require.Contains(t, stderr.String(), "Web Push keys written to key-file.yaml") + require.FileExists(t, "key-file.yaml") +} + func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error { webPushArgs := []string{ "ntfy", From 20cca8e88840d75e8c774bcd8033d3ed22738aaa Mon Sep 17 00:00:00 2001 From: Nogweii Date: Tue, 25 Jun 2024 22:59:17 -0700 Subject: [PATCH 004/378] update go.sum --- go.sum | 57 ++------------------------------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/go.sum b/go.sum index 52702b33..ae11632e 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,18 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA= cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth v0.4.0 h1:vcJWEguhY8KuiHoSs/udg1JtIRYm3YAWPBE1moF1m3U= -cloud.google.com/go/auth v0.4.0/go.mod h1:tO/chJN3obc5AbRYFQDsuFbL4wW5y8LfbPtDCfgwOVE= cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg= -cloud.google.com/go/compute v1.26.0/go.mod h1:T9RIRap4pVHCGUkVFRJ9hygT3KCXjip41X1GgWtBBII= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= -cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= firebase.google.com/go/v4 v4.14.0 h1:Tc9jWzMUApUFUA5UUx/HcBeZ+LPjlhG2vNRfWJrcMwU= @@ -104,9 +90,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -114,8 +99,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -136,8 +119,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -164,8 +145,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= @@ -181,17 +160,14 @@ go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -212,13 +188,9 @@ golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -239,16 +211,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -258,8 +226,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -276,36 +242,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4= -google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= -google.golang.org/api v0.178.0 h1:yoW/QMI4bRVCHF+NWOTa4cL8MoWL3Jnuc7FlcFF91Ok= -google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U= google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be h1:g4aX8SUFA8V5F4LrSY5EclyGYw1OZN4HS1jTyjB9ZDc= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be/go.mod h1:FeSdT5fk+lkxatqJP38MsUicGqHax5cLtmy/6TAuxO4= -google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae h1:HjgkYCl6cWQEKSHkpUp4Q8VB74swzyBwTz1wtTzahm0= -google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:i4np6Wrjp8EujFAUn0CM0SH+iZhY1EbrfzEIJbFkHFM= google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 h1:XpH03M6PDRKTo1oGfZBXu2SzwcbfxUokgobVinuUZoU= google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8/go.mod h1:OLh2Ylz+WlYAJaSBRpJIJLP8iQP+8da+fpxbwNEAV/o= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No= google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae h1:c55+MER4zkBS14uJhSZMGGmya0yJx5iHV4x/fpOSNRk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -326,8 +275,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 903ef71b6f956731521992c3ee7983d186f7c48a Mon Sep 17 00:00:00 2001 From: lexi Date: Sun, 22 Sep 2024 11:58:26 +0200 Subject: [PATCH 005/378] Fix typo "Firebase (FCM" -> "Firebase (FCM)" --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 5fc1b6e5..878bf9c7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1374,7 +1374,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 | | `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | | `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | -| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | +| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). | | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | | `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) | From b843c69c16d0115946beb507d051bbfe783dd4b8 Mon Sep 17 00:00:00 2001 From: Quantum Date: Wed, 16 Oct 2024 21:59:32 -0400 Subject: [PATCH 006/378] docs: add quantum5/ntfy-run to integrations and examples --- docs/examples.md | 6 ++++++ docs/integrations.md | 1 + 2 files changed, 7 insertions(+) diff --git a/docs/examples.md b/docs/examples.md index d6f83f30..8f2442fd 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -31,6 +31,12 @@ GitHub have been hopeless. In case it ever becomes available, I want to know imm */6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi ``` +You can also use [`ntfy-run`](https://github.com/quantum5/ntfy-run) to send the output of your cronjob in the +notification, so that you know exactly why it failed: + +``` +0 0 * * * ntfy-run -n https://ntfy.sh/backups --success-priority low --failure-tags warning ~/backup-computer +``` ## Low disk space alerts Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..b25dab40 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -146,6 +146,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) - [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell) +- [ntfy-run](https://github.com/quantum5/ntfy-run) - Tool to run a command, capture its output, and send it to ntfy (Rust) ## Blog + forum posts From 90f21ba4081dbb63be48f1a83bbdfe5de1861c2d Mon Sep 17 00:00:00 2001 From: jim3692 <31220180+jim3692@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:48:32 +0200 Subject: [PATCH 007/378] Add Clipboard IO to projects --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..608952e5 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -146,6 +146,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) - [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell) +- [Clipboard IO](https://github.com/jim3692/clipboard-io) - End to end encrypted clipboard ## Blog + forum posts From c844c24a16ff17c9987264a1dc20679809c99737 Mon Sep 17 00:00:00 2001 From: KuroSetsuna29 Date: Sat, 2 Nov 2024 00:22:40 -0400 Subject: [PATCH 008/378] allow configurable web push expiry duration --- cmd/serve.go | 16 ++++++++++++++++ docs/config.md | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 62e0a14a..871a5aec 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -100,6 +100,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "automatically expire unused subscriptions after this time"}), ) var cmdServe = &cli.Command{ @@ -140,6 +142,8 @@ func execServe(c *cli.Context) error { webPushFile := c.String("web-push-file") webPushEmailAddress := c.String("web-push-email-address") webPushStartupQueries := c.String("web-push-startup-queries") + webPushExpiryDurationStr := c.String("web-push-expiry-duration") + webPushExpiryWarningDurationStr := c.String("web-push-expiry-warning-duration") cacheFile := c.String("cache-file") cacheDurationStr := c.String("cache-duration") cacheStartupQueries := c.String("cache-startup-queries") @@ -226,6 +230,14 @@ func execServe(c *cli.Context) error { if err != nil { return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr) } + webPushExpiryDuration, err := util.ParseDuration(webPushExpiryDurationStr) + if err != nil { + return fmt.Errorf("invalid web push expiry duration: %s", webPushExpiryDurationStr) + } + webPushExpiryWarningDuration, err := util.ParseDuration(webPushExpiryWarningDurationStr) + if err != nil { + return fmt.Errorf("invalid web push expiry warning duration: %s", webPushExpiryWarningDurationStr) + } // Convert sizes to bytes messageSizeLimit, err := util.ParseSize(messageSizeLimitStr) @@ -304,6 +316,8 @@ func execServe(c *cli.Context) error { if messageSizeLimit > 5*1024*1024 { return errors.New("message-size-limit cannot be higher than 5M") } + } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { + return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") } // Backwards compatibility @@ -417,6 +431,8 @@ func execServe(c *cli.Context) error { conf.WebPushFile = webPushFile conf.WebPushEmailAddress = webPushEmailAddress conf.WebPushStartupQueries = webPushStartupQueries + conf.WebPushExpiryDuration = webPushExpiryDuration + conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration // Set up hot-reloading of config go sigHandlerConfigReload(config) diff --git a/docs/config.md b/docs/config.md index 9479301a..303173b9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -876,7 +876,9 @@ a database to keep track of the browser's subscriptions, and an admin email addr - `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 - `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` - `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com` -- `web-push-startup-queries` is an optional list of queries to run on startup` +- `web-push-startup-queries` is an optional list of queries to run on startup` +- `web-push-expiry-warning-duration` defines the duration for which unused subscriptions are sent a warning (default is `7d`) +- `web-push-expiry-duration` defines the duration for which unused subscriptions will expire (default is `9d`) Limitations: @@ -904,7 +906,7 @@ web-push-email-address: sysadmin@example.com ``` The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days, -and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), +and will automatically expire after 9 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), subscriptions are also removed automatically. The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription From 136b656ccbb2d17f0155112899c3196a254641e7 Mon Sep 17 00:00:00 2001 From: KuroSetsuna29 Date: Sat, 2 Nov 2024 08:50:57 -0400 Subject: [PATCH 009/378] fix descriptions --- cmd/serve.go | 4 ++-- docs/config.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 871a5aec..48cd19a0 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -100,8 +100,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "automatically expire unused subscriptions after this time"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}), ) var cmdServe = &cli.Command{ diff --git a/docs/config.md b/docs/config.md index 303173b9..e04d72c5 100644 --- a/docs/config.md +++ b/docs/config.md @@ -877,8 +877,8 @@ a database to keep track of the browser's subscriptions, and an admin email addr - `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` - `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com` - `web-push-startup-queries` is an optional list of queries to run on startup` -- `web-push-expiry-warning-duration` defines the duration for which unused subscriptions are sent a warning (default is `7d`) -- `web-push-expiry-duration` defines the duration for which unused subscriptions will expire (default is `9d`) +- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `7d`) +- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `9d`) Limitations: From 9241b0550c503546546c2eb5e8b442ec0e1abab2 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Mon, 4 Nov 2024 21:33:35 -0700 Subject: [PATCH 010/378] feat: add subscribe param --- docs/releases.md | 1 + docs/subscribe/api.md | 8 ++++++++ server/message_cache.go | 17 +++++++++++++++++ server/message_cache_test.go | 5 +++++ server/server.go | 6 ++++-- server/server_test.go | 5 +++++ server/types.go | 11 ++++++++--- 7 files changed, 48 insertions(+), 5 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 69222b82..7a1e9ede 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1378,6 +1378,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** * Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) +* Add `latest` subscription param for grabbing just the most recent message (thanks to [@wunter8](https://github.com/wunter8)) **Bug fixes + maintenance:** diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index 3f1c0e81..2387c663 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -257,6 +257,14 @@ curl -s "ntfy.sh/mytopic/json?since=1645970742" curl -s "ntfy.sh/mytopic/json?since=nFS3knfcQ1xe" ``` +### Fetch latest message +If you only want the most recent message sent to a topic and do not have a message ID or timestamp to use with +`since=`, you can use `since=latest` to grab the most recent message from the cache for a particular topic. + +``` +curl -s "ntfy.sh/mytopic/json?poll=1&since=latest" +``` + ### Fetch scheduled messages Messages that are [scheduled to be delivered](../publish.md#scheduled-delivery) at a later date are not typically returned when subscribing via the API, which makes sense, because after all, the messages have technically not been diff --git a/server/message_cache.go b/server/message_cache.go index 4f677816..e314ace3 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -99,6 +99,13 @@ const ( WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` + selectMessagesLatestQuery = ` + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + FROM messages + WHERE topic = ? AND published = 1 + ORDER BY time DESC, id DESC + LIMIT 1 + ` selectMessagesDueQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages @@ -416,6 +423,8 @@ func (c *messageCache) addMessages(ms []*message) error { func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) { if since.IsNone() { return make([]*message, 0), nil + } else if since.IsLatest() { + return c.messagesLatest(topic) } else if since.IsID() { return c.messagesSinceID(topic, since, scheduled) } @@ -462,6 +471,14 @@ func (c *messageCache) messagesSinceID(topic string, since sinceMarker, schedule return readMessages(rows) } +func (c *messageCache) messagesLatest(topic string) ([]*message, error) { + rows, err := c.db.Query(selectMessagesLatestQuery, topic) + if err != nil { + return nil, err + } + return readMessages(rows) +} + func (c *messageCache) MessagesDue() ([]*message, error) { rows, err := c.db.Query(selectMessagesDueQuery, time.Now().Unix()) if err != nil { diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 589ecc42..778f28fe 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -66,6 +66,11 @@ func testCacheMessages(t *testing.T, c *messageCache) { require.Equal(t, 1, len(messages)) require.Equal(t, "my other message", messages[0].Message) + // mytopic: latest + messages, _ = c.Messages("mytopic", sinceLatestMessage, false) + require.Equal(t, 1, len(messages)) + require.Equal(t, "my other message", messages[0].Message) + // example: count counts, err = c.MessageCounts() require.Nil(t, err) diff --git a/server/server.go b/server/server.go index ee2da76a..9a7f9d43 100644 --- a/server/server.go +++ b/server/server.go @@ -1556,8 +1556,8 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled b // parseSince returns a timestamp identifying the time span from which cached messages should be received. // -// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), or -// "all" for all messages. +// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), +// "all" for all messages, or "latest" for the most recent message for a topic func parseSince(r *http.Request, poll bool) (sinceMarker, error) { since := readParam(r, "x-since", "since", "si") @@ -1569,6 +1569,8 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) { return sinceNoMessages, nil } else if since == "all" { return sinceAllMessages, nil + } else if since == "latest" { + return sinceLatestMessage, nil } else if since == "none" { return sinceNoMessages, nil } diff --git a/server/server_test.go b/server/server_test.go index 75379f8f..7fbbeb85 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -594,6 +594,11 @@ func TestServer_PublishAndPollSince(t *testing.T) { require.Equal(t, 1, len(messages)) require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=latest", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=INVALID", "", nil) require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code) } diff --git a/server/types.go b/server/types.go index fb08fb05..c6bdb4d1 100644 --- a/server/types.go +++ b/server/types.go @@ -169,8 +169,12 @@ func (t sinceMarker) IsNone() bool { return t == sinceNoMessages } +func (t sinceMarker) IsLatest() bool { + return t == sinceLatestMessage +} + func (t sinceMarker) IsID() bool { - return t.id != "" + return t.id != "" && t.id != "latest" } func (t sinceMarker) Time() time.Time { @@ -182,8 +186,9 @@ func (t sinceMarker) ID() string { } var ( - sinceAllMessages = sinceMarker{time.Unix(0, 0), ""} - sinceNoMessages = sinceMarker{time.Unix(1, 0), ""} + sinceAllMessages = sinceMarker{time.Unix(0, 0), ""} + sinceNoMessages = sinceMarker{time.Unix(1, 0), ""} + sinceLatestMessage = sinceMarker{time.Unix(0, 0), "latest"} ) type queryFilter struct { From 8feb0f1a2e625ad83492ca490eafcf3072068a35 Mon Sep 17 00:00:00 2001 From: Dmitry Gudkov <30697406+dmitrygudkov@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:28:59 -0500 Subject: [PATCH 011/378] Update integrations.md: Added EasyMorph EasyMorph (https://easymorph.com) is a visual workflow-based data preparation and automation tool. It has 180+ actions, including a dedicated action to send notifications to ntfy as a workflow step. The proposed link leads to the official help page for the "Send message to ntfy" action. --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..4806c50c 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -26,6 +26,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server - [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring - [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring +- [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool ## Integration via HTTP/SMTP/etc. From 19f8a3558824056b1118bd28f45180feab72c66c Mon Sep 17 00:00:00 2001 From: Scott Edlund Date: Sat, 23 Nov 2024 13:24:24 +0700 Subject: [PATCH 012/378] docs: publish.md typo --- docs/publish.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/publish.md b/docs/publish.md index 460fcd35..37b46809 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1007,7 +1007,7 @@ Here's an **easier example with a shorter JSON payload**: === "Command line (curl)" ``` - # To use { and } in the URL without encoding, we need to turn of + # To use { and } in the URL without encoding, we need to turn off # curl's globbing using --globoff curl \ From 27398e7d72f5fb8a93ff8861fda5b5f20fce5697 Mon Sep 17 00:00:00 2001 From: David Wronek Date: Tue, 10 Dec 2024 08:40:36 +0100 Subject: [PATCH 013/378] docs: config.md: fix typo Add a missing parenthesis. Signed-off-by: David Wronek --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 9479301a..6e9ccb6f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1379,7 +1379,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 | | `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | | `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | -| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | +| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). | | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | | `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) | From 4c179b7d9d7644b1edf41b0abca9f1534605c441 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:40:36 +0100 Subject: [PATCH 014/378] docs: add ToC to integrations.md Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> --- docs/integrations.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..0745c633 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -4,6 +4,16 @@ There are quite a few projects that work with ntfy, integrate ntfy, or have been I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community. +## Table of Contents +- [Official integrations](#official-integrations) +- [Integration via HTTP/SMTP/etc.](#integration-via-httpsmtpetc) +- [UnifiedPush integrations](#unifiedpush-integrations) +- [Libraries](#libraries) +- [CLIs + GUIs](#clis--guis) +- [Projects + scripts](#projects--scripts) +- [Blog + forum posts](#blog--forum-posts) +- [Alternative ntfy servers](#alternative-ntfy-servers) + ## Official integrations - [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification From 758828e7aaa3c3294d46798eab752361e514916d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:48:01 +0100 Subject: [PATCH 015/378] docs: add integration: Daily Fact Ntfy Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..51740b66 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -74,6 +74,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy - [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications - [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11 +- [Daily Fact Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - Generate [llm](https://github.com/simonw/llm) generated fact every day about any topic you're interested in. ## Projects + scripts From 80bc600ff0c2662fa1c6205fe44695129f0e0c7b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:48:01 +0100 Subject: [PATCH 016/378] docs: add integration: Ntfy_CSV_Reminders Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..4510e78b 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -74,6 +74,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy - [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications - [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11 +- [Ntfy_CSV_Reminders](https://github.com/thiswillbeyourgithub/Ntfy_CSV_Reminders) - A Python tool that sends random-timing phone notifications for recurring tasks by using daily probability checks based on CSV-defined frequencies. ## Projects + scripts From 6345e7f864cfcafc48e3d62c6cf0963dbbc7a8a9 Mon Sep 17 00:00:00 2001 From: Kyle Duren Date: Wed, 1 Jan 2025 22:08:30 -0500 Subject: [PATCH 017/378] Update quickstart example Just noticed the behind proxy was missing from the example that was supposed to include it. --- docs/config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.md b/docs/config.md index 9479301a..10bb78e6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -50,6 +50,7 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file: listen-http: ":2586" cache-file: "/var/cache/ntfy/cache.db" attachment-cache-dir: "/var/cache/ntfy/attachments" + behind-proxy: true ``` === "server.yml (ntfy.sh config)" From 926967b6e7e3658b84de1dede794346ebcae5f60 Mon Sep 17 00:00:00 2001 From: Kyle Duren Date: Sun, 5 Jan 2025 20:29:08 +0000 Subject: [PATCH 018/378] adding logic to specifcy client-ip header from proxy --- cmd/serve.go | 18 +++-- go.sum | 202 +---------------------------------------------- server/config.go | 2 + server/server.go | 4 +- server/util.go | 63 +++++++++++---- 5 files changed, 65 insertions(+), 224 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 62e0a14a..60a43e43 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,13 +5,6 @@ package cmd import ( "errors" "fmt" - "github.com/stripe/stripe-go/v74" - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" - "heckel.io/ntfy/v2/log" - "heckel.io/ntfy/v2/server" - "heckel.io/ntfy/v2/user" - "heckel.io/ntfy/v2/util" "io/fs" "math" "net" @@ -22,6 +15,14 @@ import ( "strings" "syscall" "time" + + "github.com/stripe/stripe-go/v74" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/server" + "heckel.io/ntfy/v2/user" + "heckel.io/ntfy/v2/util" ) func init() { @@ -89,6 +90,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-client-ip-header", Aliases: []string{"proxy_client_ip_header"}, EnvVars: []string{"NTFY_PROXY_CLIENT_IP_HEADER"}, Value: "", Usage: "if set, use specified header to determine visitor IP address instead of XFF (for rate limiting)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}), @@ -186,6 +188,7 @@ func execServe(c *cli.Context) error { visitorEmailLimitBurst := c.Int("visitor-email-limit-burst") visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish") behindProxy := c.Bool("behind-proxy") + proxyClientIPHeader := c.String("proxy-client-ip-header") stripeSecretKey := c.String("stripe-secret-key") stripeWebhookKey := c.String("stripe-webhook-key") billingContact := c.String("billing-contact") @@ -402,6 +405,7 @@ func execServe(c *cli.Context) error { conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.BehindProxy = behindProxy + conf.ProxyClientIPHeader = proxyClientIPHeader conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact diff --git a/go.sum b/go.sum index 0dd057b5..1bad8365 100644 --- a/go.sum +++ b/go.sum @@ -1,74 +1,25 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= -cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA= -cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth v0.4.0 h1:vcJWEguhY8KuiHoSs/udg1JtIRYm3YAWPBE1moF1m3U= -cloud.google.com/go/auth v0.4.0/go.mod h1:tO/chJN3obc5AbRYFQDsuFbL4wW5y8LfbPtDCfgwOVE= -cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= -cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= -cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s= -cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw= cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= -cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= -cloud.google.com/go/compute v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg= -cloud.google.com/go/compute v1.26.0/go.mod h1:T9RIRap4pVHCGUkVFRJ9hygT3KCXjip41X1GgWtBBII= -cloud.google.com/go/compute v1.27.2 h1:5cE5hdrwJV/92ravlwIFRGnyH9CpLGhh4N0ZDVTU+BA= -cloud.google.com/go/compute v1.28.1 h1:XwPcZjgMCnU2tkwY10VleUjSAfpTj9RDn+kGrbYsi8o= -cloud.google.com/go/compute v1.28.1/go.mod h1:b72iXMY4FucVry3NR3Li4kVyyTvbMDE7x5WsqvxjsYk= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= -cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw= -cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ= cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= -cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= -cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= -cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= -cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= -cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E= -cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics= cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= -cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= -cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= -firebase.google.com/go/v4 v4.14.0 h1:Tc9jWzMUApUFUA5UUx/HcBeZ+LPjlhG2vNRfWJrcMwU= -firebase.google.com/go/v4 v4.14.0/go.mod h1:pLATyL6xH2o9AMe7rqHdmmOUE/Ph7wcwepIs+uiEKPg= firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= @@ -84,8 +35,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -103,15 +52,9 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -147,32 +90,19 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= -github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= -github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= @@ -181,12 +111,10 @@ 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -197,23 +125,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= -github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= -github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= -github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -233,63 +151,29 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -310,21 +194,9 @@ golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -332,8 +204,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -347,24 +217,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -374,16 +232,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -395,56 +245,20 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4= -google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= -google.golang.org/api v0.178.0 h1:yoW/QMI4bRVCHF+NWOTa4cL8MoWL3Jnuc7FlcFF91Ok= -google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U= -google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= -google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= -google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= -google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs= google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be h1:g4aX8SUFA8V5F4LrSY5EclyGYw1OZN4HS1jTyjB9ZDc= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be/go.mod h1:FeSdT5fk+lkxatqJP38MsUicGqHax5cLtmy/6TAuxO4= -google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae h1:HjgkYCl6cWQEKSHkpUp4Q8VB74swzyBwTz1wtTzahm0= -google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:i4np6Wrjp8EujFAUn0CM0SH+iZhY1EbrfzEIJbFkHFM= -google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 h1:XpH03M6PDRKTo1oGfZBXu2SzwcbfxUokgobVinuUZoU= -google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8/go.mod h1:OLh2Ylz+WlYAJaSBRpJIJLP8iQP+8da+fpxbwNEAV/o= -google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc= -google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61 h1:KipVMxePgXPFBzXOvpKbny3RVdVmJOD64R/Ob7GPWEs= google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:HiAZQz/G7n0EywFjmncAwsfnmFm2bjm7qPjwl8hyzjM= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae h1:c55+MER4zkBS14uJhSZMGGmya0yJx5iHV4x/fpOSNRk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -452,10 +266,6 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -469,10 +279,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/config.go b/server/config.go index 7267ce9d..4fa711e9 100644 --- a/server/config.go +++ b/server/config.go @@ -144,6 +144,7 @@ type Config struct { VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics BehindProxy bool + ProxyClientIPHeader string StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration @@ -233,6 +234,7 @@ func NewConfig() *Config { VisitorStatsResetTime: DefaultVisitorStatsResetTime, VisitorSubscriberRateLimiting: false, BehindProxy: false, + ProxyClientIPHeader: "", StripeSecretKey: "", StripeWebhookKey: "", StripePriceCacheDuration: DefaultStripePriceCacheDuration, diff --git a/server/server.go b/server/server.go index ee2da76a..a35f571e 100644 --- a/server/server.go +++ b/server/server.go @@ -1925,7 +1925,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc // that subsequent logging calls still have a visitor context. func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) { // Read "Authorization" header value, and exit out early if it's not set - ip := extractIPAddress(r, s.config.BehindProxy) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyClientIPHeader) vip := s.visitor(ip, nil) if s.userManager == nil { return vip, nil @@ -2000,7 +2000,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us if err != nil { return nil, err } - ip := extractIPAddress(r, s.config.BehindProxy) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyClientIPHeader) go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{ LastAccess: time.Now(), LastOrigin: ip, diff --git a/server/util.go b/server/util.go index bcfe3037..cf9768d1 100644 --- a/server/util.go +++ b/server/util.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" - "heckel.io/ntfy/v2/util" "io" "mime" "net/http" "net/netip" "regexp" "strings" + + "heckel.io/ntfy/v2/util" ) var ( @@ -73,33 +74,61 @@ func readQueryParam(r *http.Request, names ...string) string { return "" } -func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr { +func extractIPAddress(r *http.Request, behindProxy bool, proxyClientIPHeader string) netip.Addr { + logr(r).Debug("Starting IP extraction") + remoteAddr := r.RemoteAddr + logr(r).Debug("RemoteAddr: %s", remoteAddr) + addrPort, err := netip.ParseAddrPort(remoteAddr) ip := addrPort.Addr() if err != nil { - // This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified + logr(r).Warn("Failed to parse RemoteAddr as AddrPort: %v", err) ip, err = netip.ParseAddr(remoteAddr) if err != nil { ip = netip.IPv4Unspecified() - if remoteAddr != "@" || !behindProxy { // RemoteAddr is @ when unix socket is used - logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", remoteAddr) + logr(r).Error("Failed to parse RemoteAddr as IP: %v, defaulting to 0.0.0.0", err) + } + } + + // Log initial IP before further processing + logr(r).Debug("Initial IP after RemoteAddr parsing: %s", ip) + + if proxyClientIPHeader != "" { + logr(r).Debug("Using ProxyClientIPHeader: %s", proxyClientIPHeader) + if customHeaderIP := r.Header.Get(proxyClientIPHeader); customHeaderIP != "" { + logr(r).Debug("Custom header %s value: %s", proxyClientIPHeader, customHeaderIP) + realIP, err := netip.ParseAddr(customHeaderIP) + if err != nil { + logr(r).Error("Invalid IP in %s header: %s, error: %v", proxyClientIPHeader, customHeaderIP, err) + } else { + logr(r).Debug("Successfully parsed IP from custom header: %s", realIP) + ip = realIP } - } - } - if behindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" { - // X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, - // only the right-most address can be trusted (as this is the one added by our proxy server). - // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. - ips := util.SplitNoEmpty(r.Header.Get("X-Forwarded-For"), ",") - realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr))) - if err != nil { - logr(r).Err(err).Error("invalid IP address %s received in X-Forwarded-For header", ip) - // Fall back to regular remote address if X-Forwarded-For is damaged } else { - ip = realIP + logr(r).Warn("Custom header %s is empty or missing", proxyClientIPHeader) } + } else if behindProxy { + logr(r).Debug("No ProxyClientIPHeader set, checking X-Forwarded-For") + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + logr(r).Debug("X-Forwarded-For value: %s", xff) + ips := util.SplitNoEmpty(xff, ",") + realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr))) + if err != nil { + logr(r).Error("Invalid IP in X-Forwarded-For header: %s, error: %v", xff, err) + } else { + logr(r).Debug("Successfully parsed IP from X-Forwarded-For: %s", realIP) + ip = realIP + } + } else { + logr(r).Debug("X-Forwarded-For header is empty or missing") + } + } else { + logr(r).Debug("Behind proxy is false, skipping proxy headers") } + + // Final resolved IP + logr(r).Debug("Final resolved IP: %s", ip) return ip } From 20c014ba8d4874365167a5ba9fa66a32cb6a3c11 Mon Sep 17 00:00:00 2001 From: Kyle Duren Date: Mon, 6 Jan 2025 00:57:53 +0000 Subject: [PATCH 019/378] Adding test and some docs --- docs/config.md | 3 ++- server/server_test.go | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/config.md b/docs/config.md index 9479301a..c92493fb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -555,12 +555,13 @@ Whatever your reasons may be, there are a few things to consider. If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will -be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address. +be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address. If your proxy or CDN provider uses a custom header to securely pass the source IP/Client IP to your application, you can specify that header instead of using the XFF. Using the custom header (unique per provide/cdn/proxy), will disable the use of the XFF header. === "/etc/ntfy/server.yml" ``` yaml # Tell ntfy to use "X-Forwarded-For" to identify visitors behind-proxy: true + proxy-client-ip-header: "X-Client-IP" ``` ### TLS/SSL diff --git a/server/server_test.go b/server/server_test.go index 75379f8f..42271928 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -7,8 +7,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "golang.org/x/crypto/bcrypt" - "heckel.io/ntfy/v2/user" "io" "net/http" "net/http/httptest" @@ -22,6 +20,9 @@ import ( "testing" "time" + "golang.org/x/crypto/bcrypt" + "heckel.io/ntfy/v2/user" + "github.com/SherClockHolmes/webpush-go" "github.com/stretchr/testify/require" "heckel.io/ntfy/v2/log" @@ -2181,6 +2182,19 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) { require.Equal(t, "234.5.2.1", v.ip.String()) } +func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyClientIPHeader = "X-Client-IP" + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11" + r.Header.Set("X-Client-IP", "1.2.3.4") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "1.2.3.4", v.ip.String()) +} + func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { t.Parallel() count := 50000 From 0e6a483b2f5d29c9525b2d80f8512d77bc5b71e4 Mon Sep 17 00:00:00 2001 From: Kyle Duren Date: Mon, 6 Jan 2025 01:06:28 +0000 Subject: [PATCH 020/378] fixing auto-format change --- cmd/serve.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 60a43e43..123791ac 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,6 +5,13 @@ package cmd import ( "errors" "fmt" + "github.com/stripe/stripe-go/v74" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/server" + "heckel.io/ntfy/v2/user" + "heckel.io/ntfy/v2/util" "io/fs" "math" "net" @@ -16,13 +23,7 @@ import ( "syscall" "time" - "github.com/stripe/stripe-go/v74" - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" - "heckel.io/ntfy/v2/log" - "heckel.io/ntfy/v2/server" - "heckel.io/ntfy/v2/user" - "heckel.io/ntfy/v2/util" + ) func init() { From 0aee6252bbb16b442c2151424eb97577c6f68e0f Mon Sep 17 00:00:00 2001 From: Kyle Duren Date: Mon, 6 Jan 2025 01:09:40 +0000 Subject: [PATCH 021/378] fixing auto-format change --- cmd/serve.go | 2 -- server/util.go | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 123791ac..b8004a56 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -22,8 +22,6 @@ import ( "strings" "syscall" "time" - - ) func init() { diff --git a/server/util.go b/server/util.go index cf9768d1..0f224de1 100644 --- a/server/util.go +++ b/server/util.go @@ -4,14 +4,13 @@ import ( "context" "errors" "fmt" + "heckel.io/ntfy/v2/util" "io" "mime" "net/http" "net/netip" "regexp" "strings" - - "heckel.io/ntfy/v2/util" ) var ( From a49cafbadb78477b0fb74ca6725d7f664470eb50 Mon Sep 17 00:00:00 2001 From: Kyle Duren Date: Mon, 6 Jan 2025 02:55:03 +0000 Subject: [PATCH 022/378] more correcting auto-formats --- server/server_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/server_test.go b/server/server_test.go index 42271928..9aa3ef80 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -7,6 +7,8 @@ import ( "encoding/base64" "encoding/json" "fmt" + "golang.org/x/crypto/bcrypt" + "heckel.io/ntfy/v2/user" "io" "net/http" "net/http/httptest" @@ -20,9 +22,6 @@ import ( "testing" "time" - "golang.org/x/crypto/bcrypt" - "heckel.io/ntfy/v2/user" - "github.com/SherClockHolmes/webpush-go" "github.com/stretchr/testify/require" "heckel.io/ntfy/v2/log" From 5822a2ec410eafe9271a24aee946697a04cebcd6 Mon Sep 17 00:00:00 2001 From: David Havlicek Date: Fri, 17 Jan 2025 13:26:22 -0800 Subject: [PATCH 023/378] add canary in the cage podcast coverage to integrations page --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..d402f032 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -246,6 +246,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - ugeek.github.io - 11/2021 - [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021 - [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021 +- [ntfy on The Canary in the Cage Podcast](https://odysee.com/@TheCanaryInTheCage:b/The-Canary-in-the-Cage-Episode-42:1?r=4gitYjTacQqPEjf22874USecDQYJ5y5E&t=3062) - odysee.com - 1/2025 ## Alternative ntfy servers From 2344eee2c6c8f59f6c170310330f408d5b809e01 Mon Sep 17 00:00:00 2001 From: Christian Harke Date: Mon, 20 Jan 2025 21:20:50 +0100 Subject: [PATCH 024/378] Make markdown code blocks scrollable --- web/src/components/Notifications.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 0b8b2e7d..dceb5b91 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -189,6 +189,7 @@ const MarkdownContainer = styled("div")` } pre { + overflow-x: scroll; padding: 0.9rem; } From f739a3067e9f3ddc32c3545ce73cc528b1e0285d Mon Sep 17 00:00:00 2001 From: Brian <18603393+brian6932@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:28:29 -0500 Subject: [PATCH 025/378] docs: Typo `wep` -> `web` --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 9479301a..d4766cde 100644 --- a/docs/config.md +++ b/docs/config.md @@ -865,7 +865,7 @@ it'll show `New message` as a popup. ## Web Push [Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030)) allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed. -When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the +When enabled, the user can enable **background notifications** for their topics in the web app under Settings. Once enabled by the user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then forward it to the browser. From bd39cf4b54f1959808b6416266ea94ee42b9d95e Mon Sep 17 00:00:00 2001 From: Martin Matuska Date: Sun, 26 Jan 2025 00:00:06 +0100 Subject: [PATCH 026/378] server/util.go: fix logic in extractIPAddress() --- server/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/util.go b/server/util.go index bcfe3037..73434cf7 100644 --- a/server/util.go +++ b/server/util.go @@ -82,7 +82,7 @@ func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr { ip, err = netip.ParseAddr(remoteAddr) if err != nil { ip = netip.IPv4Unspecified() - if remoteAddr != "@" || !behindProxy { // RemoteAddr is @ when unix socket is used + if remoteAddr != "@" && !behindProxy { // RemoteAddr is @ when unix socket is used logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", remoteAddr) } } From 35458230a87d983b6444da9e1e40b8bdc85ee405 Mon Sep 17 00:00:00 2001 From: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:49:22 +0100 Subject: [PATCH 027/378] add major and minor version tags to docker release flow --- .goreleaser.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index 062cce1f..2e248fca 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -197,3 +197,15 @@ docker_manifests: - *arm64v8_image - *armv7_image - *armv6_image + - name_template: "binwiederhier/ntfy:v{{ .Major }}" + image_templates: + - *amd64_image + - *arm64v8_image + - *armv7_image + - *armv6_image + - name_template: "binwiederhier/ntfy:v{{ .Major }}.{{ .Minor }}" + image_templates: + - *amd64_image + - *arm64v8_image + - *armv7_image + - *armv6_image \ No newline at end of file From 6b2cfb1d1d76a58629d205883924c6655e10cd3e Mon Sep 17 00:00:00 2001 From: barart <16019687+barart@users.noreply.github.com> Date: Wed, 5 Mar 2025 13:04:21 -0600 Subject: [PATCH 028/378] Handle anonymous read restrictions by sending a poll_request event If a topic does not allow anonymous reads, this change ensures that we send a "poll_request" event instead of relaying the message via Firebase. Additionally, we include generic text in the title and body/message. This way, if the client cannot retrieve the actual message, the user will still receive a notification, prompting them to update the client manually. --- server/server_firebase.go | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/server/server_firebase.go b/server/server_firebase.go index 4a0cb7f9..aff96db7 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -175,13 +175,26 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro } else { // If anonymous read for a topic is not allowed, we cannot send the message along // via Firebase. Instead, we send a "poll_request" message, asking the client to poll. + //App function needs all the data to create a message object, if not, it fails, + //so we set it but put a placeholders to not to send the actual message + //but generic title and message instead, we also add the poll_id so client knowns + //what message is goint to "decode" (retrieve) data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": pollRequestEvent, - "topic": m.Topic, - } - // TODO Handle APNS? + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": pollRequestEvent, + "topic": m.Topic, + "priority": fmt.Sprintf("%d", m.Priority), + "tags": strings.Join(m.Tags, ","), + "click": m.Click, + "icon": m.Icon, + "title": "Private", + "message": "Message", + "content_type": m.ContentType, + "encoding": m.Encoding, + "poll_id": m.ID, + } + apnsConfig = createAPNSAlertConfig(m, data) } } var androidConfig *messaging.AndroidConfig @@ -225,14 +238,23 @@ func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSCo for k, v := range data { apnsData[k] = v } + alertTitle := m.Title + alertBody := maybeTruncateAPNSBodyMessage(m.Message) + // If the event is pollRequestEvent (server/topic is restricted) we dont want to + //send the actual message to Firebase/APNS, so we send a generic text + //if for some reason, client cant retrieve the message, it shows this as the message and title + if event, ok := data["event"]; ok && event == pollRequestEvent { + alertTitle = "New Notification received" + alertBody = "Message cant be retrieved, open the app and refresh content" + } return &messaging.APNSConfig{ Payload: &messaging.APNSPayload{ CustomData: apnsData, Aps: &messaging.Aps{ MutableContent: true, Alert: &messaging.ApsAlert{ - Title: m.Title, - Body: maybeTruncateAPNSBodyMessage(m.Message), + Title: alertTitle, + Body: alertBody, }, }, }, From 6af8d0347038a02bec831b9046b7eedf9d65f93a Mon Sep 17 00:00:00 2001 From: Sharjeel Aziz Date: Wed, 19 Mar 2025 15:35:46 -0400 Subject: [PATCH 029/378] Add Terminal Notifications for Long-Running Commands example Signed-off-by: Sharjeel Aziz --- docs/examples.md | 59 ++++++++++++++++++ .../img/mobile-screenshot-notification.png | Bin 0 -> 72277 bytes 2 files changed, 59 insertions(+) create mode 100644 docs/static/img/mobile-screenshot-notification.png diff --git a/docs/examples.md b/docs/examples.md index d6f83f30..18523716 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -634,3 +634,62 @@ or by simply providing traccar with a valid username/password combination. phil mypass ``` + +## Terminal Notifications for Long-Running Commands + +This example provides a simple way to send notifications using [ntfy.sh](https://ntfy.sh) when a terminal command completes. It includes success or failure indicators based on the command's exit status. + +### Setup + +1. Store your ntfy.sh bearer token securely if access control is enabled: + + ```sh + echo "your_bearer_token_here" > ~/.ntfy_token + chmod 600 ~/.ntfy_token + ``` + +1. Add the following function and alias to your `.bashrc` or `.bash_profile`: + + ```sh + # Function for alert notifications using ntfy.sh + notify_via_ntfy() { + local exit_status=$? # Capture the exit status before doing anything else + local token=$(< ~/.ntfy_token) # Securely read the token + local status_icon="$([ $exit_status -eq 0 ] && echo magic_wand || echo warning)" + local last_command=$(history | tail -n1 | sed -e 's/^[[:space:]]*[0-9]\{1,\}[[:space:]]*//' -e 's/[;&|][[:space:]]*alert$//') + + curl -s -X POST "https://n.example.dev/alerts" \ + -H "Authorization: Bearer $token" \ + -H "Title: Terminal" \ + -H "X-Priority: 3" \ + -H "Tags: $status_icon" \ + -d "Command: $last_command (Exit: $exit_status)" + + echo "Tags: $status_icon" + echo "$last_command (Exit: $exit_status)" + } + + # Add an "alert" alias for long running commands using ntfy.sh + alias alert='notify_via_ntfy' + + ``` + +### Usage + +Run any long-running command and append `alert` to notify when it completes: + +```sh +sleep 10; alert +``` +![ntfy notifications on mobile device](static/img/mobile-screenshot-notification.png) + +**Notification Sent** with a success 🪄 (`magic_wand`) or failure ⚠️ (`warning`) tag. + +#### Simulating Failures + +To test failure notifications: + +```sh +false; alert # Always fails (exit 1) +ls --invalid; alert # Invalid option +cat nonexistent_file; alert # File not found \ No newline at end of file diff --git a/docs/static/img/mobile-screenshot-notification.png b/docs/static/img/mobile-screenshot-notification.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9147fc119bbd99d691f6eb9a39b41598744ba9 GIT binary patch literal 72277 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 z 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

?SAcZ6G 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 YXT3XYM?y{>hcf|UW^@x?dCepCKakpsD*ylh literal 0 HcmV?d00001 From 889a6f03f8a489e33b5738c661eb320fcd12344d Mon Sep 17 00:00:00 2001 From: Christoph Vilsmeier Date: Tue, 25 Mar 2025 16:56:47 +0100 Subject: [PATCH 030/378] docs: add integration: Monibot --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..89ddcdb7 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -26,6 +26,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server - [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring - [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring +- [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. ## Integration via HTTP/SMTP/etc. From db9b974e47b0a2f8dab4c0fea9bcb9ff9bb4d8ad Mon Sep 17 00:00:00 2001 From: jlssmt <42897917+jlssmt@users.noreply.github.com> Date: Sun, 6 Apr 2025 11:32:20 +0200 Subject: [PATCH 031/378] set LABEL org.opencontainers.image.version --- Dockerfile-build | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile-build b/Dockerfile-build index 4530ec47..457c63ff 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -44,6 +44,8 @@ RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server FROM alpine +ARG VERSION=dev + LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com" LABEL org.opencontainers.image.url="https://ntfy.sh/" LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/" @@ -52,6 +54,7 @@ LABEL org.opencontainers.image.vendor="Philipp C. Heckel" LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0" LABEL org.opencontainers.image.title="ntfy" LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST" +LABEL org.opencontainers.image.version="$VERSION" COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy From c9126e7aa9a32a2f47dce65da10be39d96541e03 Mon Sep 17 00:00:00 2001 From: Josh J Date: Mon, 7 Apr 2025 09:26:52 +0100 Subject: [PATCH 032/378] docs: correct mountPath for server.yml Fixed #1309 --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 0c823571..45870505 100644 --- a/docs/install.md +++ b/docs/install.md @@ -540,7 +540,7 @@ kubectl apply -k /ntfy cpu: 150m memory: 150Mi volumeMounts: - - mountPath: /etc/ntfy/server.yml + - mountPath: /etc/ntfy subPath: server.yml name: config-volume # generated vie configMapGenerator from kustomization file - mountPath: /var/cache/ntfy From bd08a120cda0949769225f197fbd947245703018 Mon Sep 17 00:00:00 2001 From: Volker Krause Date: Mon, 7 Apr 2025 17:19:44 +0200 Subject: [PATCH 033/378] Consider aes128gcm content encoding as an indicator for UnifiedPush Without this a UnifiedPush/Web Push message with encryption would be turned into an attachment. That in itself isn't pretty but can still work, but it requires attachments to be enabled in the first place. --- server/server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index ee2da76a..8e2f6992 100644 --- a/server/server.go +++ b/server/server.go @@ -1025,7 +1025,8 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } template = readBoolParam(r, false, "x-template", "template", "tpl") unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! - if unifiedpush { + contentEncoding := readParam(r, "content-encoding") + if unifiedpush || contentEncoding == "aes128gcm" { firebase = false unifiedpush = true } From c1d718ee688742a7381c560deed2849b35603400 Mon Sep 17 00:00:00 2001 From: patricksthannon <153395657+patricksthannon@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:53:17 -0700 Subject: [PATCH 034/378] Update integrations.md Added InvaderInformant integration to integration list. Thanks! --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..a47fa99e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -146,6 +146,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) - [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell) +- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) ## Blog + forum posts From d4dfd3f65780b7908d2f681e73c7e71aa06608ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robbie=20Bj=C3=B6rk?= Date: Sat, 26 Apr 2025 00:22:54 +0200 Subject: [PATCH 035/378] Update integrations.md Added alertmanager-ntfy-relay to integrations.md --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..f343aeef 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -82,6 +82,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js) - [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh) - [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell) +- [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) - ntfy.sh relay for Alertmanager (Go) - [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell) - [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs) - [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell) From 8b95b1a2134307ffb6cfd8fddb524c537d3f3dfc Mon Sep 17 00:00:00 2001 From: Yassir Hannoun Date: Sat, 26 Apr 2025 21:13:12 +0000 Subject: [PATCH 036/378] docs: Added UptimeObserver integration --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..b4d8154e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -36,6 +36,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy)) - [Proxmox-Ntfy](https://github.com/qtsone/proxmox-ntfy) - Python script that monitors Proxmox tasks and sends notifications using the Ntfy service. - [Scrutiny](https://github.com/AnalogJ/scrutiny) - WebUI for smartd S.M.A.R.T monitoring. Scrutiny includes shoutrrr/ntfy integration ([see integration README](https://github.com/AnalogJ/scrutiny?tab=readme-ov-file#notifications)) +- [UptimeObserver](https://uptimeobserver.com) - Uptime Monitoring tool for Websites, APIs, SSL Certificates, DNS, Domain Names and Ports. [Integration Guide](https://support.uptimeobserver.com/integrations/ntfy/) ## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations From 3f1342c05b21e1750db36f06f59507b30bf9cb4e Mon Sep 17 00:00:00 2001 From: gitmotion <43588713+gitmotion@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:25:40 -0700 Subject: [PATCH 037/378] Add ntfy-me-mcp --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..8236122a 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -146,6 +146,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) - [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell) +- [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript) ## Blog + forum posts From 03aeb707f2a3276239765292d37311786b44e4b4 Mon Sep 17 00:00:00 2001 From: Thea Tischbein Date: Mon, 5 May 2025 11:13:07 +0200 Subject: [PATCH 038/378] feat: Add optional web app flag which requires a login for every action --- cmd/serve.go | 1 + docs/config.md | 1 + server/config.go | 1 + server/server.go | 1 + server/server.yml | 2 ++ server/types.go | 1 + web/public/static/langs/en.json | 2 ++ web/src/components/Navigation.jsx | 8 +++++++- web/src/components/Notifications.jsx | 9 +++++++-- web/src/components/Preferences.jsx | 26 ++++++++++++++++---------- 10 files changed, 39 insertions(+), 13 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 62e0a14a..47c9baf4 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -62,6 +62,7 @@ var flagsServe = append( altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}), + altsrc.NewBoolFlag(&cli.BoolFlag{Name: "require-login", Aliases: []string{"require_login"}, EnvVars: []string{"NTFY_REQUIRE_LOGIN"}, Value: false, Usage: "all actions via the web app requires a login"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}), diff --git a/docs/config.md b/docs/config.md index 9479301a..6d67e57f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1424,6 +1424,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API | | `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API | | `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) | +| `require-login` | `NTFY_REQUIRE_LOGIN` | *boolean* (`true` or `false`) | `false` | All actions via the web app require a login | | `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments | | `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe | | `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact | diff --git a/server/config.go b/server/config.go index 7267ce9d..1204910e 100644 --- a/server/config.go +++ b/server/config.go @@ -240,6 +240,7 @@ func NewConfig() *Config { EnableSignup: false, EnableLogin: false, EnableReservations: false, + RequireLogin: false, AccessControlAllowOrigin: "*", Version: "", WebPushPrivateKey: "", diff --git a/server/server.go b/server/server.go index ee2da76a..c922387a 100644 --- a/server/server.go +++ b/server/server.go @@ -583,6 +583,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi EnableCalls: s.config.TwilioAccount != "", EnableEmails: s.config.SMTPSenderFrom != "", EnableReservations: s.config.EnableReservations, + ReuqireLogin: s.config.RequireLogin, EnableWebPush: s.config.WebPushPublicKey != "", BillingContact: s.config.BillingContact, WebPushPublicKey: s.config.WebPushPublicKey, diff --git a/server/server.yml b/server/server.yml index 7329d37e..ded53c5e 100644 --- a/server/server.yml +++ b/server/server.yml @@ -214,10 +214,12 @@ # - enable-signup allows users to sign up via the web app, or API # - enable-login allows users to log in via the web app, or API # - enable-reservations allows users to reserve topics (if their tier allows it) +# - require-login all user actions via the web app require a login # # enable-signup: false # enable-login: false # enable-reservations: false +# require-login: false # Server URL of a Firebase/APNS-connected ntfy server (likely "https://ntfy.sh"). # diff --git a/server/types.go b/server/types.go index fb08fb05..2565faa6 100644 --- a/server/types.go +++ b/server/types.go @@ -401,6 +401,7 @@ type apiConfigResponse struct { EnableEmails bool `json:"enable_emails"` EnableReservations bool `json:"enable_reservations"` EnableWebPush bool `json:"enable_web_push"` + RequireLogin bool `json:"require_login"` BillingContact string `json:"billing_contact"` WebPushPublicKey string `json:"web_push_public_key"` DisallowedTopics []string `json:"disallowed_topics"` diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 3ad04ea7..e0bc1085 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -97,6 +97,8 @@ "notifications_none_for_any_description": "To send notifications to a topic, simply PUT or POST to the topic URL. Here's an example using one of your topics.", "notifications_no_subscriptions_title": "It looks like you don't have any subscriptions yet.", "notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.", + "notifications_no_subscriptions_login_title": "This page requires a Login.", + "notifications_no_subscriptions_login_description": "Click \"{{linktext}}\" to login into your account.", "notifications_example": "Example", "notifications_more_details": "For more information, check out the website or documentation.", "display_name_dialog_title": "Change display name", diff --git a/web/src/components/Navigation.jsx b/web/src/components/Navigation.jsx index 7e30931a..0c4da2e5 100644 --- a/web/src/components/Navigation.jsx +++ b/web/src/components/Navigation.jsx @@ -135,7 +135,7 @@ const NavList = (props) => { {showNotificationContextNotSupportedBox && } {showNotificationIOSInstallRequired && } {alertVisible && } - {!showSubscriptionsList && ( + {!showSubscriptionsList && (session.exists() || !config.require_login) && ( navigate(routes.app)} selected={location.pathname === config.app_root}> @@ -164,30 +164,36 @@ const NavList = (props) => { )} + {session.exists() || !config.require_login && ( navigate(routes.settings)} selected={location.pathname === routes.settings}> + )} openUrl("/docs")}> + {session.exists() || !config.require_login && ( props.onPublishMessageClick()}> + )} + {session.exists() || !config.require_login && ( setSubscribeDialogOpen(true)}> + )} {showUpgradeBanner && ( // The text background gradient didn't seem to do well with switching between light/dark mode, // So adding a `key` forces React to replace the entire component when the theme changes diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 0b8b2e7d..69b5978e 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -37,6 +37,7 @@ import priority5 from "../img/priority-5.svg"; import logoOutline from "../img/ntfy-outline.svg"; import AttachmentIcon from "./AttachmentIcon"; import { useAutoSubscribe } from "./hooks"; +import session from "../app/Session"; const priorityFiles = { 1: priority1, @@ -634,12 +635,16 @@ const NoSubscriptions = () => { {t("action_bar_logo_alt")}
- {t("notifications_no_subscriptions_title")} + {!session.exists() && !config.require_login && t("notifications_no_subscriptions_title")} + {!session.exists() && config.require_login && t("notifications_no_subscriptions_login_title")}
- {t("notifications_no_subscriptions_description", { + {!session.exists() && !config.require_login && t("notifications_no_subscriptions_description", { linktext: t("nav_button_subscribe"), })} + {!session.exists() && config.require_login && t("notifications_no_subscriptions_login_description", { + linktext: t("action_bar_sign_in"), + })} diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index 6770f282..d0767a2a 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -65,16 +65,22 @@ const maybeUpdateAccountSettings = async (payload) => { } }; -const Preferences = () => ( - - - - - - - - -); +const Preferences = () => { + if (!session.exists() or !config.requireLogin) { + window.location.href = routes.app; + return <>; + } + return ( + + + + + + + + + ); +}; const Notifications = () => { const { t } = useTranslation(); From f110472204c9d74a98e9ee8cb2ee35898fbe3dcb Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Wed, 14 May 2025 11:20:30 -0600 Subject: [PATCH 039/378] fix typo --- server/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index ee2da76a..e66b9939 100644 --- a/server/server.go +++ b/server/server.go @@ -1885,14 +1885,14 @@ func (s *Server) transformMatrixJSON(next handleFunc) handleFunc { } func (s *Server) authorizeTopicWrite(next handleFunc) handleFunc { - return s.autorizeTopic(next, user.PermissionWrite) + return s.authorizeTopic(next, user.PermissionWrite) } func (s *Server) authorizeTopicRead(next handleFunc) handleFunc { - return s.autorizeTopic(next, user.PermissionRead) + return s.authorizeTopic(next, user.PermissionRead) } -func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc { +func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { if s.userManager == nil { return next(w, r, v) From cdae5493e267f8d3ac8167291f6e3d8d8d95a4bd Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Wed, 14 May 2025 11:39:18 -0600 Subject: [PATCH 040/378] write http errors to websocket connection instead of always 200 --- server/server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index e66b9939..68e20724 100644 --- a/server/server.go +++ b/server/server.go @@ -413,7 +413,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor, } else { ev.Info("WebSocket error: %s", err.Error()) } - return // Do not attempt to write to upgraded connection + w.WriteHeader(httpErr.HTTPCode) + return // Do not attempt to write any body to upgraded connection } if isNormalError { ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code) From 44b7c2f198f5171bf3f40a36b5efa2ae40402ce3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 20 May 2025 10:49:26 +0200 Subject: [PATCH 041/378] user: Allow changing the hashed password directly This adds the detection of `NTFY_PASSWORD_HASH` when creating a user or changing its passsword so that scripts don't have to manipulate the bare password. --- cmd/user.go | 37 +++++++++++------ server/server_account.go | 4 +- server/server_account_test.go | 24 +++++------ server/server_admin.go | 2 +- server/server_admin_test.go | 16 ++++---- server/server_payments_test.go | 18 ++++----- server/server_test.go | 32 +++++++-------- server/server_twilio_test.go | 8 ++-- server/server_webpush_test.go | 4 +- user/manager.go | 32 +++++++++++---- user/manager_test.go | 74 +++++++++++++++++++--------------- 11 files changed, 144 insertions(+), 107 deletions(-) diff --git a/cmd/user.go b/cmd/user.go index af3afe54..e6867b11 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -42,7 +42,7 @@ var cmdUser = &cli.Command{ Name: "add", Aliases: []string{"a"}, Usage: "Adds a new user", - UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME", + UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD_HASH=... ntfy user add [--role=admin|user] USERNAME", Action: execUserAdd, Flags: []cli.Flag{ &cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(user.RoleUser), Usage: "user role"}, @@ -55,12 +55,13 @@ granted otherwise by the auth-default-access setting). An admin user has read an topics. Examples: - ntfy user add phil # Add regular user phil - ntfy user add --role=admin phil # Add admin user phil - NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts) + ntfy user add phil # Add regular user phil + ntfy user add --role=admin phil # Add admin user phil + NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts) + NTFY_PASSWORD_HASH=... ntfy user add phil # Add user, using env variable to set password hash (for scripts) -You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if -you are creating users via scripts. +You may set the NTFY_PASSWORD environment variable to pass the password, or NTFY_PASSWORD_HASH to pass +directly the bcrypt hash. This is useful if you are creating users via scripts. `, }, { @@ -79,7 +80,7 @@ Example: Name: "change-pass", Aliases: []string{"chp"}, Usage: "Changes a user's password", - UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME", + UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME\nNTFY_PASSWORD_HASH=... ntfy user change-pass USERNAME", Action: execUserChangePass, Description: `Change the password for the given user. @@ -89,9 +90,10 @@ it twice. Example: ntfy user change-pass phil NTFY_PASSWORD=.. ntfy user change-pass phil + NTFY_PASSWORD_HASH=.. ntfy user change-pass phil -You may set the NTFY_PASSWORD environment variable to pass the new password. This is -useful if you are updating users via scripts. +You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass +directly the bcrypt hash. This is useful if you are updating users via scripts. `, }, @@ -174,7 +176,12 @@ variable to pass the new password. This is useful if you are creating/updating u func execUserAdd(c *cli.Context) error { username := c.Args().Get(0) role := user.Role(c.String("role")) - password := os.Getenv("NTFY_PASSWORD") + password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH") + + if !hashed { + password = os.Getenv("NTFY_PASSWORD") + } + if username == "" { return errors.New("username expected, type 'ntfy user add --help' for help") } else if username == userEveryone || username == user.Everyone { @@ -200,7 +207,7 @@ func execUserAdd(c *cli.Context) error { } password = p } - if err := manager.AddUser(username, password, role); err != nil { + if err := manager.AddUser(username, password, role, hashed); err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role) @@ -230,7 +237,11 @@ func execUserDel(c *cli.Context) error { func execUserChangePass(c *cli.Context) error { username := c.Args().Get(0) - password := os.Getenv("NTFY_PASSWORD") + password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH") + + if !hashed { + password = os.Getenv("NTFY_PASSWORD") + } if username == "" { return errors.New("username expected, type 'ntfy user change-pass --help' for help") } else if username == userEveryone || username == user.Everyone { @@ -249,7 +260,7 @@ func execUserChangePass(c *cli.Context) error { return err } } - if err := manager.ChangePassword(username, password); err != nil { + if err := manager.ChangePassword(username, password, hashed); err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username) diff --git a/server/server_account.go b/server/server_account.go index 3f2368da..acdf25ec 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -37,7 +37,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * return errHTTPConflictUserExists } logvr(v, r).Tag(tagAccount).Field("user_name", newAccount.Username).Info("Creating user %s", newAccount.Username) - if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { + if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser, false); err != nil { if errors.Is(err, user.ErrInvalidArgument) { return errHTTPBadRequestInvalidUsername } @@ -207,7 +207,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ return errHTTPBadRequestIncorrectPasswordConfirmation } logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name) - if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil { + if err := s.userManager.ChangePassword(u.Name, req.NewPassword, false); err != nil { return err } return s.writeJSON(w, newSuccessResponse()) diff --git a/server/server_account_test.go b/server/server_account_test.go index 72ba55c9..91db1bc5 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -87,9 +87,9 @@ func TestAccount_Signup_AsUser(t *testing.T) { defer s.closeDatabases() log.Info("1") - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) log.Info("2") - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) log.Info("3") rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -174,7 +174,7 @@ func TestAccount_ChangeSettings(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) u, _ := s.userManager.User("phil") token, _ := s.userManager.CreateToken(u.ID, "", time.Unix(0, 0), netip.IPv4Unspecified()) @@ -203,7 +203,7 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -254,7 +254,7 @@ func TestAccount_ChangePassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": ""}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -296,7 +296,7 @@ func TestAccount_ExtendToken(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -332,7 +332,7 @@ func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! @@ -345,7 +345,7 @@ func TestAccount_DeleteToken(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -455,14 +455,14 @@ func TestAccount_Reservation_AddAdminSuccess(t *testing.T) { Code: "pro", ReservationLimit: 2, })) - require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("noadmin1", "pro")) require.Nil(t, s.userManager.AddReservation("noadmin1", "mytopic", user.PermissionDenyAll)) - require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("noadmin2", "pro")) - require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, false)) // Admin can reserve topic rr := request(t, s, "POST", "/v1/account/reservation", `{"topic":"sometopic","everyone":"deny-all"}`, map[string]string{ @@ -624,7 +624,7 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) { s := newTestServer(t, conf) // Create user with tier - require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser, false)) require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "pro", MessageLimit: 20, diff --git a/server/server_admin.go b/server/server_admin.go index ac295718..49a4d4d9 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -60,7 +60,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit return err } } - if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { + if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser, false); err != nil { return err } if tier != nil { diff --git a/server/server_admin_test.go b/server/server_admin_test.go index c2f8f95a..a8bc87e1 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -14,7 +14,7 @@ func TestUser_AddRemove(t *testing.T) { defer s.closeDatabases() // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "tier1", })) @@ -56,8 +56,8 @@ func TestUser_AddRemove_Failures(t *testing.T) { defer s.closeDatabases() // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) // Cannot create user with invalid username rr := request(t, s, "PUT", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ @@ -97,8 +97,8 @@ func TestAccess_AllowReset(t *testing.T) { defer s.closeDatabases() // User and admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) // Subscribing not allowed rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ @@ -138,7 +138,7 @@ func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) { defer s.closeDatabases() // User - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) // Grant access fails, because non-admin rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ @@ -154,8 +154,8 @@ func TestAccess_AllowReset_KillConnection(t *testing.T) { defer s.closeDatabases() // User and admin, grant access to "gol*" topics - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard! start, timeTaken := time.Now(), atomic.Int64{} diff --git a/server/server_payments_test.go b/server/server_payments_test.go index 8da47a65..56d4cc6a 100644 --- a/server/server_payments_test.go +++ b/server/server_payments_test.go @@ -148,7 +148,7 @@ func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testing.T) { Code: "pro", StripeMonthlyPriceID: "price_123", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // Create subscription response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{ @@ -184,7 +184,7 @@ func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) { Code: "pro", StripeMonthlyPriceID: "price_123", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -226,7 +226,7 @@ func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) { Code: "pro", StripeMonthlyPriceID: "price_123", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -280,7 +280,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in MessageExpiryDuration: time.Hour, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) // No tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // No tier u, err := s.userManager.User("phil") require.Nil(t, err) @@ -461,7 +461,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active( AttachmentTotalSizeLimit: 1000000, AttachmentBandwidthLimit: 1000000, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) require.Nil(t, s.userManager.AddReservation("phil", "ztopic", user.PermissionDenyAll)) @@ -570,7 +570,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) { StripeMonthlyPriceID: "price_1234", ReservationLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) @@ -658,7 +658,7 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) { StripeMonthlyPriceID: "price_456", StripeYearlyPriceID: "price_457", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ StripeCustomerID: "acct_123", @@ -690,7 +690,7 @@ func TestPayments_Subscription_Delete_At_Period_End(t *testing.T) { Return(&stripe.Subscription{}, nil) // Create user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ StripeCustomerID: "acct_123", StripeSubscriptionID: "sub_123", @@ -724,7 +724,7 @@ func TestPayments_CreatePortalSession(t *testing.T) { }, nil) // Create user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ StripeCustomerID: "acct_123", StripeSubscriptionID: "sub_123", diff --git a/server/server_test.go b/server/server_test.go index 75379f8f..88c88d1c 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -411,7 +411,7 @@ func TestServer_PublishAt_FromUser(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfigWithAuthFile(t)) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), "In": "1h", @@ -781,7 +781,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) { c := newTestConfigWithAuthFile(t) s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -795,7 +795,7 @@ func TestServer_Auth_Success_User(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ @@ -809,7 +809,7 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", user.PermissionReadWrite)) @@ -830,7 +830,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": util.BasicAuth("phil", "INVALID"), @@ -843,7 +843,7 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", user.PermissionReadWrite)) // Not mytopic! response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ @@ -857,7 +857,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) { c.AuthDefault = user.PermissionReadWrite // Open by default s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", user.PermissionDenyAll)) require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead)) @@ -906,7 +906,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, false)) u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "some pass")))) response := request(t, s, "GET", u, "", nil) @@ -954,8 +954,8 @@ func TestServer_StatsResetter(t *testing.T) { MessageLimit: 5, MessageExpiryDuration: -5 * time.Second, // Second, what a hack! })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) - require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("tieruser", "test")) // Send an anonymous message @@ -1099,7 +1099,7 @@ func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) { require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "test", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) u, err := s.userManager.User("phil") @@ -1696,7 +1696,7 @@ func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) { MessageLimit: 5, MessageExpiryDuration: -5 * time.Second, // Second, what a hack! })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish to reach message limit @@ -1932,7 +1932,7 @@ func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) { AttachmentExpiryDuration: sevenDays, // 7 days AttachmentBandwidthLimit: 100000, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish and make sure we can retrieve it @@ -1977,7 +1977,7 @@ func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing.T) { AttachmentExpiryDuration: time.Hour, AttachmentBandwidthLimit: 14000, // < 3x5000 bytes -> enough for one upload, one download })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish and make sure we can retrieve it @@ -2015,7 +2015,7 @@ func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) { AttachmentExpiryDuration: 30 * time.Second, AttachmentBandwidthLimit: 1000000, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish small file as anonymous @@ -2237,7 +2237,7 @@ func TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor(t *testing.T) { defer s.closeDatabases() // Create user without tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // Publish a message (anonymous user) rr := request(t, s, "POST", "/mytopic", "hi", nil) diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go index 89a36051..2501916a 100644 --- a/server/server_twilio_test.go +++ b/server/server_twilio_test.go @@ -63,7 +63,7 @@ func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -140,7 +140,7 @@ func TestServer_Twilio_Call_Success(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -185,7 +185,7 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -216,7 +216,7 @@ func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) // Do the thing diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go index c32c7bf8..ab7a20c4 100644 --- a/server/server_webpush_test.go +++ b/server/server_webpush_test.go @@ -96,7 +96,7 @@ func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) { config.AuthDefault = user.PermissionDenyAll s := newTestServer(t, config) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ @@ -126,7 +126,7 @@ func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) { config := configureAuth(t, newTestConfigWithWebPush(t)) s := newTestServer(t, config) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ diff --git a/user/manager.go b/user/manager.go index 9f54625f..d691d42f 100644 --- a/user/manager.go +++ b/user/manager.go @@ -864,14 +864,23 @@ func (a *Manager) resolvePerms(base, perm Permission) error { } // AddUser adds a user with the given username, password and role -func (a *Manager) AddUser(username, password string, role Role) error { +func (a *Manager) AddUser(username, password string, role Role, hashed bool) error { if !AllowedUsername(username) || !AllowedRole(role) { return ErrInvalidArgument } - hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) - if err != nil { - return err + + var hash []byte + var err error = nil + + if hashed { + hash = []byte(password) + } else { + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + if err != nil { + return err + } } + userID := util.RandomStringPrefix(userIDPrefix, userIDLength) syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil { @@ -1192,10 +1201,17 @@ func (a *Manager) ReservationOwner(topic string) (string, error) { } // ChangePassword changes a user's password -func (a *Manager) ChangePassword(username, password string) error { - hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) - if err != nil { - return err +func (a *Manager) ChangePassword(username, password string, hashed bool) error { + var hash []byte + var err error + + if hashed { + hash = []byte(password) + } else { + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + if err != nil { + return err + } } if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil { return err diff --git a/user/manager_test.go b/user/manager_test.go index e9a95b0f..c81b8cab 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -18,9 +18,9 @@ const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this sh func TestManager_FullScenario_Default_DenyAll(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddUser("john", "john", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) + require.Nil(t, a.AddUser("john", "john", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite)) @@ -134,7 +134,7 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) { // and longer ACL rules are prioritized as well. a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "test*", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "*", PermissionRead)) @@ -147,20 +147,20 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) { func TestManager_AddUser_Invalid(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin)) - require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role")) + require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin, false)) + require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role", false)) } func TestManager_AddUser_Timing(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) start := time.Now().UnixMilli() - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) } func TestManager_AddUser_And_Query(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) require.Nil(t, a.ChangeBilling("user", &Billing{ StripeCustomerID: "acct_123", StripeSubscriptionID: "sub_123", @@ -187,7 +187,7 @@ func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) { a := newTestManager(t, PermissionDenyAll) // Create user, add reservations and token - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) require.Nil(t, a.AddReservation("user", "mytopic", PermissionRead)) u, err := a.User("user") @@ -237,7 +237,7 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) { a := newTestManager(t, PermissionDenyAll) // Create user, add reservations and token - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) u, err := a.User("user") require.Nil(t, err) @@ -248,8 +248,8 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) { func TestManager_UserManagement(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite)) @@ -339,21 +339,31 @@ func TestManager_UserManagement(t *testing.T) { func TestManager_ChangePassword(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) + require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) + require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true)) _, err := a.Authenticate("phil", "phil") require.Nil(t, err) - require.Nil(t, a.ChangePassword("phil", "newpass")) + _, err = a.Authenticate("jane", "jane") + require.Nil(t, err) + + require.Nil(t, a.ChangePassword("phil", "newpass", false)) _, err = a.Authenticate("phil", "phil") require.Equal(t, ErrUnauthenticated, err) _, err = a.Authenticate("phil", "newpass") require.Nil(t, err) + + require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true)) + _, err = a.Authenticate("jane", "jane") + require.Equal(t, ErrUnauthenticated, err) + _, err = a.Authenticate("jane", "newpass") + require.Nil(t, err) } func TestManager_ChangeRole(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead)) @@ -378,8 +388,8 @@ func TestManager_ChangeRole(t *testing.T) { func TestManager_Reservations(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll)) require.Nil(t, a.AddReservation("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead)) @@ -460,7 +470,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) { AttachmentTotalSizeLimit: 524288000, AttachmentExpiryDuration: 24 * time.Hour, })) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.ChangeTier("ben", "pro")) require.Nil(t, a.AddReservation("ben", "mytopic", PermissionDenyAll)) @@ -507,7 +517,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) { func TestManager_Token_Valid(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) u, err := a.User("ben") require.Nil(t, err) @@ -551,7 +561,7 @@ func TestManager_Token_Valid(t *testing.T) { func TestManager_Token_Invalid(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length require.Nil(t, u) @@ -570,7 +580,7 @@ func TestManager_Token_NotFound(t *testing.T) { func TestManager_Token_Expire(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) u, err := a.User("ben") require.Nil(t, err) @@ -618,7 +628,7 @@ func TestManager_Token_Expire(t *testing.T) { func TestManager_Token_Extend(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // Try to extend token for user without token u, err := a.User("ben") @@ -647,8 +657,8 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { // Tests that tokens are automatically deleted when the maximum number of tokens is reached a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) ben, err := a.User("ben") require.Nil(t, err) @@ -723,7 +733,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { func TestManager_EnqueueStats_ResetStats(t *testing.T) { a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond) require.Nil(t, err) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // Baseline: No messages or emails u, err := a.User("ben") @@ -765,7 +775,7 @@ func TestManager_EnqueueStats_ResetStats(t *testing.T) { func TestManager_EnqueueTokenUpdate(t *testing.T) { a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 500*time.Millisecond) require.Nil(t, err) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // Create user and token u, err := a.User("ben") @@ -798,7 +808,7 @@ func TestManager_EnqueueTokenUpdate(t *testing.T) { func TestManager_ChangeSettings(t *testing.T) { a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond) require.Nil(t, err) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // No settings u, err := a.User("ben") @@ -866,7 +876,7 @@ func TestManager_Tier_Create_Update_List_Delete(t *testing.T) { AttachmentBandwidthLimit: 21474836480, StripeMonthlyPriceID: "price_2", })) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) require.Nil(t, a.ChangeTier("phil", "pro")) ti, err := a.Tier("pro") @@ -981,7 +991,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) { Name: "Pro", ReservationLimit: 4, })) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) require.Nil(t, a.ChangeTier("phil", "pro")) // Add 10 reservations (pro tier allows that) @@ -1007,7 +1017,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) { func TestUser_PhoneNumberAddListRemove(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) phil, err := a.User("phil") require.Nil(t, err) require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890")) @@ -1032,8 +1042,8 @@ func TestUser_PhoneNumberAddListRemove(t *testing.T) { func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) phil, err := a.User("phil") require.Nil(t, err) ben, err := a.User("ben") From d1ac8d03e0a0971c1857bc525e381ace704ff4ee Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 21 May 2025 18:49:19 -0400 Subject: [PATCH 042/378] Security updates --- cmd/serve.go | 4 +- go.mod | 116 +- go.sum | 539 ++---- log/log.go | 2 +- main.go | 2 +- server/server.go | 2 +- web/package-lock.json | 4050 ++++++++++++++++++++++------------------- 7 files changed, 2449 insertions(+), 2266 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 62e0a14a..f0ea9f5a 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -424,9 +424,9 @@ func execServe(c *cli.Context) error { // Run server s, err := server.New(conf) if err != nil { - log.Fatal(err.Error()) + log.Fatal("%s", err.Error()) } else if err := s.Run(); err != nil { - log.Fatal(err.Error()) + log.Fatal("%s", err.Error()) } log.Info("Exiting.") return nil diff --git a/go.mod b/go.mod index 3806043b..05315a6c 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,27 @@ module heckel.io/ntfy/v2 -go 1.22 +go 1.24 -toolchain go1.22.1 +toolchain go1.24.0 require ( - cloud.google.com/go/firestore v1.17.0 // indirect - cloud.google.com/go/storage v1.43.0 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + cloud.google.com/go/firestore v1.18.0 // indirect + cloud.google.com/go/storage v1.54.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 - github.com/gabriel-vasile/mimetype v1.4.5 + github.com/gabriel-vasile/mimetype v1.4.9 github.com/gorilla/websocket v1.5.3 - github.com/mattn/go-sqlite3 v1.14.23 - github.com/olebedev/when v1.0.0 - github.com/stretchr/testify v1.9.0 - github.com/urfave/cli/v2 v2.27.4 - golang.org/x/crypto v0.27.0 - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 - golang.org/x/term v0.24.0 - golang.org/x/time v0.6.0 - google.golang.org/api v0.199.0 + github.com/mattn/go-sqlite3 v1.14.28 + github.com/olebedev/when v1.1.0 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli/v2 v2.27.6 + golang.org/x/crypto v0.38.0 + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 + golang.org/x/term v0.32.0 + golang.org/x/time v0.11.0 + google.golang.org/api v0.234.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -30,63 +30,75 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi require github.com/pkg/errors v0.9.1 // indirect require ( - firebase.google.com/go/v4 v4.14.1 - github.com/SherClockHolmes/webpush-go v1.3.0 + firebase.google.com/go/v4 v4.15.2 + github.com/SherClockHolmes/webpush-go v1.4.0 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.22.0 github.com/stripe/stripe-go/v74 v74.30.0 ) require ( - cloud.google.com/go v0.115.1 // indirect - cloud.google.com/go/auth v0.9.5 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect - cloud.google.com/go/compute/metadata v0.5.2 // indirect - cloud.google.com/go/iam v1.2.1 // indirect - cloud.google.com/go/longrunning v0.6.1 // indirect + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go v0.121.2 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect + github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/s2a-go v0.1.8 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/klauspost/compress v1.17.10 // indirect - github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.59.1 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect - go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/grpc v1.67.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0dd057b5..6a26dec4 100644 --- a/go.sum +++ b/go.sum @@ -1,345 +1,216 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= -cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA= -cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= -cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= -cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth v0.4.0 h1:vcJWEguhY8KuiHoSs/udg1JtIRYm3YAWPBE1moF1m3U= -cloud.google.com/go/auth v0.4.0/go.mod h1:tO/chJN3obc5AbRYFQDsuFbL4wW5y8LfbPtDCfgwOVE= -cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= -cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= -cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s= -cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= -cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw= -cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= -cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= -cloud.google.com/go/compute v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg= -cloud.google.com/go/compute v1.26.0/go.mod h1:T9RIRap4pVHCGUkVFRJ9hygT3KCXjip41X1GgWtBBII= -cloud.google.com/go/compute v1.27.2 h1:5cE5hdrwJV/92ravlwIFRGnyH9CpLGhh4N0ZDVTU+BA= -cloud.google.com/go/compute v1.28.1 h1:XwPcZjgMCnU2tkwY10VleUjSAfpTj9RDn+kGrbYsi8o= -cloud.google.com/go/compute v1.28.1/go.mod h1:b72iXMY4FucVry3NR3Li4kVyyTvbMDE7x5WsqvxjsYk= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= -cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= -cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= -cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= -cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw= -cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ= -cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= -cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= -cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= -cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= -cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= -cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= -cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E= -cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics= -cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= -cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= -cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= -cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= -cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= -cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= -firebase.google.com/go/v4 v4.14.0 h1:Tc9jWzMUApUFUA5UUx/HcBeZ+LPjlhG2vNRfWJrcMwU= -firebase.google.com/go/v4 v4.14.0/go.mod h1:pLATyL6xH2o9AMe7rqHdmmOUE/Ph7wcwepIs+uiEKPg= -firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= -firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg= +cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= +cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI+0= +cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +firebase.google.com/go/v4 v4.15.2 h1:KJtV4rAfO2CVCp40hBfVk+mqUqg7+jQKx7yOgFDnXBg= +firebase.google.com/go/v4 v4.15.2/go.mod h1:qkD/HtSumrPMTLs0ahQrje5gTw2WKFKrzVFoqy4SbKA= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= -github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= -github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= +github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= +github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= -github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= +github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= -github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= -github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= -github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= -github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= -github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= -github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w= -github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= +github.com/olebedev/when v1.1.0 h1:dlpoRa7huImhNtEx4yl0WYfTHVEWmJmIWd7fEkTHayc= +github.com/olebedev/when v1.1.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= -github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= -github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= -github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= -github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= -github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= -github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= -go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= -go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= -go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= -go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -347,26 +218,23 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -374,107 +242,38 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4= -google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= -google.golang.org/api v0.178.0 h1:yoW/QMI4bRVCHF+NWOTa4cL8MoWL3Jnuc7FlcFF91Ok= -google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U= -google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= -google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= -google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= -google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= -google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs= -google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4= +google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be h1:g4aX8SUFA8V5F4LrSY5EclyGYw1OZN4HS1jTyjB9ZDc= -google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be/go.mod h1:FeSdT5fk+lkxatqJP38MsUicGqHax5cLtmy/6TAuxO4= -google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae h1:HjgkYCl6cWQEKSHkpUp4Q8VB74swzyBwTz1wtTzahm0= -google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:i4np6Wrjp8EujFAUn0CM0SH+iZhY1EbrfzEIJbFkHFM= -google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 h1:XpH03M6PDRKTo1oGfZBXu2SzwcbfxUokgobVinuUZoU= -google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8/go.mod h1:OLh2Ylz+WlYAJaSBRpJIJLP8iQP+8da+fpxbwNEAV/o= -google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc= -google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= -google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61 h1:KipVMxePgXPFBzXOvpKbny3RVdVmJOD64R/Ob7GPWEs= -google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:HiAZQz/G7n0EywFjmncAwsfnmFm2bjm7qPjwl8hyzjM= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae h1:c55+MER4zkBS14uJhSZMGGmya0yJx5iHV4x/fpOSNRk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= -google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 h1:2zGWyk04EwQ3mmV4dd4M4U7P/igHi5p7CBJEg1rI6A8= +google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237/go.mod h1:LhI4bRmX3rqllzQ+BGneexULkEjBf2gsAfkbeCA8IbU= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -483,5 +282,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/log/log.go b/log/log.go index 20ad6151..98d9652f 100644 --- a/log/log.go +++ b/log/log.go @@ -198,7 +198,7 @@ func (w *peekLogWriter) Write(p []byte) (n int, err error) { if len(p) == 0 || p[0] == '{' || CurrentFormat() == TextFormat { return w.w.Write(p) } - m := newEvent().Tag(tagStdLog).Render(InfoLevel, strings.TrimSpace(string(p))) + m := newEvent().Tag(tagStdLog).Render(InfoLevel, "%s", strings.TrimSpace(string(p))) if m == "" { return 0, nil } diff --git a/main.go b/main.go index d4600dc8..d23072d5 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ If you want to chat, simply join the Discord server (https://discord.gg/cT7ECsZj the Matrix room (https://matrix.to/#/#ntfy:matrix.org). ntfy %s (%s), runtime %s, built at %s -Copyright (C) 2022 Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2 +Copyright (C) Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2 `, version, commit[:7], runtime.Version(), date) app := cmd.New() diff --git a/server/server.go b/server/server.go index ee2da76a..7e542d85 100644 --- a/server/server.go +++ b/server/server.go @@ -1016,7 +1016,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) } } contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") diff --git a/web/package-lock.json b/web/package-lock.json index e2f51f61..813da25a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -50,6 +50,7 @@ "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" @@ -59,42 +60,46 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -113,56 +118,48 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -171,17 +168,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", - "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -192,13 +190,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", - "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -209,10 +208,11 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -225,40 +225,42 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -268,35 +270,38 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", - "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-wrap-function": "^7.25.0", - "@babel/traverse": "^7.25.0" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -306,14 +311,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", - "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -322,104 +328,84 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", - "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -429,13 +415,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", - "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.3" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -445,12 +432,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", - "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -460,12 +448,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", - "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -475,14 +464,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -492,13 +482,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", - "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.0" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -512,6 +503,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" }, @@ -519,76 +511,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", - "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -598,138 +528,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", - "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -743,6 +548,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -755,12 +561,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -770,15 +577,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", - "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-remap-async-to-generator": "^7.25.0", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/traverse": "^7.25.4" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -788,14 +595,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", - "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -805,12 +613,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -820,12 +629,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", - "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -835,13 +645,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", - "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -851,14 +662,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -868,16 +679,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", - "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { @@ -888,13 +700,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -904,12 +717,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -919,13 +733,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -935,12 +750,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -950,13 +766,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", - "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -966,13 +783,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -982,13 +799,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -998,13 +815,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1014,13 +831,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1030,14 +848,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", - "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.1" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1047,13 +866,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1063,12 +882,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", - "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1078,13 +898,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1094,12 +914,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1109,13 +930,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1125,14 +947,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1142,15 +964,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", - "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1160,13 +983,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1176,13 +1000,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1192,12 +1017,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1207,13 +1033,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1223,13 +1049,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1239,15 +1065,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", + "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1257,13 +1084,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1273,13 +1101,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1289,14 +1117,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1306,12 +1134,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1321,13 +1150,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", - "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1337,15 +1167,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1355,12 +1185,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1370,12 +1201,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1385,12 +1217,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1400,13 +1233,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1415,13 +1248,31 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1431,12 +1282,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1446,13 +1298,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1462,12 +1315,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1477,12 +1331,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1492,12 +1347,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1507,12 +1363,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1522,13 +1379,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1538,13 +1396,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1554,13 +1413,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", - "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1570,93 +1430,80 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", - "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.4", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.25.1", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.25.2", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-modules-systemjs": "^7.25.0", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.25.4", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.8", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.37.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -1671,6 +1518,7 @@ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -1680,46 +1528,40 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1728,28 +1570,29 @@ } }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@emotion/babel-plugin": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", - "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.2.0", + "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", @@ -1761,16 +1604,18 @@ "node_modules/@emotion/babel-plugin/node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" }, "node_modules/@emotion/cache": { - "version": "11.13.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } @@ -1778,17 +1623,20 @@ "node_modules/@emotion/cache/node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1796,19 +1644,21 @@ "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" }, "node_modules/@emotion/react": { - "version": "11.13.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", - "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, @@ -1822,33 +1672,36 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", - "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.1", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", - "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", + "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.0", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0" + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", @@ -1863,25 +1716,29 @@ "node_modules/@emotion/unitless": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", @@ -1891,6 +1748,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1907,6 +1765,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1923,6 +1782,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1939,6 +1799,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1955,6 +1816,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1971,6 +1833,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1987,6 +1850,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2003,6 +1867,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2019,6 +1884,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2035,6 +1901,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2051,6 +1918,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2067,6 +1935,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2083,6 +1952,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2099,6 +1969,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2115,6 +1986,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2131,6 +2003,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2147,6 +2020,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2163,6 +2037,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2179,6 +2054,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -2195,6 +2071,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2211,6 +2088,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2227,6 +2105,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2236,25 +2115,30 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2264,6 +2148,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2287,6 +2172,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2302,6 +2188,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2314,6 +2201,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2324,6 +2212,7 @@ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -2338,6 +2227,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -2351,12 +2241,14 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "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==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2370,6 +2262,7 @@ "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" } @@ -2378,6 +2271,7 @@ "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" } @@ -2387,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2395,12 +2290,14 @@ "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==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "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" @@ -2410,6 +2307,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz", "integrity": "sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==", + "license": "BSD-2-Clause", "dependencies": { "unist-util-visit": "^1.4.1" }, @@ -2418,18 +2316,20 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", - "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz", + "integrity": "sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", - "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.17.1.tgz", + "integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -2442,8 +2342,8 @@ }, "peerDependencies": { "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2452,21 +2352,22 @@ } }, "node_modules/@mui/material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", - "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", + "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.7", - "@mui/system": "^5.16.7", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", + "@mui/core-downloads-tracker": "^5.17.1", + "@mui/system": "^5.17.1", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.3.1", + "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "engines": { @@ -2479,9 +2380,9 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -2496,12 +2397,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", - "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.6", + "@mui/utils": "^5.17.1", "prop-types": "^15.8.1" }, "engines": { @@ -2512,8 +2414,8 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2522,12 +2424,13 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", - "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", + "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", + "@emotion/cache": "^11.13.5", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, @@ -2541,7 +2444,7 @@ "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -2553,15 +2456,16 @@ } }, "node_modules/@mui/system": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", - "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", + "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.6", - "@mui/styled-engine": "^5.16.6", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.16.14", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -2576,8 +2480,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -2592,9 +2496,10 @@ } }, "node_modules/@mui/types": { - "version": "7.2.17", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", - "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -2605,16 +2510,17 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", - "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/types": "^7.2.15", + "@mui/types": "~7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "react-is": "^19.0.0" }, "engines": { "node": ">=12.0.0" @@ -2624,8 +2530,8 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2638,6 +2544,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2651,6 +2558,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -2660,6 +2568,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2672,15 +2581,17 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, "node_modules/@remix-run/router": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", - "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -2689,13 +2600,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", @@ -2708,6 +2621,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -2717,10 +2631,11 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -2730,16 +2645,18 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } @@ -2748,55 +2665,63 @@ "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mdast": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", "dependencies": { "@types/unist": "^2" } }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", - "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", + "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "license": "MIT", + "peer": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", - "dependencies": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { "@types/react": "*" } }, @@ -2805,6 +2730,7 @@ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -2813,43 +2739,48 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2862,6 +2793,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2871,6 +2803,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2887,44 +2820,53 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -2938,6 +2880,7 @@ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2958,6 +2901,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2974,17 +2918,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2994,15 +2940,16 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3012,15 +2959,16 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3034,6 +2982,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -3046,19 +2995,19 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -3071,19 +3020,32 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 4.0.0" } @@ -3093,6 +3055,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -3104,10 +3067,11 @@ } }, "node_modules/axe-core": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", - "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", "dev": true, + "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -3117,6 +3081,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -3125,6 +3090,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -3136,13 +3102,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { @@ -3150,25 +3117,27 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3178,6 +3147,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3187,13 +3157,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3204,6 +3176,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -3212,9 +3185,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", - "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -3230,11 +3203,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001663", - "electron-to-chromium": "^1.5.28", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -3247,13 +3221,15 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -3262,16 +3238,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3284,14 +3291,15 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001664", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", - "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -3306,33 +3314,31 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3342,6 +3348,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3351,6 +3358,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3360,27 +3368,36 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, "node_modules/comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3390,13 +3407,15 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -3405,26 +3424,30 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.38.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", - "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", "dev": true, + "license": "MIT", "dependencies": { - "browserslist": "^4.23.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -3435,6 +3458,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -3450,15 +3474,17 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "license": "MIT", "dependencies": { "node-fetch": "2.6.7" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3473,6 +3499,7 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3481,6 +3508,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.3.0.tgz", "integrity": "sha512-ZZXXn51SnxRxAZ6fdY7mBDPmA4OZd83q/J9Gdqz3YmE9TUq+9tZl+tdOnCi7PpNygI6PEkehj9rgifv5+W8a5A==", + "license": "Apache-2.0", "engines": { "node": ">=10.0.0" } @@ -3488,23 +3516,26 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3514,29 +3545,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -3548,9 +3581,10 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3563,49 +3597,19 @@ } } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "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==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3615,6 +3619,7 @@ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3632,6 +3637,7 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3648,6 +3654,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.7.tgz", "integrity": "sha512-2a+BXvVhY5op+smDRLxeBAivE7YcYaneXJ1la3HOkUfX9zKkE/AJ8CNgjiXbtXepFyFmJNGSbmjOwqbT749r/w==", + "license": "Apache-2.0", "engines": { "node": ">=6.0" } @@ -3656,6 +3663,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz", "integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==", + "license": "Apache-2.0", "peerDependencies": { "@types/react": ">=16", "dexie": "^3.2 || ^4.0.1-alpha", @@ -3667,6 +3675,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -3678,16 +3687,33 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -3699,21 +3725,24 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.29", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", - "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", - "dev": true + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -3722,62 +3751,69 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", "dependencies": { "stackframe": "^1.3.4" } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.10", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", + "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -3787,13 +3823,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3803,60 +3837,45 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -3865,37 +3884,44 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -3910,6 +3936,7 @@ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -3946,6 +3973,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3954,6 +3982,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -3965,7 +3994,9 @@ "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4021,6 +4052,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", "dev": true, + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", @@ -4042,6 +4074,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, + "license": "MIT", "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", @@ -4061,6 +4094,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4073,6 +4107,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -4084,6 +4119,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -4093,6 +4129,7 @@ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -4110,15 +4147,17 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", - "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4128,7 +4167,7 @@ "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.9.0", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", @@ -4137,13 +4176,14 @@ "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -4151,6 +4191,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -4160,6 +4201,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -4168,12 +4210,13 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", - "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "license": "MIT", "dependencies": { - "aria-query": "~5.1.3", + "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", @@ -4181,14 +4224,13 @@ "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.19", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.0" + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" @@ -4198,28 +4240,29 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.0.tgz", - "integrity": "sha512-IHBePmfWH5lKhJnJ7WB1V+v/GolbB0rjS8XYVCSQCZKaQCAUhMoVoOEn1Ef8Z8Wf0a7l8KTJvuZg5/e4qrZ6nA==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", + "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", + "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "engines": { @@ -4234,6 +4277,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4246,6 +4290,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -4258,6 +4303,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4275,6 +4321,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4291,6 +4338,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4298,60 +4346,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/eslint/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -4362,32 +4362,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -4400,6 +4380,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -4417,6 +4398,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -4429,6 +4411,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4441,6 +4424,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -4449,13 +4433,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -4463,25 +4449,28 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -4492,6 +4481,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4503,25 +4493,39 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", - "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", - "dev": true + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -4531,6 +4535,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -4543,6 +4548,7 @@ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } @@ -4552,6 +4558,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4561,6 +4568,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4573,6 +4581,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4583,13 +4592,15 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4606,6 +4617,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -4616,18 +4628,26 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/fs-extra": { @@ -4635,6 +4655,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -4649,7 +4670,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -4657,6 +4679,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -4669,20 +4692,24 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -4696,6 +4723,7 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4705,21 +4733,28 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4732,17 +4767,33 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4757,6 +4808,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4777,6 +4829,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -4788,6 +4841,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -4797,6 +4851,7 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -4809,12 +4864,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4824,29 +4880,37 @@ "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 + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { @@ -4854,6 +4918,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -4862,10 +4927,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -4874,10 +4943,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4890,6 +4960,7 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -4904,6 +4975,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -4915,6 +4987,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", "integrity": "sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.3", "comma-separated-tokens": "^1.0.0", @@ -4933,6 +5006,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -4940,20 +5014,23 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", "dependencies": { "void-elements": "3.1.0" } }, "node_modules/humanize-duration": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.1.tgz", - "integrity": "sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==" + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.2.tgz", + "integrity": "sha512-jcTwWYeCJf4dN5GJnjBmHd42bNyK94lY49QTkrsAQrMTUoIYLevvDpmQtg5uv8ZrdIRIbzdasmSNZ278HHUPEg==", + "license": "Unlicense" }, "node_modules/i18next": { "version": "21.10.0", @@ -4973,6 +5050,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.2" } @@ -4981,6 +5059,7 @@ "version": "6.1.8", "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.8.tgz", "integrity": "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.19.0" } @@ -4989,6 +5068,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.5.tgz", "integrity": "sha512-tLuHWuLWl6CmS07o+UB6EcQCaUjrZ1yhdseIN7sfq0u7phsMePJ8pqlGhIAdRDPF/q7ooyo5MID5DRFBCH+x5w==", + "license": "MIT", "dependencies": { "cross-fetch": "3.1.5" } @@ -4997,21 +5077,24 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5028,6 +5111,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -5038,6 +5122,7 @@ "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, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5047,22 +5132,25 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "license": "MIT" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5072,6 +5160,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5081,6 +5170,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -5090,30 +5180,16 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5125,15 +5201,21 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5143,25 +5225,30 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5188,6 +5275,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "engines": { "node": ">=4" } @@ -5197,6 +5285,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5205,9 +5294,10 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -5219,11 +5309,14 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -5234,12 +5327,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5252,6 +5347,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5262,29 +5358,38 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5298,6 +5403,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -5309,6 +5415,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5319,6 +5426,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5330,36 +5438,28 @@ "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-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5373,6 +5473,7 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5382,6 +5483,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5390,18 +5492,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5415,6 +5521,7 @@ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5424,6 +5531,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5432,12 +5540,13 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -5451,6 +5560,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5459,12 +5569,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5474,12 +5586,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5489,12 +5604,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -5508,6 +5624,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5516,25 +5633,30 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5547,25 +5669,32 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jake": { @@ -5573,6 +5702,7 @@ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -5586,81 +5716,12 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -5670,42 +5731,24 @@ "node": ">= 10.13.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "license": "BSD-3-Clause" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -5714,50 +5757,57 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -5770,6 +5820,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -5782,6 +5833,7 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5791,6 +5843,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -5806,6 +5859,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -5814,13 +5868,15 @@ "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, + "license": "MIT", "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -5833,6 +5889,7 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5842,6 +5899,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5853,13 +5911,15 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -5874,30 +5934,35 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -5910,6 +5975,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -5919,14 +5985,26 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "dev": true, + "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", + "license": "MIT", "dependencies": { "unist-util-visit": "^2.0.0" }, @@ -5939,6 +6017,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0", @@ -5953,6 +6032,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0" @@ -5966,6 +6046,7 @@ "version": "0.8.5", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-string": "^2.0.0", @@ -5982,6 +6063,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz", "integrity": "sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -6001,6 +6083,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0", @@ -6015,6 +6098,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0" @@ -6028,6 +6112,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -6036,19 +6121,22 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -6067,6 +6155,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "debug": "^4.0.0", "parse-entities": "^2.0.0" @@ -6077,6 +6166,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -6090,6 +6180,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6102,6 +6193,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6109,12 +6201,13 @@ "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==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -6122,6 +6215,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6133,12 +6227,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -6155,40 +6251,27 @@ } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6201,19 +6284,23 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -6224,14 +6311,16 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -6242,6 +6331,7 @@ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6260,6 +6350,7 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6270,12 +6361,14 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -6291,6 +6384,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -6300,6 +6394,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -6312,11 +6407,30 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6332,6 +6446,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -6346,6 +6461,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -6357,6 +6473,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -6374,6 +6491,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -6392,6 +6510,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6401,6 +6520,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6410,6 +6530,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6417,26 +6538,30 @@ "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==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -6445,18 +6570,19 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -6472,9 +6598,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -6486,6 +6613,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -6495,6 +6623,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -6510,6 +6639,7 @@ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true, + "license": "MIT", "engines": { "node": "^14.13.1 || >=16.0.0" }, @@ -6521,6 +6651,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -6530,12 +6661,14 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", "dependencies": { "xtend": "^4.0.0" }, @@ -6549,6 +6682,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6571,44 +6705,45 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.0" } }, "node_modules/react-i18next": { "version": "11.18.6", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.5", "html-parse-stringify": "^3.0.1" @@ -6630,6 +6765,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "license": "MIT", "dependencies": { "throttle-debounce": "^2.1.0" }, @@ -6638,15 +6774,17 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6655,6 +6793,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-remark/-/react-remark-2.1.0.tgz", "integrity": "sha512-7dEPxRGQ23sOdvteuRGaQAs9cEOH/BOeCN4CqsJdk3laUDIDYRCWnM6a3z92PzXHUuxIRLXQNZx7SiO0ijUcbw==", + "license": "MIT", "dependencies": { "rehype-react": "^6.0.0", "remark-parse": "^9.0.0", @@ -6673,11 +6812,12 @@ } }, "node_modules/react-router": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", - "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -6687,12 +6827,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", - "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2", - "react-router": "6.26.2" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -6706,6 +6847,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -6718,18 +6860,20 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -6742,13 +6886,15 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -6756,30 +6902,19 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -6789,15 +6924,16 @@ } }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -6805,31 +6941,44 @@ "node": ">=4" } }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/rehype-react": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz", "integrity": "sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==", + "license": "MIT", "dependencies": { "@mapbox/hast-util-table-cell-style": "^0.2.0", "hast-to-hyperscript": "^9.0.0" @@ -6843,6 +6992,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^0.8.0" }, @@ -6855,6 +7005,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz", "integrity": "sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==", + "license": "MIT", "dependencies": { "mdast-util-to-hast": "^10.2.0" }, @@ -6868,22 +7019,27 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6892,15 +7048,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6912,6 +7070,7 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -6927,6 +7086,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -6957,19 +7117,22 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -6997,17 +7160,36 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -7017,18 +7199,17 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -7038,6 +7219,7 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -7047,6 +7229,7 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7064,6 +7247,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7074,11 +7258,27 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7091,20 +7291,79 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -7117,6 +7376,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7126,6 +7386,7 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7135,6 +7396,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -7145,6 +7407,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7154,12 +7417,14 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/space-separated-tokens": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7169,6 +7434,7 @@ "version": "2.0.10", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "license": "MIT", "dependencies": { "stackframe": "^1.3.4" } @@ -7176,12 +7442,14 @@ "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" }, "node_modules/stacktrace-gps": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "license": "MIT", "dependencies": { "source-map": "0.5.6", "stackframe": "^1.3.4" @@ -7191,6 +7459,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7199,52 +7468,48 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "license": "MIT", "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, + "license": "MIT", "dependencies": { - "internal-slot": "^1.0.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" }, "engines": { "node": ">= 0.4" } }, - "node_modules/string.prototype.includes": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz", - "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7258,21 +7523,26 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7282,15 +7552,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7300,6 +7575,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -7317,6 +7593,7 @@ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", @@ -7331,6 +7608,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7343,6 +7621,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7352,6 +7631,7 @@ "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -7361,6 +7641,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -7372,19 +7653,22 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "license": "MIT", "dependencies": { "inline-style-parser": "0.1.1" } }, "node_modules/stylis": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", - "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" }, "node_modules/stylis-plugin-rtl": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/stylis-plugin-rtl/-/stylis-plugin-rtl-2.1.1.tgz", "integrity": "sha512-q6xIkri6fBufIO/sV55md2CbgS5c6gg9EhSVATtHHCdOnbN/jcI0u3lYhNVeuI65c4lQPo67g8xmq5jrREvzlg==", + "license": "MIT", "dependencies": { "cssjanus": "^2.0.1" }, @@ -7393,20 +7677,23 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "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==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7419,6 +7706,7 @@ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7428,6 +7716,7 @@ "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", "dev": true, + "license": "MIT", "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", @@ -7442,13 +7731,14 @@ } }, "node_modules/terser": { - "version": "5.34.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", - "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -7463,29 +7753,24 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/throttle-debounce": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7496,12 +7781,14 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/trough": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7512,6 +7799,7 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -7524,6 +7812,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -7536,6 +7825,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7548,6 +7838,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -7556,30 +7847,32 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -7589,17 +7882,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -7609,17 +7904,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -7629,31 +7925,37 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7663,6 +7965,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, + "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -7676,6 +7979,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7685,6 +7989,7 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7693,6 +7998,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "license": "MIT", "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -7711,6 +8017,7 @@ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^2.0.0" }, @@ -7722,6 +8029,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7731,6 +8039,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7740,6 +8049,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7749,6 +8059,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7758,6 +8069,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.2" }, @@ -7770,6 +8082,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "license": "MIT", "dependencies": { "unist-util-visit-parents": "^2.0.0" } @@ -7778,6 +8091,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "license": "MIT", "dependencies": { "unist-util-is": "^3.0.0" } @@ -7785,13 +8099,15 @@ "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -7801,15 +8117,16 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4", "yarn": "*" } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -7825,9 +8142,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7841,6 +8159,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -7849,6 +8168,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", @@ -7864,6 +8184,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" @@ -7874,10 +8195,11 @@ } }, "node_modules/vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -7933,6 +8255,7 @@ "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.15.2.tgz", "integrity": "sha512-l1srtaad5NMNrAtAuub6ArTYG5Ci9AwofXXQ6IsbpCMYQ/0HUndwI7RB2x95+1UBFm7VGttQtT7woBlVnNhBRw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4", "fast-glob": "^3.2.12", @@ -7953,6 +8276,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7961,6 +8285,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7969,12 +8294,14 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -7985,6 +8312,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -7996,39 +8324,45 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", - "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", + "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8042,6 +8376,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -8056,15 +8391,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -8079,6 +8417,7 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8088,6 +8427,7 @@ "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", "dev": true, + "license": "MIT", "dependencies": { "idb": "^7.0.1", "workbox-core": "6.6.0" @@ -8098,6 +8438,7 @@ "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0" } @@ -8107,6 +8448,7 @@ "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", "dev": true, + "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.11.1", @@ -8155,6 +8497,7 @@ "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", "dev": true, + "license": "MIT", "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", @@ -8172,6 +8515,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" @@ -8195,6 +8539,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", "@types/resolve": "1.17.1", @@ -8215,6 +8560,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" @@ -8228,6 +8574,7 @@ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", @@ -8245,6 +8592,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8260,13 +8608,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -8279,6 +8629,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -8295,6 +8646,7 @@ "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "jest-worker": "^26.2.1", @@ -8310,6 +8662,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, @@ -8322,6 +8675,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^2.1.0" } @@ -8330,13 +8684,15 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/workbox-build/node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, + "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", @@ -8349,6 +8705,7 @@ "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", "deprecated": "workbox-background-sync@6.6.0", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0" } @@ -8357,13 +8714,15 @@ "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/workbox-expiration": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", "dev": true, + "license": "MIT", "dependencies": { "idb": "^7.0.1", "workbox-core": "6.6.0" @@ -8375,6 +8734,7 @@ "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", "dev": true, + "license": "MIT", "dependencies": { "workbox-background-sync": "6.6.0", "workbox-core": "6.6.0", @@ -8387,6 +8747,7 @@ "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0" } @@ -8396,6 +8757,7 @@ "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0", "workbox-routing": "6.6.0", @@ -8407,6 +8769,7 @@ "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0" } @@ -8416,6 +8779,7 @@ "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", "dev": true, + "license": "MIT", "dependencies": { "workbox-cacheable-response": "6.6.0", "workbox-core": "6.6.0", @@ -8430,6 +8794,7 @@ "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0" } @@ -8439,6 +8804,7 @@ "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0" } @@ -8448,6 +8814,7 @@ "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "6.6.0", "workbox-routing": "6.6.0" @@ -8457,13 +8824,15 @@ "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/workbox-window": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", "dev": true, + "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "6.6.0" @@ -8473,12 +8842,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } @@ -8487,12 +8858,14 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", "engines": { "node": ">= 6" } @@ -8502,6 +8875,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, From bd192edf1e2a2c953cef2b6f79bb1a799bce2371 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 21 May 2025 18:52:45 -0400 Subject: [PATCH 043/378] Release notes --- docs/releases.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/releases.md b/docs/releases.md index 69222b82..5d0f0be9 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1382,6 +1382,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** * Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [pcouy](https://github.com/pcouy)) +* Security updates for dependencies and Docker images **Documentation:** From 0ad266a495356acaf6d8fada12421919b2a14a2a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 21 May 2025 18:53:29 -0400 Subject: [PATCH 044/378] Derp --- docs/releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index 5d0f0be9..002e0d06 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1382,7 +1382,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** * Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [pcouy](https://github.com/pcouy)) -* Security updates for dependencies and Docker images +* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) **Documentation:** From 3f21da7768f29e5707d18fd85c3bb29994e101b9 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 21 May 2025 18:55:21 -0400 Subject: [PATCH 045/378] Pipelines --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/test.yaml | 2 +- Dockerfile-build | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b6dc8ddb..72b9e360 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.22.x' + go-version: '1.24.x' - name: Install node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 80155e5b..70a70552 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,7 +12,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.22.x' + go-version: '1.24.x' - name: Install node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b0f99ffd..cfd9d754 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,7 +9,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.22.x' + go-version: '1.24.x' - name: Install node uses: actions/setup-node@v3 with: diff --git a/Dockerfile-build b/Dockerfile-build index 4530ec47..832b85bf 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -1,4 +1,4 @@ -FROM golang:1.22-bullseye as builder +FROM golang:1.24-bullseye as builder ARG VERSION=dev ARG COMMIT=unknown From 1569c22a65898526c834c3be041212ca81964a9a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 21 May 2025 20:02:06 -0400 Subject: [PATCH 046/378] Fix some broken links in the docs --- docs/config.md | 2 +- docs/develop.md | 2 +- docs/examples.md | 54 +++++++++++++++++++------------------------ docs/publish.md | 4 ++-- docs/releases.md | 9 ++++++-- docs/subscribe/api.md | 4 ++-- 6 files changed, 37 insertions(+), 38 deletions(-) diff --git a/docs/config.md b/docs/config.md index d4766cde..d6438e16 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1382,7 +1382,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | -| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) | +| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) | | `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) | | `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) | | `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | diff --git a/docs/develop.md b/docs/develop.md index e343503b..43ac2d4f 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -384,7 +384,7 @@ strictly based off of my development on this app. There may be other versions of ### Apple setup !!! info - Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required + Along with this step, the [PLIST Deployment](#plist-config) step is also required for these changes to take effect in the iOS app. 1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add) diff --git a/docs/examples.md b/docs/examples.md index 18523716..343de120 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -639,44 +639,39 @@ or by simply providing traccar with a valid username/password combination. This example provides a simple way to send notifications using [ntfy.sh](https://ntfy.sh) when a terminal command completes. It includes success or failure indicators based on the command's exit status. -### Setup - -1. Store your ntfy.sh bearer token securely if access control is enabled: +Store your ntfy.sh bearer token securely if access control is enabled: ```sh echo "your_bearer_token_here" > ~/.ntfy_token chmod 600 ~/.ntfy_token ``` -1. Add the following function and alias to your `.bashrc` or `.bash_profile`: +Add the following function and alias to your `.bashrc` or `.bash_profile`: - ```sh - # Function for alert notifications using ntfy.sh - notify_via_ntfy() { - local exit_status=$? # Capture the exit status before doing anything else - local token=$(< ~/.ntfy_token) # Securely read the token - local status_icon="$([ $exit_status -eq 0 ] && echo magic_wand || echo warning)" - local last_command=$(history | tail -n1 | sed -e 's/^[[:space:]]*[0-9]\{1,\}[[:space:]]*//' -e 's/[;&|][[:space:]]*alert$//') + ```sh + # Function for alert notifications using ntfy.sh + notify_via_ntfy() { + local exit_status=$? # Capture the exit status before doing anything else + local token=$(< ~/.ntfy_token) # Securely read the token + local status_icon="$([ $exit_status -eq 0 ] && echo magic_wand || echo warning)" + local last_command=$(history | tail -n1 | sed -e 's/^[[:space:]]*[0-9]\{1,\}[[:space:]]*//' -e 's/[;&|][[:space:]]*alert$//') - curl -s -X POST "https://n.example.dev/alerts" \ - -H "Authorization: Bearer $token" \ - -H "Title: Terminal" \ - -H "X-Priority: 3" \ - -H "Tags: $status_icon" \ - -d "Command: $last_command (Exit: $exit_status)" + curl -s -X POST "https://n.example.dev/alerts" \ + -H "Authorization: Bearer $token" \ + -H "Title: Terminal" \ + -H "X-Priority: 3" \ + -H "Tags: $status_icon" \ + -d "Command: $last_command (Exit: $exit_status)" - echo "Tags: $status_icon" - echo "$last_command (Exit: $exit_status)" - } + echo "Tags: $status_icon" + echo "$last_command (Exit: $exit_status)" + } - # Add an "alert" alias for long running commands using ntfy.sh - alias alert='notify_via_ntfy' + # Add an "alert" alias for long running commands using ntfy.sh + alias alert='notify_via_ntfy' + ``` - ``` - -### Usage - -Run any long-running command and append `alert` to notify when it completes: +Now you can run any long-running command and append `alert` to notify when it completes: ```sh sleep 10; alert @@ -685,11 +680,10 @@ sleep 10; alert **Notification Sent** with a success 🪄 (`magic_wand`) or failure ⚠️ (`warning`) tag. -#### Simulating Failures - To test failure notifications: ```sh false; alert # Always fails (exit 1) ls --invalid; alert # Invalid option -cat nonexistent_file; alert # File not found \ No newline at end of file +cat nonexistent_file; alert # File not found +``` \ No newline at end of file diff --git a/docs/publish.md b/docs/publish.md index 37b46809..25bff035 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1243,7 +1243,7 @@ all the supported fields: | `priority` | - | *int (one of: 1, 2, 3, 4, or 5)* | `4` | Message [priority](#message-priority) with 1=min, 3=default and 5=max | | `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications | | `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) | -| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-url) | +| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-a-url) | | `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted | | `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) | | `filename` | - | *string* | `file.jpg` | File name of the attachment | @@ -3094,7 +3094,7 @@ may be read/write protected so that only users with the correct credentials can To publish/subscribe to protected topics, you can: * Use [username & password](#username-password) via Basic auth, e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` -* Use [access tokens](#bearer-auth) via Bearer/Basic auth, e.g. `Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2` +* Use [access tokens](#access-tokens) via Bearer/Basic auth, e.g. `Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2` * or use either with the [`auth` query parameter](#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw` !!! warning diff --git a/docs/releases.md b/docs/releases.md index 002e0d06..65af14d5 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -689,7 +689,7 @@ minute or so, due to competing stats gathering (personal installations will like **Features:** -* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#wal-for-message-cache) (no ticket) +* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#message-cache) (no ticket) * ntfy CLI can now [wait for a command or PID](subscribe/cli.md#wait-for-pidcommand) before publishing ([#263](https://github.com/binwiederhier/ntfy/issues/263), thanks to the [original ntfy](https://github.com/dschep/ntfy) for the idea) * Trace: Log entire HTTP request to simplify debugging (no ticket) * Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3)) @@ -1381,16 +1381,21 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** -* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [pcouy](https://github.com/pcouy)) +* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) * Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) +* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) +* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) +* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) **Documentation:** +* Lots of new integrations: [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp), [UptimeObserver](https://uptimeobserver.com), [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay), [Monibot](https://monibot.io/), ... Amazing! * Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) * Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) * Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) * Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode)) * Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) +* Lots of other tiny docs updates, tanks to everyone who contributed! ### ntfy Android app v1.16.1 (UNRELEASED) diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index 3f1c0e81..5dad35b4 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -132,7 +132,7 @@ easy to use. Here's what it looks like. You may also want to check out the [full ### Subscribe as raw stream The `/raw` endpoint will output one line per message, and **will only include the message body**. It's useful for extremely simple scripts, and doesn't include all the data. Additional fields such as [priority](../publish.md#message-priority), -[tags](../publish.md#tags--emojis--) or [message title](../publish.md#message-title) are not included in this output +[tags](../publish.md#tags-emojis) or [message title](../publish.md#message-title) are not included in this output format. Keepalive messages are sent as empty lines. === "Command line (curl)" @@ -305,7 +305,7 @@ Depending on whether the server is configured to support [access control](../con may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can: -* Use [basic auth](../publish.md#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` +* Use [basic auth](../publish.md#authentication), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` * or use the [`auth` query parameter](../publish.md#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw` Please refer to the [publishing documentation](../publish.md#authentication) for additional details. From 7aab7d387f72b5ec6ac54faa4092f59fb03deb42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 00:04:33 +0000 Subject: [PATCH 047/378] Bump esbuild, vite and vite-plugin-pwa in /web Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.4 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa). These dependencies need to be updated together. Updates `esbuild` from 0.18.20 to 0.25.4 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.18.20...v0.25.4) Updates `vite` from 4.5.14 to 6.3.5 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite) Updates `vite-plugin-pwa` from 0.15.2 to 1.0.0 - [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases) - [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v0.15.2...v1.0.0) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.25.4 dependency-type: indirect - dependency-name: vite dependency-version: 6.3.5 dependency-type: direct:development - dependency-name: vite-plugin-pwa dependency-version: 1.0.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- web/package-lock.json | 1222 ++++++++++++++++++++++++++--------------- web/package.json | 4 +- 2 files changed, 793 insertions(+), 433 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 813da25a..7a08b299 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.4.2", - "@mui/material": "latest", + "@mui/material": "*", "dexie": "^3.2.1", "dexie-react-hooks": "^1.1.1", "humanize-duration": "^3.27.3", @@ -20,8 +20,8 @@ "i18next-browser-languagedetector": "^6.1.4", "i18next-http-backend": "^1.4.0", "js-base64": "^3.7.2", - "react": "latest", - "react-dom": "latest", + "react": "*", + "react-dom": "*", "react-i18next": "^11.16.2", "react-infinite-scroll-component": "^6.1.0", "react-remark": "^2.1.0", @@ -41,8 +41,8 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.15.0" + "vite": "^6.3.5", + "vite-plugin-pwa": "^1.0.0" } }, "node_modules/@ampproject/remapping": { @@ -1740,10 +1740,27 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], @@ -1754,13 +1771,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], @@ -1771,13 +1788,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], @@ -1788,13 +1805,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], @@ -1805,13 +1822,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], @@ -1822,13 +1839,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], @@ -1839,13 +1856,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], @@ -1856,13 +1873,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], @@ -1873,13 +1890,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], @@ -1890,13 +1907,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], @@ -1907,13 +1924,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], @@ -1924,13 +1941,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], @@ -1941,13 +1958,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], @@ -1958,13 +1975,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], @@ -1975,13 +1992,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], @@ -1992,13 +2009,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], @@ -2009,13 +2026,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], @@ -2026,13 +2060,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], @@ -2043,13 +2094,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], @@ -2060,13 +2111,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], @@ -2077,13 +2128,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], @@ -2094,13 +2145,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], @@ -2111,7 +2162,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2596,6 +2647,357 @@ "node": ">=14.0.0" } }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "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/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "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/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2662,9 +3064,9 @@ } }, "node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, @@ -2690,6 +3092,8 @@ "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2726,14 +3130,11 @@ } }, "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", @@ -3171,19 +3572,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -3224,19 +3612,6 @@ "dev": true, "license": "MIT" }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3470,6 +3845,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -3931,9 +4315,9 @@ } }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3941,31 +4325,34 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { @@ -4430,9 +4817,9 @@ } }, "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "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" }, @@ -4459,36 +4846,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4530,6 +4887,21 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4576,19 +4948,6 @@ "node": ">=10" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -5441,16 +5800,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -5716,21 +6065,6 @@ "node": ">=10" } }, - "node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", @@ -6124,23 +6458,6 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "license": "MIT" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromark": { "version": "2.11.4", "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", @@ -6161,20 +6478,6 @@ "parse-entities": "^2.0.0" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6557,13 +6860,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -7082,19 +7385,42 @@ } }, "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" } }, @@ -7215,9 +7541,9 @@ } }, "node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7372,6 +7698,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7765,17 +8098,21 @@ "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.4.4", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tr46": { @@ -7948,7 +8285,9 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -8195,41 +8534,51 @@ } }, "node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -8239,6 +8588,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -8247,29 +8599,44 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-plugin-pwa": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.15.2.tgz", - "integrity": "sha512-l1srtaad5NMNrAtAuub6ArTYG5Ci9AwofXXQ6IsbpCMYQ/0HUndwI7RB2x95+1UBFm7VGttQtT7woBlVnNhBRw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.0.0.tgz", + "integrity": "sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4", - "fast-glob": "^3.2.12", - "pretty-bytes": "^6.0.0", - "workbox-build": "^6.5.4", - "workbox-window": "^6.5.4" + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, + "engines": { + "node": ">=16.0.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0", - "workbox-build": "^6.5.4", - "workbox-window": "^6.5.4" + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } } }, "node_modules/void-elements": { @@ -8423,40 +8790,41 @@ } }, "node_modules/workbox-background-sync": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", - "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", + "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-broadcast-update": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", - "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz", + "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-build": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", - "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz", + "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", "dev": true, "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", + "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", @@ -8466,30 +8834,29 @@ "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "6.6.0", - "workbox-broadcast-update": "6.6.0", - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-google-analytics": "6.6.0", - "workbox-navigation-preload": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-range-requests": "6.6.0", - "workbox-recipes": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0", - "workbox-streams": "6.6.0", - "workbox-sw": "6.6.0", - "workbox-window": "6.6.0" + "workbox-background-sync": "7.3.0", + "workbox-broadcast-update": "7.3.0", + "workbox-cacheable-response": "7.3.0", + "workbox-core": "7.3.0", + "workbox-expiration": "7.3.0", + "workbox-google-analytics": "7.3.0", + "workbox-navigation-preload": "7.3.0", + "workbox-precaching": "7.3.0", + "workbox-range-requests": "7.3.0", + "workbox-recipes": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0", + "workbox-streams": "7.3.0", + "workbox-sw": "7.3.0", + "workbox-window": "7.3.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=16.0.0" } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { @@ -8534,27 +8901,6 @@ } } }, - "node_modules/workbox-build/node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", @@ -8587,6 +8933,13 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/workbox-build/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, "node_modules/workbox-build/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -8604,6 +8957,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -8611,6 +8971,19 @@ "dev": true, "license": "MIT" }, + "node_modules/workbox-build/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -8640,23 +9013,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/workbox-build/node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, "node_modules/workbox-build/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -8700,142 +9056,140 @@ } }, "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "workbox-background-sync@6.6.0", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", + "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", - "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz", + "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==", "dev": true, "license": "MIT" }, "node_modules/workbox-expiration": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", - "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz", + "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", - "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz", + "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-background-sync": "7.3.0", + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" } }, "node_modules/workbox-navigation-preload": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", - "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz", + "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-precaching": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", - "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz", + "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" } }, "node_modules/workbox-range-requests": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", - "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz", + "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-recipes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", - "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz", + "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-cacheable-response": "7.3.0", + "workbox-core": "7.3.0", + "workbox-expiration": "7.3.0", + "workbox-precaching": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" } }, "node_modules/workbox-routing": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", - "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz", + "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-strategies": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", - "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz", + "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-streams": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", - "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz", + "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0" + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0" } }, "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz", + "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==", "dev": true, "license": "MIT" }, "node_modules/workbox-window": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", - "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz", + "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", "dev": true, "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/wrappy": { @@ -8862,12 +9216,18 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } }, "node_modules/yocto-queue": { diff --git a/web/package.json b/web/package.json index bb84ff16..0de56abd 100644 --- a/web/package.json +++ b/web/package.json @@ -44,8 +44,8 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.15.0" + "vite": "^6.3.5", + "vite-plugin-pwa": "^1.0.0" }, "browserslist": { "production": [ From 7067d8aa771228030ef01246556176f9590d947b Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 21 May 2025 20:55:54 -0400 Subject: [PATCH 048/378] Release notes --- cmd/webpush.go | 12 ++++++------ docs/integrations.md | 6 +++--- docs/releases.md | 18 +++++++++++++++--- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/cmd/webpush.go b/cmd/webpush.go index bd44f5aa..a5f66e60 100644 --- a/cmd/webpush.go +++ b/cmd/webpush.go @@ -11,9 +11,9 @@ import ( "github.com/urfave/cli/v2/altsrc" ) -var flagsWebpush = append( +var flagsWebPush = append( []cli.Flag{}, - altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"f"}, Usage: "write vapid keys to this file"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "output-file", Aliases: []string{"f"}, Usage: "write VAPID keys to this file"}), ) func init() { @@ -33,7 +33,7 @@ var cmdWebPush = &cli.Command{ Usage: "Generate VAPID keys to enable browser background push notifications", UsageText: "ntfy webpush keys", Category: categoryServer, - Flags: flagsWebpush, + Flags: flagsWebPush, }, }, } @@ -44,16 +44,16 @@ func generateWebPushKeys(c *cli.Context) error { return err } - if keyFile := c.String("key-file"); keyFile != "" { + if outputFIle := c.String("output-file"); outputFIle != "" { contents := fmt.Sprintf(`--- web-push-public-key: %s web-push-private-key: %s `, publicKey, privateKey) - err = os.WriteFile(keyFile, []byte(contents), 0660) + err = os.WriteFile(outputFIle, []byte(contents), 0660) if err != nil { return err } - _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys written to %s.`, keyFile) + _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys written to %s.`, outputFIle) } else { _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: diff --git a/docs/integrations.md b/docs/integrations.md index ad84cb48..2dc210d4 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -9,9 +9,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Integration via HTTP/SMTP/etc.](#integration-via-httpsmtpetc) - [UnifiedPush integrations](#unifiedpush-integrations) - [Libraries](#libraries) -- [CLIs + GUIs](#clis--guis) -- [Projects + scripts](#projects--scripts) -- [Blog + forum posts](#blog--forum-posts) +- [CLIs + GUIs](#clis-guis) +- [Projects + scripts](#projects-scripts) +- [Blog + forum posts](#blog-forum-posts) - [Alternative ntfy servers](#alternative-ntfy-servers) ## Official integrations diff --git a/docs/releases.md b/docs/releases.md index 65af14d5..03bdd1d0 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1377,19 +1377,31 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) +* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi) for implementing) +* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii) for implementing) **Bug fixes + maintenance:** -* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) * Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) +* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) * Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) * Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) * Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) +* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) **Documentation:** -* Lots of new integrations: [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp), [UptimeObserver](https://uptimeobserver.com), [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay), [Monibot](https://monibot.io/), ... Amazing! +* Lots of new integrations and projects. Amazing! + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [UptimeObserver](https://uptimeobserver.com) + * [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) + * [Monibot](https://monibot.io/) + * [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) + * [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) + * [ntfy-run](https://github.com/quantum5/ntfy-run) + * [Clipboard IO](https://github.com/jim3692/clipboard-io) + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) * Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) * Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) * Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) From e64a0bd8c99b8b05ab80081208c633501d219d82 Mon Sep 17 00:00:00 2001 From: Patrick Morris <31410292+ptmorris1@users.noreply.github.com> Date: Thu, 22 May 2025 13:37:54 -0500 Subject: [PATCH 049/378] Add NtfyPwsh integration and blog --- docs/integrations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/integrations.md b/docs/integrations.md index 2dc210d4..1c14ab95 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -166,6 +166,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Clipboard IO](https://github.com/jim3692/clipboard-io) - End to end encrypted clipboard - [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript) - [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) +- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell) ## Blog + forum posts @@ -267,6 +268,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021 - [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021 - [ntfy on The Canary in the Cage Podcast](https://odysee.com/@TheCanaryInTheCage:b/The-Canary-in-the-Cage-Episode-42:1?r=4gitYjTacQqPEjf22874USecDQYJ5y5E&t=3062) - odysee.com - 1/2025 +- [NtfyPwsh - A PowerShell Module to Send Ntfy Messages](https://ptmorris1.github.io/posts/NtfyPwsh/) - github.io - 5/2025 ## Alternative ntfy servers From ad3e7960ce67b3cb963ebfc015fc91ec996c2ed3 Mon Sep 17 00:00:00 2001 From: tr4nt0r <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 23 May 2025 01:31:16 +0200 Subject: [PATCH 050/378] Add official Home Assistant integration and async python library --- docs/integrations.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/integrations.md b/docs/integrations.md index 1c14ab95..52f75f22 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -5,6 +5,7 @@ There are quite a few projects that work with ntfy, integrate ntfy, or have been I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community. ## Table of Contents + - [Official integrations](#official-integrations) - [Integration via HTTP/SMTP/etc.](#integration-via-httpsmtpetc) - [UnifiedPush integrations](#unifiedpush-integrations) @@ -38,6 +39,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring - [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool - [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. +- [Home Assistant](https://www.home-assistant.io/integrations/ntfy) - Home Assistant is an open-source platform for automating and controlling smart home devices. ## Integration via HTTP/SMTP/etc. @@ -76,6 +78,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go) - [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP) - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) +- [aiontfy](https://github.com/tr4nt0r/aiontfy) - Asynchronous client library for publishing and subscribing to ntfy (Python) ## CLIs + GUIs From 6d15b9face0f6dcc98ff2d0dfe95b47700139454 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 22 May 2025 20:48:24 -0400 Subject: [PATCH 051/378] Fix up APNs PR --- cmd/webpush_test.go | 2 +- docs/integrations.md | 2 +- docs/releases.md | 2 ++ server/server_firebase.go | 58 +++++++++++++++++---------------------- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/cmd/webpush_test.go b/cmd/webpush_test.go index c2f19f6f..01e1a7a1 100644 --- a/cmd/webpush_test.go +++ b/cmd/webpush_test.go @@ -16,7 +16,7 @@ func TestCLI_WebPush_GenerateKeys(t *testing.T) { func TestCLI_WebPush_WriteKeysToFile(t *testing.T) { app, _, _, stderr := newTestApp() - require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--key-file=key-file.yaml")) + require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--output-file=key-file.yaml")) require.Contains(t, stderr.String(), "Web Push keys written to key-file.yaml") require.FileExists(t, "key-file.yaml") } diff --git a/docs/integrations.md b/docs/integrations.md index 52f75f22..5e550b53 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -18,6 +18,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had ## Official integrations - [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification +- [Home Assistant](https://www.home-assistant.io/integrations/ntfy) ⭐ - Home Assistant is an open-source platform for automating and controlling smart home devices. - [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs - [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform - [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool @@ -39,7 +40,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring - [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool - [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. -- [Home Assistant](https://www.home-assistant.io/integrations/ntfy) - Home Assistant is an open-source platform for automating and controlling smart home devices. ## Integration via HTTP/SMTP/etc. diff --git a/docs/releases.md b/docs/releases.md index 03bdd1d0..dee81727 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1383,11 +1383,13 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** * Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) +* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!) * Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) * Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) * Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) * Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) * WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) +* Make Markdown in web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) **Documentation:** diff --git a/server/server_firebase.go b/server/server_firebase.go index aff96db7..2b01add2 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -50,7 +50,7 @@ func (c *firebaseClient) Send(v *visitor, m *message) error { ev.Field("firebase_message", util.MaybeMarshalJSON(fbm)).Trace("Firebase message") } err = c.sender.Send(fbm) - if err == errFirebaseQuotaExceeded { + if errors.Is(err, errFirebaseQuotaExceeded) { logvm(v, m). Tag(tagFirebase). Err(err). @@ -133,7 +133,7 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro "time": fmt.Sprintf("%d", m.Time), "event": m.Event, "topic": m.Topic, - "message": m.Message, + "message": newMessageBody, "poll_id": m.PollID, } apnsConfig = createAPNSAlertConfig(m, data) @@ -173,28 +173,29 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro } apnsConfig = createAPNSAlertConfig(m, data) } else { - // If anonymous read for a topic is not allowed, we cannot send the message along + // If "anonymous read" for a topic is not allowed, we cannot send the message along // via Firebase. Instead, we send a "poll_request" message, asking the client to poll. - //App function needs all the data to create a message object, if not, it fails, - //so we set it but put a placeholders to not to send the actual message - //but generic title and message instead, we also add the poll_id so client knowns - //what message is goint to "decode" (retrieve) + // + // The data map needs to contain all the fields for it to function properly. If not all + // fields are set, the iOS app fails to decode the message. + // + // See https://github.com/binwiederhier/ntfy/pull/1345 data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": pollRequestEvent, - "topic": m.Topic, - "priority": fmt.Sprintf("%d", m.Priority), - "tags": strings.Join(m.Tags, ","), - "click": m.Click, - "icon": m.Icon, - "title": "Private", - "message": "Message", - "content_type": m.ContentType, - "encoding": m.Encoding, - "poll_id": m.ID, - } - apnsConfig = createAPNSAlertConfig(m, data) + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": pollRequestEvent, + "topic": m.Topic, + "priority": fmt.Sprintf("%d", m.Priority), + "tags": "", + "click": "", + "icon": "", + "title": "", + "message": newMessageBody, + "content_type": m.ContentType, + "encoding": m.Encoding, + "poll_id": m.ID, + } + apnsConfig = createAPNSAlertConfig(m, data) } } var androidConfig *messaging.AndroidConfig @@ -238,23 +239,14 @@ func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSCo for k, v := range data { apnsData[k] = v } - alertTitle := m.Title - alertBody := maybeTruncateAPNSBodyMessage(m.Message) - // If the event is pollRequestEvent (server/topic is restricted) we dont want to - //send the actual message to Firebase/APNS, so we send a generic text - //if for some reason, client cant retrieve the message, it shows this as the message and title - if event, ok := data["event"]; ok && event == pollRequestEvent { - alertTitle = "New Notification received" - alertBody = "Message cant be retrieved, open the app and refresh content" - } return &messaging.APNSConfig{ Payload: &messaging.APNSPayload{ CustomData: apnsData, Aps: &messaging.Aps{ MutableContent: true, Alert: &messaging.ApsAlert{ - Title: alertTitle, - Body: alertBody, + Title: m.Title, + Body: maybeTruncateAPNSBodyMessage(m.Message), }, }, }, From 1598087e1fae9aed9d2d3473237e55766c2b27c5 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 22 May 2025 20:58:28 -0400 Subject: [PATCH 052/378] Fix tests --- docs/releases.md | 3 ++- server/server_firebase_test.go | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index dee81727..877cd674 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1379,6 +1379,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi) for implementing) * Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii) for implementing) +* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) **Bug fixes + maintenance:** @@ -1389,7 +1390,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) * Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) * WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) -* Make Markdown in web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) +* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) **Documentation:** diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 9b653a29..8d88fcf0 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -223,13 +223,22 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) { require.Equal(t, &messaging.AndroidConfig{ Priority: "high", }, fbm.Android) - require.Equal(t, "", fbm.Data["message"]) - require.Equal(t, "", fbm.Data["priority"]) + require.Equal(t, "New message", fbm.Data["message"]) + require.Equal(t, "5", fbm.Data["priority"]) require.Equal(t, map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": "poll_request", - "topic": "mytopic", + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": "poll_request", + "topic": "mytopic", + "message": "New message", + "title": "", + "tags": "", + "click": "", + "icon": "", + "priority": "5", + "encoding": "", + "content_type": "", + "poll_id": m.ID, }, fbm.Data) } From f595dff66f0b3b00c5f2c5e798888122f58a662b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Tue, 30 Jul 2024 05:02:41 +0000 Subject: [PATCH 053/378] Translated using Weblate (Galician) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/ --- web/public/static/langs/gl.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json index a7666a19..bb4715f6 100644 --- a/web/public/static/langs/gl.json +++ b/web/public/static/langs/gl.json @@ -62,7 +62,7 @@ "notifications_none_for_topic_title": "Aínda non recibiches ningunha notificación para este tema.", "reserve_dialog_checkbox_label": "Reservar tema e configurar acceso", "notifications_loading": "Cargando notificacións…", - "publish_dialog_base_url_placeholder": "URL de servizo, ex. https://exemplo.com", + "publish_dialog_base_url_placeholder": "URL do servizo, ex. https://exemplo.com", "publish_dialog_topic_label": "Nome do tema", "publish_dialog_topic_placeholder": "Nome do tema, ex. alertas_equipo", "publish_dialog_topic_reset": "Restablecer tema", @@ -315,17 +315,17 @@ "account_basics_password_dialog_current_password_incorrect": "Contrasinal incorrecto", "account_basics_phone_numbers_dialog_number_label": "Número de teléfono", "account_basics_password_dialog_button_submit": "Modificar contrasinal", - "account_basics_username_title": "Usuario", + "account_basics_username_title": "Identificador", "account_basics_phone_numbers_dialog_check_verification_button": "Código de confirmación", "account_usage_messages_title": "Mesaxes publicados", "account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS", "account_basics_tier_change_button": "Cambiar", "account_basics_phone_numbers_dialog_description": "Para usar a característica de chamadas de teléfono, vostede debe engadir e verificar ao menos un número de teléfono. A verificación pode ser realizada vía SMS ou a través de chamada.", - "account_delete_title": "Borrar conta", + "account_delete_title": "Eliminar a conta", "account_delete_dialog_label": "Contrasinal", "account_basics_tier_admin_suffix_with_tier": "(con tier {{tier}})", - "subscribe_dialog_login_username_label": "Nome de usuario, ex. phil", - "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} non autorizado", + "subscribe_dialog_login_username_label": "Identificador, ex. xoana", + "subscribe_dialog_error_user_not_authorized": "Identificador {{username}} non autorizado", "account_basics_title": "Conta", "account_basics_phone_numbers_no_phone_numbers_yet": "Aínda non hay números de teléfono", "subscribe_dialog_subscribe_button_generate_topic_name": "Xerar nome", @@ -333,9 +333,9 @@ "subscribe_dialog_subscribe_button_subscribe": "Subscribirse", "account_basics_phone_numbers_dialog_title": "Engadir número de teléfono", "account_basics_username_admin_tooltip": "É vostede Admin", - "account_delete_dialog_description": "Isto borrará permanentemente a túa conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu nome de usuario non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirme co seu contrasinal na caixa inferior.", + "account_delete_dialog_description": "Isto borrará permanentemente a conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu identificador non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirma co contrasinal na caixa inferior.", "account_usage_reservations_none": "Non hai temas reservados para esta conta", - "subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. phil_alertas", + "subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. alertas_xoana", "account_usage_title": "Uso", "account_basics_tier_upgrade_button": "Mexorar a Pro", "subscribe_dialog_error_topic_already_reserved": "Tema xa reservado", @@ -351,11 +351,11 @@ "account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado no portapapeis", "account_basics_tier_title": "Tipo de conta", "account_usage_cannot_create_portal_session": "Non foi posible abrir o portal de pagos", - "account_delete_description": "Borrar permanentemente a túa conta", + "account_delete_description": "Eliminar a conta de xeito definitivo", "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444", "account_basics_phone_numbers_dialog_code_placeholder": "ex. 123456", "account_basics_tier_manage_billing_button": "Xestionar pagos", - "account_basics_username_description": "Ei, ese eres ti ❤", + "account_basics_username_description": "Ei, es ti ❤", "account_basics_password_dialog_confirm_password_label": "Confirmar contrasinal", "account_basics_tier_interval_yearly": "anual", "account_delete_dialog_button_submit": "Borrar permanentemente a conta", @@ -364,7 +364,7 @@ "account_basics_password_dialog_new_password_label": "Novo contrasinal", "account_usage_of_limit": "de {{limit}}", "subscribe_dialog_error_user_anonymous": "anónimo", - "account_usage_basis_ip_description": "Estadísticas de uso e límites para esta conta están basados na sua IP, polo que poden estar compartidos con outros usuarios. Os limites mostrados son aproximados, basados nos ratios de limite existentes.", + "account_usage_basis_ip_description": "As estatísticas de uso e límites para esta conta están basados na IP, polo que poden estar compartidas con outras usuarias. Os limites mostrados son aproximados, baseados nos límites das taxas existentes.", "account_basics_password_dialog_title": "Modificar contrasinal", "account_usage_limits_reset_daily": "Límite de uso é reiniciado diariamente a medianoite (UTC(", "account_usage_unlimited": "Sen límites", @@ -380,7 +380,7 @@ "account_basics_phone_numbers_dialog_verify_button_call": "Chámame", "account_usage_emails_title": "Emails enviados", "account_basics_phone_numbers_dialog_channel_sms": "SMS", - "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse.", + "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, escribe as credenciais para subscribirte.", "action_bar_mute_notifications": "Acalar notificacións", "action_bar_unmute_notifications": "Reactivar notificacións", "alert_notification_permission_required_title": "Notificacións desactivadas", From dc6b8ece1e818f90869232b3d6341eaa9fd9b4c0 Mon Sep 17 00:00:00 2001 From: Stefano Maggi Date: Sat, 3 Aug 2024 22:18:57 +0000 Subject: [PATCH 054/378] Translated using Weblate (Italian) Currently translated at 87.6% (355 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/ --- web/public/static/langs/it.json | 38 ++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index 7ddd60fc..6605d4dd 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -316,5 +316,41 @@ "action_bar_unmute_notifications": "Riattiva audio notifiche", "alert_notification_ios_install_required_title": "E' richiesta l'installazione di iOS", "alert_notification_ios_install_required_description": "Fare clic sull'icona Condividi e Aggiungi alla schermata home per abilitare le notifiche su iOS", - "publish_dialog_checkbox_markdown": "Formatta come markdown" + "publish_dialog_checkbox_markdown": "Formatta come markdown", + "account_upgrade_dialog_interval_yearly": "Annualmente", + "account_tokens_table_token_header": "Token", + "account_tokens_table_label_header": "Etichetta", + "account_tokens_table_cannot_delete_or_edit": "Impossibile modificare o eliminare il token della sessione corrente", + "account_tokens_dialog_label": "Etichetta, ad esempio Notifiche Radarr", + "account_tokens_dialog_title_delete": "Elimina token di accesso", + "account_tokens_dialog_title_edit": "Modifica token di accesso", + "account_tokens_dialog_button_create": "Crea token", + "account_tokens_dialog_button_update": "Aggiorna token", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mails giornaliere", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messaggi giornalieri", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} spazio di archiviazione totale", + "notifications_actions_failed_notification": "Azione non riuscita", + "account_usage_attachment_storage_description": "{{filesize}} per file, eliminato dopo {{expiry}}", + "account_upgrade_dialog_title": "Cambia livello account", + "account_upgrade_dialog_interval_monthly": "Mensilmente", + "account_upgrade_dialog_cancel_warning": "Questa azione annullerà il tuo abbonamento e declasserà il tuo account il {{date}}. In quella data, le prenotazioni degli argomenti e i messaggi memorizzati nella cache del server verranno eliminati.", + "account_upgrade_dialog_reservations_warning_other": "Il livello selezionato consente meno argomenti riservati rispetto al livello attuale. Prima di cambiare il livello, elimina almeno {{count}} prenotazioni. Puoi rimuovere le prenotazioni nelle Impostazioni.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} argomenti riservati", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-mail giornaliere", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} telefonate giornaliere", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} telefonate giornaliere", + "account_upgrade_dialog_tier_features_no_calls": "Nessuna telefonata", + "account_tokens_description": "Utilizza i token di accesso quando pubblichi e ti iscrivi tramite l'API ntfy, così non dovrai inviare le credenziali del tuo account. Consulta la documentazione per saperne di più.", + "account_tokens_table_copied_to_clipboard": "Token di accesso copiato", + "account_tokens_table_create_token_button": "Crea token di accesso", + "account_tokens_table_last_origin_tooltip": "Dall'indirizzo IP {{ip}}, clicca per cercare", + "account_tokens_dialog_title_create": "Crea token di accesso", + "account_tokens_dialog_button_cancel": "Annulla", + "web_push_unknown_notification_body": "Potrebbe essere necessario aggiornare ntfy aprendo l'app web", + "account_upgrade_dialog_proration_info": "Prorata: quando si esegue l'upgrade tra piani a pagamento, la differenza di prezzo verrà addebitata immediatamente. Quando si esegue il downgrade a un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.", + "account_tokens_table_last_access_header": "Ultimo accesso", + "account_tokens_table_expires_header": "Scade", + "account_tokens_table_never_expires": "Non scade mai", + "account_tokens_table_current_session": "Sessione corrente del browser" } From 7835fc65c4e36c04ac31594528c99993a8842c84 Mon Sep 17 00:00:00 2001 From: Jakob Malchow Date: Sat, 3 Aug 2024 22:28:10 +0000 Subject: [PATCH 055/378] Translated using Weblate (Italian) Currently translated at 87.6% (355 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/ --- web/public/static/langs/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index 6605d4dd..3ce17812 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -352,5 +352,6 @@ "account_tokens_table_last_access_header": "Ultimo accesso", "account_tokens_table_expires_header": "Scade", "account_tokens_table_never_expires": "Non scade mai", - "account_tokens_table_current_session": "Sessione corrente del browser" + "account_tokens_table_current_session": "Sessione corrente del browser", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "Risparmia fino al {{discount}}%" } From 94d0c5a3358c00d9c07faa831e1389a120d8f392 Mon Sep 17 00:00:00 2001 From: Stefano Maggi Date: Sat, 3 Aug 2024 22:36:56 +0000 Subject: [PATCH 056/378] Translated using Weblate (Italian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/ --- web/public/static/langs/it.json | 52 ++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index 3ce17812..1ba1eba8 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -353,5 +353,55 @@ "account_tokens_table_expires_header": "Scade", "account_tokens_table_never_expires": "Non scade mai", "account_tokens_table_current_session": "Sessione corrente del browser", - "account_upgrade_dialog_interval_yearly_discount_save_up_to": "Risparmia fino al {{discount}}%" + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "Risparmia fino al {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save": "conserva {{discount}}%", + "prefs_users_description_no_sync": "Gli utenti e le password non vengono sincronizzati con il tuo account.", + "prefs_reservations_title": "Argomenti riservati", + "prefs_reservations_table_access_header": "Accesso", + "reservation_delete_dialog_action_delete_title": "Elimina i messaggi e gli allegati memorizzati nella cache", + "reservation_delete_dialog_submit_button": "Elimina prenotazione", + "account_tokens_dialog_expires_label": "Il token di accesso scade tra", + "account_tokens_dialog_expires_unchanged": "Lascia la data di scadenza invariata", + "account_tokens_delete_dialog_submit_button": "Elimina definitivamente il token", + "prefs_reservations_description": "Qui puoi riservare i nomi degli argomenti per uso personale. Riservare un argomento ti dà la proprietà dell'argomento e ti consente di definire i permessi di accesso per altri utenti sull'argomento.", + "prefs_reservations_add_button": "Aggiungi argomento riservato", + "prefs_reservations_edit_button": "Modifica accesso argomento", + "prefs_reservations_delete_button": "Reimposta accesso argomento", + "prefs_reservations_table_everyone_read_only": "Posso pubblicare e iscrivermi, tutti possono iscriversi", + "prefs_reservations_table_not_subscribed": "Non iscritto", + "prefs_reservations_table_everyone_write_only": "Posso pubblicare ed iscrivermi, tutti possono pubblicare", + "prefs_reservations_table_everyone_read_write": "Tutti possono pubblicare e iscriversi", + "prefs_reservations_dialog_title_delete": "Elimina prenotazione argomento", + "prefs_reservations_dialog_description": "Prenotando un argomento ne diventi proprietario e puoi definire le autorizzazioni di accesso per altri utenti.", + "reservation_delete_dialog_action_keep_description": "I messaggi e gli allegati memorizzati nella cache del server diventeranno visibili al pubblico per le persone a conoscenza del nome dell'argomento.", + "reservation_delete_dialog_action_delete_description": "I messaggi e gli allegati memorizzati nella cache verranno eliminati definitivamente. Questa azione non può essere annullata.", + "prefs_reservations_limit_reached": "Hai raggiunto il limite di argomenti riservati.", + "prefs_reservations_table_click_to_subscribe": "Clicca per iscriverti", + "prefs_reservations_dialog_title_add": "Prenota argomento", + "prefs_reservations_dialog_title_edit": "Modifica argomento riservato", + "account_tokens_dialog_expires_x_days": "Il token scade tra {{days}} giorni", + "account_tokens_dialog_expires_never": "Il token non scade mai", + "account_tokens_delete_dialog_title": "Elimina token di accesso", + "account_tokens_delete_dialog_description": "Prima di eliminare un token di accesso, assicurati che nessuna applicazione o script lo stia utilizzando attivamente. Questa azione non può essere annullata.", + "prefs_notifications_web_push_title": "Notifiche in background", + "prefs_notifications_web_push_enabled_description": "Le notifiche vengono ricevute anche quando l'app Web non è in esecuzione (tramite Web Push)", + "prefs_notifications_web_push_disabled_description": "Le notifiche vengono ricevute quando l'app Web è in esecuzione (tramite WebSocket)", + "prefs_notifications_web_push_enabled": "Abilitato per {{server}}", + "prefs_notifications_web_push_disabled": "Disabilitato", + "prefs_users_table_cannot_delete_or_edit": "Impossibile eliminare o modificare l'utente registrato", + "prefs_appearance_theme_title": "Tema", + "prefs_appearance_theme_system": "Sistema (predefinito)", + "prefs_appearance_theme_dark": "Modalità scura", + "prefs_appearance_theme_light": "Modalità chiara", + "prefs_reservations_table_topic_header": "Argomento", + "prefs_reservations_dialog_access_label": "Accesso", + "reservation_delete_dialog_description": "La rimozione di una prenotazione comporta la rinuncia alla proprietà dell'argomento e consente ad altri di riservarlo. Puoi mantenere o eliminare i messaggi e gli allegati esistenti.", + "prefs_reservations_table_everyone_deny_all": "Solo io posso pubblicare e iscrivermi", + "prefs_reservations_dialog_topic_label": "Argomento", + "reservation_delete_dialog_action_keep_title": "Mantieni i messaggi e gli allegati memorizzati nella cache", + "web_push_subscription_expiring_title": "Le notifiche verranno sospese", + "web_push_subscription_expiring_body": "Apri ntfy per continuare a ricevere notifiche", + "web_push_unknown_notification_title": "Notifica sconosciuta ricevuta dal server", + "account_tokens_dialog_expires_x_hours": "Il token scade tra {{hours}} ore", + "prefs_reservations_table": "Tabella argomenti riservati" } From bbce1200b4770f84dc16a66d2a9c2cad318e4876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Thu, 29 Aug 2024 05:56:50 +0000 Subject: [PATCH 057/378] Translated using Weblate (Galician) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/ --- web/public/static/langs/gl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json index bb4715f6..2fa8c32d 100644 --- a/web/public/static/langs/gl.json +++ b/web/public/static/langs/gl.json @@ -172,7 +172,7 @@ "account_tokens_table_token_header": "Token", "prefs_notifications_delete_after_never": "Nunca", "prefs_users_description": "Engadir/eliminar usuarias dos temas protexidos. Ten en conta que as credenciais gárdanse na almacenaxe local do navegador.", - "subscribe_dialog_subscribe_description": "Os temas poderían non estar proxetidos con contrasinal, así que elixe un nome complicado de adiviñar. Unha vez subscrita, podes PUT/POST notificacións.", + "subscribe_dialog_subscribe_description": "Os temas poden non estar protexidos con contrasinal, asi que escolle un nome que non sexa fácil de pesquisar. Unha vez suscrito, podes notificar con PUT/POST.", "account_upgrade_dialog_interval_yearly_discount_save_up_to": "aforro ata un {{discount}}%", "account_tokens_dialog_label": "Etiqueta, ex. notificación de Radarr", "account_tokens_table_expires_header": "Caducidade", From 49d258706d1806d3bcdcf32db4ef6d3e28a17fcd Mon Sep 17 00:00:00 2001 From: githubozaurus Date: Wed, 4 Sep 2024 11:26:38 +0000 Subject: [PATCH 058/378] Translated using Weblate (Romanian) Currently translated at 31.1% (126 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/ --- web/public/static/langs/ro.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index 67b92e1d..c58aa7b1 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -123,5 +123,8 @@ "message_bar_show_dialog": "Arată dialogul de publicare", "signup_error_username_taken": "Numele de utilizator {{username}} este deja folosit", "login_title": "Autentifică-te în contul ntfy", - "action_bar_reservation_add": "Rezervă topicul" + "action_bar_reservation_add": "Rezervă topicul", + "action_bar_mute_notifications": "Oprește notificările", + "action_bar_unmute_notifications": "Pornește notificările", + "nav_topics_title": "Subiecte abonate" } From 1c6aa49fca666bcd4facb0a286589a00d2e88b33 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:48:54 +0000 Subject: [PATCH 059/378] Translated using Weblate (German) Currently translated at 95.3% (386 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index a548d0b4..17476bfd 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -383,5 +383,7 @@ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag", "action_bar_mute_notifications": "Benachrichtigungen stummschalten", "action_bar_unmute_notifications": "Benachrichtigungen laut schalten", - "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert" + "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert", + "alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser", + "notifications_actions_failed_notification": "Aktion nicht erfolgreich" } From a92c8a9ec9c9fe5eff72fe78458f9d8c23ef3d47 Mon Sep 17 00:00:00 2001 From: Malte Saling Date: Thu, 19 Sep 2024 13:32:19 +0000 Subject: [PATCH 060/378] Translated using Weblate (German) Currently translated at 95.5% (387 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 17476bfd..67d06f04 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -385,5 +385,6 @@ "action_bar_unmute_notifications": "Benachrichtigungen laut schalten", "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert", "alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser", - "notifications_actions_failed_notification": "Aktion nicht erfolgreich" + "notifications_actions_failed_notification": "Aktion nicht erfolgreich", + "alert_notification_ios_install_required_title": "iOS Installation erforderlich" } From 871883f6e9699d9a41f337de78eafff2f60cd5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?= Date: Wed, 25 Sep 2024 16:00:45 +0000 Subject: [PATCH 061/378] Translated using Weblate (Finnish) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/ --- web/public/static/langs/fi.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json index f5954f8d..ab2f6188 100644 --- a/web/public/static/langs/fi.json +++ b/web/public/static/langs/fi.json @@ -400,5 +400,11 @@ "error_boundary_button_reload_ntfy": "Lataa ntfy uudelleen", "web_push_subscription_expiring_title": "Ilmoitukset keskeytetään", "web_push_subscription_expiring_body": "Avaa ntfy jatkaaksesi ilmoitusten vastaanottamista", - "web_push_unknown_notification_title": "Tuntematon ilmoitus vastaanotettu palvelimelta" + "web_push_unknown_notification_title": "Tuntematon ilmoitus vastaanotettu palvelimelta", + "alert_notification_ios_install_required_description": "Napauta Jaa-kuvaketta ja Lisää aloitusnäyttöön ottaaksesi ilmoitukset käyttöön iOS:ssä", + "prefs_notifications_web_push_disabled_description": "Ilmoituksia vastaanotetaan, kun verkkosovellus on käynnissä (WebSocket:in kautta)", + "web_push_unknown_notification_body": "Voit joutua päivittämään ntfy:n avaamalla verkkosovelluksen", + "notifications_actions_failed_notification": "Epäonnistunut toiminto", + "subscribe_dialog_subscribe_use_another_background_info": "Ilmoituksia muilta palvelimilta ei vastaanoteta, mikäli verkkosovellus ei ole avoinna", + "prefs_notifications_web_push_enabled_description": "Ilmoituksia vastaanotetaan siitä huolimatta, että verkkosovellus ei ole käynnissä (Web Push:n kautta)" } From 236b7b7a167f52f4dadff2da5c44c98e1fc8156b Mon Sep 17 00:00:00 2001 From: Carl Fritze Date: Mon, 30 Sep 2024 13:31:05 +0000 Subject: [PATCH 062/378] Translated using Weblate (German) Currently translated at 99.5% (403 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 67d06f04..8e7cfc12 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -386,5 +386,21 @@ "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert", "alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser", "notifications_actions_failed_notification": "Aktion nicht erfolgreich", - "alert_notification_ios_install_required_title": "iOS Installation erforderlich" + "alert_notification_ios_install_required_title": "iOS Installation erforderlich", + "alert_notification_ios_install_required_description": "Klicke auf das Teilen-Symbol und “Zum Home-Bildschirm” um auf iOS Benachrichtigungen zu aktivieren", + "subscribe_dialog_subscribe_use_another_background_info": "Benachrichtigungen von anderen Servern werden nicht empfangen, wenn die Web App nicht geöffnet ist", + "publish_dialog_checkbox_markdown": "Als Markdown formatieren", + "prefs_notifications_web_push_title": "Hintergrund-Benachrichtigungen", + "prefs_notifications_web_push_disabled_description": "Benachrichtigungen werden empfangen wenn die Web App läuft (über WebSocket)", + "prefs_notifications_web_push_enabled": "Aktiviert für {{server}}", + "prefs_notifications_web_push_disabled": "Deaktiviert", + "prefs_appearance_theme_title": "Thema", + "prefs_appearance_theme_system": "System (Standard)", + "prefs_appearance_theme_dark": "Nachtmodus", + "prefs_appearance_theme_light": "Tagmodus", + "error_boundary_button_reload_ntfy": "ntfy neu laden", + "web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert", + "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", + "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom server empfangen", + "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest." } From 8acf0f43501aede2cf397c3dcdb57394912b41a9 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 8 Oct 2024 10:27:48 +0000 Subject: [PATCH 063/378] Translated using Weblate (Indonesian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/ --- web/public/static/langs/id.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json index a562436a..0095138b 100644 --- a/web/public/static/langs/id.json +++ b/web/public/static/langs/id.json @@ -24,7 +24,7 @@ "nav_button_subscribe": "Berlangganan ke topik", "alert_notification_permission_required_title": "Notifikasi dinonaktifkan", "alert_notification_permission_required_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.", - "alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda.", + "alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda", "notifications_attachment_open_title": "Pergi ke {{url}}", "notifications_attachment_open_button": "Buka lampiran", "notifications_attachment_link_expires": "tautan kadaluwarsa {{date}}", From 58d7cb8ef80e7cdef13084fbd93a15bfb321ba5c Mon Sep 17 00:00:00 2001 From: Ricardo Vieira Date: Tue, 15 Oct 2024 18:25:07 +0000 Subject: [PATCH 064/378] Translated using Weblate (Portuguese) Currently translated at 76.2% (309 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/ --- web/public/static/langs/pt.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json index 48159d21..d5946536 100644 --- a/web/public/static/langs/pt.json +++ b/web/public/static/langs/pt.json @@ -290,5 +290,23 @@ "account_usage_messages_title": "Mensagens publicadas", "account_usage_calls_title": "Ligações realizadas", "account_usage_calls_none": "Esta conta não pode realizar ligações", - "account_usage_reservations_title": "Tópicos reservados" + "account_usage_reservations_title": "Tópicos reservados", + "account_basics_username_title": "Utilizador", + "account_delete_dialog_description": "Isto irá eliminar definitivamente a sua conta, incluindo dados que estejam armazenados no servidor. Apos ser eliminado, o nome de utilizador ficará indisponível durante 7 dias. Se deseja mesmo proceder, por favor confirm com a sua palavra pass na caixa abaixo.", + "account_delete_dialog_button_submit": "Eliminar conta definitivamente", + "account_delete_dialog_billing_warning": "Eliminar a sua conta também cancela a sua subscrição de faturação imediatamente. Não terá acesso ao portal de faturação de futuro.", + "account_upgrade_dialog_title": "Alterar o nível da sua conta", + "account_upgrade_dialog_interval_monthly": "Mensalmente", + "account_upgrade_dialog_interval_yearly": "Anualmente", + "account_upgrade_dialog_interval_yearly_discount_save": "poupe {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "poupe até {{discount}}%", + "account_delete_dialog_label": "Palavra pass", + "account_usage_cannot_create_portal_session": "Impossível abrir o portal de faturação", + "account_usage_basis_ip_description": "Estatísticas de utilização e limites para esta conta são baseadas no seu endereço IP, pelo que podem ser partilhados com outros utilizadores. Os limites mostrados acima são aproximados com base nos limites existentes.", + "account_usage_attachment_storage_description": "{{filesize}} por ficheiro, eliminado após {{expiry}}", + "account_delete_title": "Eliminar conta", + "account_delete_description": "Eliminar definitivamente a sua conta", + "account_delete_dialog_button_cancel": "Cancelar", + "account_upgrade_dialog_cancel_warning": "Isto irá cancelar a sua assinatura, e fazer downgrade da sua conta em {{date}}. Nessa data, tópicos reservados bem como mensagens guardadas no servidor serão eliminados.", + "account_upgrade_dialog_proration_info": "Proporção: Quando atualizar entre planos pagos, a diferença de preço será debitada imediatamente. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação." } From c03f79550875f77d54395d423765483fafa4ca28 Mon Sep 17 00:00:00 2001 From: Soaibuzzaman Date: Mon, 21 Oct 2024 09:19:21 +0200 Subject: [PATCH 065/378] Added translation using Weblate (Bengali) --- web/public/static/langs/bn.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/bn.json diff --git a/web/public/static/langs/bn.json b/web/public/static/langs/bn.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/bn.json @@ -0,0 +1 @@ +{} From 41083cfd0736074d3d96c95ea241138af316ff43 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:54:57 +0000 Subject: [PATCH 066/378] Translated using Weblate (German) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 8e7cfc12..92dec374 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -36,7 +36,7 @@ "message_bar_type_message": "Gib hier eine Nachricht ein", "message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung", "alert_not_supported_title": "Benachrichtigungen werden nicht unterstützt", - "alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt.", + "alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt", "action_bar_settings": "Einstellungen", "action_bar_clear_notifications": "Alle Benachrichtigungen löschen", "alert_notification_permission_required_button": "Jetzt erlauben", @@ -402,5 +402,6 @@ "web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert", "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom server empfangen", - "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest." + "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest.", + "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht läuft (über Web Push)" } From 6daf4141c61dd6156240f4ff36a8e3c804df156d Mon Sep 17 00:00:00 2001 From: 109247019824 Date: Thu, 7 Nov 2024 02:27:48 +0000 Subject: [PATCH 067/378] Translated using Weblate (Bulgarian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/ --- web/public/static/langs/bg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json index 15c8cc95..59b85e5b 100644 --- a/web/public/static/langs/bg.json +++ b/web/public/static/langs/bg.json @@ -212,7 +212,7 @@ "nav_upgrade_banner_label": "Надграждане до ntfy Pro", "signup_form_confirm_password": "Парола отново", "signup_disabled": "Регистрациите са затворени", - "signup_error_creation_limit_reached": "Достигнатео е ограничението за създаване на профили", + "signup_error_creation_limit_reached": "Достигнато е ограничението за създаване на профили", "display_name_dialog_title": "Промяна на показваното име", "action_bar_reservation_edit": "Промяна на резервацията", "action_bar_sign_up": "Регистриране", From 5f6b7e6f8238e73860852e5f1a91adf6490acada Mon Sep 17 00:00:00 2001 From: Shoshin Akamine Date: Fri, 8 Nov 2024 04:31:47 +0000 Subject: [PATCH 068/378] Translated using Weblate (Japanese) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/ --- web/public/static/langs/ja.json | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json index 84afc30b..3d9643e0 100644 --- a/web/public/static/langs/ja.json +++ b/web/public/static/langs/ja.json @@ -7,7 +7,7 @@ "action_bar_clear_notifications": "全ての通知を消去", "action_bar_unsubscribe": "購読解除", "nav_button_documentation": "ドキュメント", - "alert_not_supported_description": "通知機能はこのブラウザではサポートされていません。", + "alert_not_supported_description": "通知機能はこのブラウザではサポートされていません", "notifications_copied_to_clipboard": "クリップボードにコピーしました", "notifications_example": "例", "publish_dialog_title_topic": "{{topic}}に送信", @@ -28,7 +28,7 @@ "message_bar_type_message": "メッセージを入力してください", "nav_topics_title": "購読しているトピック", "nav_button_subscribe": "トピックを購読", - "alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください。", + "alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください", "alert_notification_permission_required_button": "許可する", "notifications_attachment_link_expires": "リンクは {{date}} に失効します", "notifications_click_copy_url_button": "リンクをコピー", @@ -191,7 +191,7 @@ "signup_form_username": "ユーザー名", "signup_form_password": "パスワード", "signup_form_confirm_password": "パスワードを確認", - "signup_already_have_account": "アカウントをお持ちならサインイン", + "signup_already_have_account": "アカウントをお持ちならサインイン!", "signup_disabled": "サインアップは無効化されています", "signup_error_creation_limit_reached": "アカウント作成制限に達しました", "login_title": "あなたのntfyアカウントにサインイン", @@ -380,5 +380,28 @@ "account_upgrade_dialog_tier_features_calls_other": "電話 1日 {{calls}} 回", "publish_dialog_chip_call_no_verified_numbers_tooltip": "認証済み電話番号がありません", "account_basics_phone_numbers_dialog_channel_sms": "SMS", - "account_basics_phone_numbers_dialog_channel_call": "電話する" + "account_basics_phone_numbers_dialog_channel_call": "電話する", + "error_boundary_button_reload_ntfy": "ntfyをリロード", + "prefs_appearance_theme_light": "ライトモード", + "web_push_subscription_expiring_title": "通知は一時停止されます", + "web_push_subscription_expiring_body": "ntfyを開いて通知の受信を継続させてください", + "alert_notification_ios_install_required_description": "Shareアイコンをクリック・ホーム画面に追加してiOSでの通知を有効化して下さい", + "action_bar_mute_notifications": "通知をミュート", + "action_bar_unmute_notifications": "通知ミュートを解除", + "alert_notification_permission_denied_title": "通知はブロックされています", + "alert_notification_permission_denied_description": "ブラウザで通知を再度有効化してください", + "notifications_actions_failed_notification": "アクション失敗", + "alert_notification_ios_install_required_title": "iOS用インストールが必要です", + "publish_dialog_checkbox_markdown": "Markdownとして表示", + "subscribe_dialog_subscribe_use_another_background_info": "ウェブアプリが開かれていない場合は他のサーバーからの通知は受信されません", + "prefs_notifications_web_push_title": "バックグラウンド通知", + "prefs_notifications_web_push_enabled_description": "ウェブアプリが開かれていなくても通知を受信します (Web Push経由)", + "prefs_notifications_web_push_disabled_description": "ウェブアプリが開かれていなくても通知を受信します (WebSocket経由)", + "prefs_notifications_web_push_enabled": "{{server}}で有効", + "prefs_notifications_web_push_disabled": "無効", + "prefs_appearance_theme_title": "テーマ", + "prefs_appearance_theme_system": "システム (既定)", + "prefs_appearance_theme_dark": "ダークモード", + "web_push_unknown_notification_title": "不明な通知を受信しました", + "web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります" } From db2dc09189d0e45e60064a08d0e1f1bc4686df8a Mon Sep 17 00:00:00 2001 From: K0ntact Date: Sat, 9 Nov 2024 11:48:42 +0000 Subject: [PATCH 069/378] Translated using Weblate (Vietnamese) Currently translated at 7.1% (29 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/vi/ --- web/public/static/langs/vi.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/web/public/static/langs/vi.json b/web/public/static/langs/vi.json index b2f94441..6167c4bc 100644 --- a/web/public/static/langs/vi.json +++ b/web/public/static/langs/vi.json @@ -9,13 +9,23 @@ "signup_already_have_account": "Đã có tài khoản? Đăng nhập!", "signup_disabled": "Đăng kí bị đóng", "signup_error_username_taken": "Tên {{username}} đã được sử dụng", - "signup_error_creation_limit_reached": "Đã bị giới hạn tạo tài khoản", + "signup_error_creation_limit_reached": "Đã đạt giới hạn tạo tài khoản", "login_title": "Đăng nhập vào tài khoản ntfy", "login_link_signup": "Đăng kí", - "login_disabled": "Đăng nhập bị đóng", + "login_disabled": "Đăng nhập bị vô hiệu hóa", "action_bar_show_menu": "Hiện menu", "signup_form_password": "Mật khẩu", "action_bar_settings": "Cài đặt", "signup_form_confirm_password": "Xác nhận mật khẩu", - "signup_form_button_submit": "Đăng kí" + "signup_form_button_submit": "Đăng kí", + "action_bar_change_display_name": "Đổi tên hiển thị", + "action_bar_send_test_notification": "Gửi thông báo thử", + "action_bar_clear_notifications": "Xóa tất cả thông báo", + "action_bar_logo_alt": "Logo ntfy", + "action_bar_account": "Tài khoản", + "action_bar_reservation_limit_reached": "Đã đạt giới hạn", + "action_bar_unsubscribe": "Hủy đăng kí", + "action_bar_unmute_notifications": "Bật thông báo", + "action_bar_toggle_mute": "Bật/tắt thông báo", + "action_bar_mute_notifications": "Tắt thông báo" } From b81f7b21a9d39d57fe44872b98c8e9f0476ef6eb Mon Sep 17 00:00:00 2001 From: Luis Eduardo Brito Date: Sun, 10 Nov 2024 20:09:29 +0000 Subject: [PATCH 070/378] Translated using Weblate (Portuguese (Brazil)) Currently translated at 83.9% (340 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/ --- web/public/static/langs/pt_BR.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json index bfaf68af..90c9ab7a 100644 --- a/web/public/static/langs/pt_BR.json +++ b/web/public/static/langs/pt_BR.json @@ -321,5 +321,23 @@ "account_upgrade_dialog_tier_current_label": "Atual", "account_upgrade_dialog_tier_price_per_month": "mês", "account_upgrade_dialog_button_cancel": "Cancelar", - "account_upgrade_dialog_tier_selected_label": "Selecionado" + "account_upgrade_dialog_tier_selected_label": "Selecionado", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por arquivo", + "account_tokens_table_last_access_header": "Último acesso", + "account_upgrade_dialog_button_cancel_subscription": "Cancelar assinatura", + "account_tokens_table_never_expires": "Nunca expira", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} cobrado anualmente. Salvar {{save}}.", + "account_upgrade_dialog_tier_features_no_calls": "Nenhuma chamada", + "account_tokens_table_token_header": "Token", + "account_upgrade_dialog_button_update_subscription": "Atualizar assinatura", + "account_tokens_table_current_session": "Sessão atual do navegador", + "account_tokens_table_copied_to_clipboard": "Token de acesso copiado", + "account_tokens_title": "Tokens de Acesso", + "account_upgrade_dialog_button_redirect_signup": "Cadastre-se agora", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas diárias", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} daily phone calls", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} armazenamento total", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} por ano. Cobrado mensalmente.", + "account_upgrade_dialog_button_pay_now": "Pague agora para assinar", + "account_tokens_table_expires_header": "Expira" } From 1b8906f1fd3365121c5cbe67c31fb9d8a6fa96fc Mon Sep 17 00:00:00 2001 From: Cairo Braga Date: Sat, 16 Nov 2024 19:29:10 +0000 Subject: [PATCH 071/378] Translated using Weblate (Portuguese (Brazil)) Currently translated at 84.1% (341 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/ --- web/public/static/langs/pt_BR.json | 47 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json index 90c9ab7a..9d4dba3a 100644 --- a/web/public/static/langs/pt_BR.json +++ b/web/public/static/langs/pt_BR.json @@ -8,10 +8,10 @@ "nav_button_settings": "Configurações", "nav_button_subscribe": "Inscrever no tópico", "alert_notification_permission_required_title": "Notificações estão desativadas", - "alert_notification_permission_required_description": "Conceder ao navegador permissão para mostrar notificações.", + "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações", "alert_notification_permission_required_button": "Conceder agora", "alert_not_supported_title": "Notificações não são suportadas", - "alert_not_supported_description": "Notificações não são suportadas pelo seu navegador.", + "alert_not_supported_description": "Notificações não são suportadas pelo seu navegador", "notifications_copied_to_clipboard": "Copiado para a área de transferência", "notifications_tags": "Etiquetas", "notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência", @@ -189,15 +189,15 @@ "prefs_users_delete_button": "Excluir usuário", "error_boundary_unsupported_indexeddb_title": "Navegação anônima não suportada", "error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.

Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso nesta edição do GitHub, ou falar conosco em Discord ou Matrix.", - "action_bar_reservation_add": "Reserve topic", - "action_bar_reservation_edit": "Change reservation", - "signup_disabled": "Registrar está desativado", - "signup_error_username_taken": "Usuário {{username}} já existe", + "action_bar_reservation_add": "Reservar tópico", + "action_bar_reservation_edit": "Mudar reserva", + "signup_disabled": "O registro está desativado", + "signup_error_username_taken": "O nome de usuário {{username}} já está em uso", "signup_error_creation_limit_reached": "Limite de criação de contas atingido", "action_bar_reservation_delete": "Remover reserva", "action_bar_account": "Conta", - "action_bar_change_display_name": "Change display name", - "common_copy_to_clipboard": "Copiar para área de transferência", + "action_bar_change_display_name": "Mudar nome de exibição", + "common_copy_to_clipboard": "Copiar para a Área de Transferência", "login_link_signup": "Registrar", "login_title": "Entrar na sua conta ntfy", "login_form_button_submit": "Entrar", @@ -210,13 +210,13 @@ "action_bar_sign_up": "Registrar", "nav_button_account": "Conta", "signup_title": "Criar uma conta ntfy", - "signup_form_username": "Usuário", + "signup_form_username": "Nome de usuário", "signup_form_password": "Senha", "signup_form_confirm_password": "Confirmar senha", - "signup_form_button_submit": "Registrar", + "signup_form_button_submit": "Criar conta", "account_basics_phone_numbers_title": "Telefones", - "signup_form_toggle_password_visibility": "Ativar visibilidade de senha", - "signup_already_have_account": "Já possui uma conta? Entrar!", + "signup_form_toggle_password_visibility": "Alterar visibilidade da senha", + "signup_already_have_account": "Já tem uma conta? Entre!", "nav_upgrade_banner_label": "Atualizar para ntfy Pro", "account_basics_phone_numbers_dialog_description": "Para usar o recurso de notificação de chamada, é necessários adicionar e verificar pelo menos um número de telefone. A verificação pode ser feita por SMS ou chamada telefônica.", "account_basics_phone_numbers_description": "Para notificações de chamada telefônica", @@ -224,7 +224,7 @@ "account_basics_tier_canceled_subscription": "Sua assinatura foi cancelada e será rebaixada para uma conta gratuita em {{date}}.", "account_basics_password_dialog_current_password_incorrect": "Senha incorreta", "account_basics_phone_numbers_dialog_number_label": "Número de telefone", - "account_basics_password_dialog_button_submit": "Alterar senha", + "account_basics_password_dialog_button_submit": "Mudar senha", "reserve_dialog_checkbox_label": "Guardar tópico e configurar acesso", "account_basics_username_title": "Nome de usuário", "account_basics_phone_numbers_dialog_check_verification_button": "Confirmar código", @@ -250,11 +250,11 @@ "account_basics_tier_free": "Grátis", "account_basics_tier_admin": "Administrador", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nenhum número de telefone verificado", - "account_basics_password_description": "Alterar a senha da sua conta", + "account_basics_password_description": "Mudar a senha da sua conta", "publish_dialog_call_label": "Chamada telefônica", "account_usage_calls_title": "Chamadas de telefone feitas", "account_basics_tier_basic": "Básico", - "alert_not_supported_context_description": "Notificações são suportadas apenas através de HTTPS. Esta é uma limitação da API de Notificações.", + "alert_not_supported_context_description": "Notificações são suportadas somente por HTTPS. Essa é uma limitação da Notifications API.", "account_basics_phone_numbers_copied_to_clipboard": "Número de telefone copiado para a área de transferência", "account_basics_tier_title": "Tipo de conta", "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444", @@ -268,14 +268,14 @@ "account_basics_password_dialog_new_password_label": "Nova senha", "display_name_dialog_placeholder": "Nome de exibição", "account_usage_of_limit": "de {{limit}}", - "account_basics_password_dialog_title": "Alterar senha", + "account_basics_password_dialog_title": "Mudar senha", "account_usage_limits_reset_daily": "Os limites de uso são redefinidos diariamente à meia-noite (UTC)", "account_usage_unlimited": "Ilimitado", "account_basics_password_dialog_current_password_label": "Senha atual", "account_usage_reservations_title": "Tópicos reservados", "account_usage_calls_none": "Nenhum telefonema pode ser feito com esta conta", - "display_name_dialog_title": "Alterar o nome de exibição", - "nav_upgrade_banner_description": "Guarde tópicos, mais mensagens & emails e anexos grandes", + "display_name_dialog_title": "Alterar nome de exibição", + "nav_upgrade_banner_description": "Reserve tópicos, mais mensagens e e-mails, e anexos maiores", "publish_dialog_call_reset": "Remover chamada telefônica", "account_basics_phone_numbers_dialog_code_label": "Código de verificação", "account_basics_tier_paid_until": "Assinatura paga até {{date}}, será renovada automaticamente", @@ -302,15 +302,15 @@ "subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas quando o web app não estiver aberto", "account_usage_basis_ip_description": "As estatísticas e limites de uso desta conta são baseados no seu endereço IP, portanto, podem ser compartilhados com outros usuários. Os limites mostrados acima são aproximados com base nos limites de taxa existentes.", "account_usage_cannot_create_portal_session": "Não foi possível abrir o portal de cobrança", - "account_delete_description": "Deletar conta permanentemente", + "account_delete_description": "Deletar sua conta permanentemente", "account_delete_dialog_button_cancel": "Cancelar", "account_delete_dialog_button_submit": "Deletar conta permanentemente", "account_upgrade_dialog_interval_monthly": "Mensal", "account_upgrade_dialog_interval_yearly_discount_save": "desconto de {{discount}}%", "account_upgrade_dialog_interval_yearly_discount_save_up_to": "desconto de até {{discount}}%", "account_upgrade_dialog_cancel_warning": "Isso cancelará sua assinatura e fará downgrade de sua conta em {{date}}. Nessa data, as reservas de tópicos, bem como as mensagens armazenadas em cache no servidor serão excluídas.", - "account_upgrade_dialog_reservations_warning_one": "O nível selecionada permite menos tópicos reservados do que a camada atual. Antes de alterar seu nível, exclua pelo menos uma reserva. Você pode remover reservas nas Configurações", - "account_upgrade_dialog_reservations_warning_other": "O plano selecionado permite menos tópicos reservados do que o seu plano atual. Antes de mudar seu plano, exclua por favor ao menos {{count}} reservas. Você pode remover reservas em Configurações.", + "account_upgrade_dialog_reservations_warning_one": "O nível selecionado permite menos tópicos reservados do que o nível atual. Antes de alterar seu nível, exclua pelo menos uma reserva. Você pode remover reservas nas Configurações.", + "account_upgrade_dialog_reservations_warning_other": "O nível selecionado permite menos tópicos reservados do que o seu nível atual. Antes de mudar seu nível, por favor exclua ao menos {{count}} reservas. Você pode remover reservas nas Configurações.", "account_upgrade_dialog_tier_features_no_reservations": "Sem tópicos reservados", "account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensagen diária", "account_upgrade_dialog_tier_features_emails_one": "{{emails}} email diário", @@ -335,9 +335,10 @@ "account_tokens_title": "Tokens de Acesso", "account_upgrade_dialog_button_redirect_signup": "Cadastre-se agora", "account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas diárias", - "account_upgrade_dialog_tier_features_calls_other": "{{calls}} daily phone calls", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} chamadas telefônicas diárias", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} armazenamento total", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} por ano. Cobrado mensalmente.", "account_upgrade_dialog_button_pay_now": "Pague agora para assinar", - "account_tokens_table_expires_header": "Expira" + "account_tokens_table_expires_header": "Expira", + "prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta." } From 1e1b2be46405ee9dd9918248c866141f8fd63b7a Mon Sep 17 00:00:00 2001 From: Ed Date: Sat, 16 Nov 2024 19:17:31 +0000 Subject: [PATCH 072/378] Translated using Weblate (Portuguese) Currently translated at 76.5% (310 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/ --- web/public/static/langs/pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json index d5946536..8e57445e 100644 --- a/web/public/static/langs/pt.json +++ b/web/public/static/langs/pt.json @@ -291,7 +291,7 @@ "account_usage_calls_title": "Ligações realizadas", "account_usage_calls_none": "Esta conta não pode realizar ligações", "account_usage_reservations_title": "Tópicos reservados", - "account_basics_username_title": "Utilizador", + "account_basics_username_title": "Usuário", "account_delete_dialog_description": "Isto irá eliminar definitivamente a sua conta, incluindo dados que estejam armazenados no servidor. Apos ser eliminado, o nome de utilizador ficará indisponível durante 7 dias. Se deseja mesmo proceder, por favor confirm com a sua palavra pass na caixa abaixo.", "account_delete_dialog_button_submit": "Eliminar conta definitivamente", "account_delete_dialog_billing_warning": "Eliminar a sua conta também cancela a sua subscrição de faturação imediatamente. Não terá acesso ao portal de faturação de futuro.", From 86c548ae37e91e3a91878034848c0e779dadc02f Mon Sep 17 00:00:00 2001 From: Cairo Braga Date: Sat, 16 Nov 2024 19:16:50 +0000 Subject: [PATCH 073/378] Translated using Weblate (Portuguese) Currently translated at 76.5% (310 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/ --- web/public/static/langs/pt.json | 49 +++++++++++++++++---------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json index 8e57445e..1c988568 100644 --- a/web/public/static/langs/pt.json +++ b/web/public/static/langs/pt.json @@ -16,7 +16,7 @@ "nav_button_muted": "Notificações desativadas", "nav_button_connecting": "A ligar", "alert_notification_permission_required_title": "As notificações estão desativadas", - "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações.", + "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações", "alert_not_supported_title": "Notificações não suportadas", "notifications_list": "Lista de notificações", "alert_not_supported_description": "As notificações não são suportadas pelo seu navegador", @@ -215,14 +215,14 @@ "action_bar_reservation_add": "Reservar tópico", "action_bar_sign_up": "Registar", "nav_button_account": "Conta", - "common_copy_to_clipboard": "Copiar", - "nav_upgrade_banner_label": "Atualizar para ntfy Pro", - "alert_not_supported_context_description": "Notificações são suportadas apenas sobre HTTPS. Essa é uma limitação da API de Notificações.", - "display_name_dialog_title": "Alterar nome mostrado", - "display_name_dialog_description": "Configura um nome alternativo ao tópico que é mostrado na lista de assinaturas. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.", - "display_name_dialog_placeholder": "Nome exibido", + "common_copy_to_clipboard": "Copiar à área de transferência", + "nav_upgrade_banner_label": "Upgrade para ntfy Pro", + "alert_not_supported_context_description": "As notificações são apenas suportadas através de HTTPS. Isto é uma limitação da Notifications API.", + "display_name_dialog_title": "Alterar o nome público", + "display_name_dialog_description": "Configurar um nome alternativo para um tópico que é mostrado na lista de subscrições. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.", + "display_name_dialog_placeholder": "Nome público", "reserve_dialog_checkbox_label": "Reservar tópico e configurar acesso", - "publish_dialog_call_label": "Chamada telefônica", + "publish_dialog_call_label": "Chamada telefónica", "publish_dialog_call_placeholder": "Número de telefone para ligar com a mensagem, ex: +12223334444, ou 'Sim'", "publish_dialog_call_reset": "Remover chamada telefônica", "publish_dialog_chip_call_label": "Chamada telefônica", @@ -231,17 +231,17 @@ "alert_notification_ios_install_required_description": "Clique no ícone Compartilhar e Adicionar à Tela Inicial para ativar as notificações no iOS", "publish_dialog_checkbox_markdown": "Formatar como Markdown", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Números de telefone não verificados", - "subscribe_dialog_error_topic_already_reserved": "Tópico já está reservado", + "subscribe_dialog_error_topic_already_reserved": "Tópico já reservado", "action_bar_mute_notifications": "Silenciar notificações", "alert_notification_permission_denied_title": "Notificações estão bloqueadas", "alert_notification_permission_denied_description": "Por favor reative-as em seu navegador", "alert_notification_ios_install_required_title": "Requer instalação em iOS", "notifications_actions_failed_notification": "Houve uma falha na ação", - "publish_dialog_call_item": "Ligar para o número {{number}}", + "publish_dialog_call_item": "Ligar para o número de telefone {{number}}", "subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas enquanto o aplicativo web não estiver aberto", - "account_basics_username_description": "Olá, é você ❤", - "account_basics_password_dialog_new_password_label": "Nova senha", - "account_basics_password_dialog_current_password_incorrect": "Senha incorreta", + "account_basics_username_description": "Olá, és tu ❤", + "account_basics_password_dialog_new_password_label": "Nova palavra-passe", + "account_basics_password_dialog_current_password_incorrect": "Palavra-passe inválida", "account_basics_phone_numbers_title": "Números de telefone", "account_basics_phone_numbers_dialog_description": "Para utilizar o recurso de notificação por ligação, você precisa adicionar e verificar pelo menos um número de telefone. A verificação poderá ser feita via SMS ou ligação telefônica.", "account_basics_phone_numbers_dialog_title": "Adicionar número de telefone", @@ -258,20 +258,20 @@ "account_usage_reservations_none": "Esta conta não possui tópicos reservados", "account_usage_attachment_storage_title": "Armazenamento de anexos", "account_usage_emails_title": "E-mails enviados", - "account_basics_password_description": "Alterar a senha da sua conta", - "account_basics_password_dialog_title": "Alterar a senha", + "account_basics_password_description": "Mudar a palavra-passe da conta", + "account_basics_password_dialog_title": "Mudar a palavra-passe", "account_basics_phone_numbers_description": "Para notificações por ligação", "account_basics_tier_paid_until": "Assinatura paga até {{date}}, e será renovada automaticamente", - "account_basics_password_dialog_confirm_password_label": "Confirmar senha", - "account_basics_password_dialog_button_submit": "Alterar senha", + "account_basics_password_dialog_confirm_password_label": "Confirmar palavra-passe", + "account_basics_password_dialog_button_submit": "Mudar palavra-passe", "account_basics_title": "Conta", - "account_basics_username_admin_tooltip": "Você é Administrador", - "account_basics_password_title": "Senha", - "account_basics_password_dialog_current_password_label": "Senha atual", + "account_basics_username_admin_tooltip": "És Admin", + "account_basics_password_title": "Palavra-passe", + "account_basics_password_dialog_current_password_label": "Palavra-passe atual", "account_basics_phone_numbers_no_phone_numbers_yet": "Nenhum número de telefone", "account_basics_phone_numbers_copied_to_clipboard": "Telefones copiados para área de transferência", "account_basics_phone_numbers_dialog_channel_sms": "SMS", - "account_usage_title": "Uso", + "account_usage_title": "Utilização", "account_usage_of_limit": "de {{limit}}", "account_usage_unlimited": "Ilimitado", "account_usage_limits_reset_daily": "Limites de uso são resetados diariamente à meia noite (UTC)", @@ -292,7 +292,7 @@ "account_usage_calls_none": "Esta conta não pode realizar ligações", "account_usage_reservations_title": "Tópicos reservados", "account_basics_username_title": "Usuário", - "account_delete_dialog_description": "Isto irá eliminar definitivamente a sua conta, incluindo dados que estejam armazenados no servidor. Apos ser eliminado, o nome de utilizador ficará indisponível durante 7 dias. Se deseja mesmo proceder, por favor confirm com a sua palavra pass na caixa abaixo.", + "account_delete_dialog_description": "Isto irá eliminar definitivamente a sua conta, incluindo dados que estejam armazenados no servidor. Apos ser eliminado, o nome de utilizador ficará indisponível durante 7 dias. Se deseja mesmo proceder, por favor confirme com a sua palavra-passe na caixa abaixo.", "account_delete_dialog_button_submit": "Eliminar conta definitivamente", "account_delete_dialog_billing_warning": "Eliminar a sua conta também cancela a sua subscrição de faturação imediatamente. Não terá acesso ao portal de faturação de futuro.", "account_upgrade_dialog_title": "Alterar o nível da sua conta", @@ -300,7 +300,7 @@ "account_upgrade_dialog_interval_yearly": "Anualmente", "account_upgrade_dialog_interval_yearly_discount_save": "poupe {{discount}}%", "account_upgrade_dialog_interval_yearly_discount_save_up_to": "poupe até {{discount}}%", - "account_delete_dialog_label": "Palavra pass", + "account_delete_dialog_label": "Palavra-passe", "account_usage_cannot_create_portal_session": "Impossível abrir o portal de faturação", "account_usage_basis_ip_description": "Estatísticas de utilização e limites para esta conta são baseadas no seu endereço IP, pelo que podem ser partilhados com outros utilizadores. Os limites mostrados acima são aproximados com base nos limites existentes.", "account_usage_attachment_storage_description": "{{filesize}} por ficheiro, eliminado após {{expiry}}", @@ -308,5 +308,6 @@ "account_delete_description": "Eliminar definitivamente a sua conta", "account_delete_dialog_button_cancel": "Cancelar", "account_upgrade_dialog_cancel_warning": "Isto irá cancelar a sua assinatura, e fazer downgrade da sua conta em {{date}}. Nessa data, tópicos reservados bem como mensagens guardadas no servidor serão eliminados.", - "account_upgrade_dialog_proration_info": "Proporção: Quando atualizar entre planos pagos, a diferença de preço será debitada imediatamente. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação." + "account_upgrade_dialog_proration_info": "Proporção: Quando atualizar entre planos pagos, a diferença de preço será debitada imediatamente. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação.", + "prefs_users_description_no_sync": "Utilizadores e palavras-passe não estão sincronizados com a sua conta." } From 70a9301e25eda903e2126ece20dae7e21e882578 Mon Sep 17 00:00:00 2001 From: Christer Solstrand Johannessen Date: Fri, 22 Nov 2024 12:22:02 +0000 Subject: [PATCH 074/378] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 51.1% (207 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/ --- web/public/static/langs/nb_NO.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json index ca259523..530c5242 100644 --- a/web/public/static/langs/nb_NO.json +++ b/web/public/static/langs/nb_NO.json @@ -195,5 +195,16 @@ "signup_form_username": "Brukernavn", "signup_form_password": "Passord", "signup_form_button_submit": "Meld deg på", - "signup_form_confirm_password": "Bekreft passord" + "signup_form_confirm_password": "Bekreft passord", + "signup_disabled": "Registrering er deaktivert", + "common_copy_to_clipboard": "Kopier til utklippstavle", + "signup_form_toggle_password_visibility": "Slå av/på passordvisning", + "signup_already_have_account": "Har du allerede en konto? Logg inn!", + "signup_error_username_taken": "Brukernavnet {{username}} er allerede opptatt", + "signup_error_creation_limit_reached": "Grense for nye kontoer nådd", + "login_title": "Logg inn på ntfy-kontoen din", + "login_form_button_submit": "Logg inn", + "login_link_signup": "Registrer deg", + "login_disabled": "Innlogging deaktivert", + "action_bar_change_display_name": "Endre visningsnavn" } From b26666f635b8e9873e96f3a4f9a5f24d8350601d Mon Sep 17 00:00:00 2001 From: Christer Solstrand Johannessen Date: Mon, 25 Nov 2024 13:09:59 +0000 Subject: [PATCH 075/378] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/ --- web/public/static/langs/nb_NO.json | 205 ++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 4 deletions(-) diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json index 530c5242..2bcf6391 100644 --- a/web/public/static/langs/nb_NO.json +++ b/web/public/static/langs/nb_NO.json @@ -3,7 +3,7 @@ "action_bar_settings": "Innstillinger", "action_bar_send_test_notification": "Send testmerknad", "action_bar_clear_notifications": "Tøm alle merknader", - "action_bar_unsubscribe": "Opphev abonnement", + "action_bar_unsubscribe": "Meld av", "message_bar_type_message": "Skriv en melding her", "nav_button_all_notifications": "Alle merknader", "nav_button_settings": "Innstillinger", @@ -133,8 +133,8 @@ "publish_dialog_chip_delay_label": "Forsink leveringen", "publish_dialog_details_examples_description": "For eksempler og en detaljert beskrivelse av alle sendefunksjoner, se dokumentasjonen.", "publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com", - "alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.", - "alert_not_supported_description": "Varsler støttes ikke i nettleseren din.", + "alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler", + "alert_not_supported_description": "Varsler støttes ikke i nettleseren din", "notifications_attachment_file_app": "Android-app-fil", "notifications_no_subscriptions_description": "Klikk på \"{{linktext}}\"-koblingen for å opprette eller abonnere på et emne. Etter det kan du sende meldinger via PUT eller POST, og du vil motta varsler her.", "notifications_actions_http_request_title": "Send HTTP {{metode}} til {{url}}", @@ -206,5 +206,202 @@ "login_form_button_submit": "Logg inn", "login_link_signup": "Registrer deg", "login_disabled": "Innlogging deaktivert", - "action_bar_change_display_name": "Endre visningsnavn" + "action_bar_change_display_name": "Endre visningsnavn", + "account_basics_tier_interval_yearly": "årlig", + "account_basics_tier_change_button": "Endre", + "account_usage_reservations_title": "Reserverte emner", + "account_usage_cannot_create_portal_session": "Kunne ikke åpne betalingsportalen", + "account_delete_dialog_label": "Passord", + "account_tokens_table_copied_to_clipboard": "Tilgangstoken kopiert", + "account_tokens_table_last_origin_tooltip": "Fra IP-adresse {{ip}}, klikk for å gjøre oppslag", + "account_tokens_dialog_title_create": "Opprett tilgangstoken", + "account_tokens_delete_dialog_title": "Slett tilgangstoken", + "prefs_users_table_cannot_delete_or_edit": "Kan ikke slette eller redigere innlogget bruker", + "prefs_reservations_table_everyone_deny_all": "Bare jeg kan publisere og abonnere", + "prefs_reservations_dialog_access_label": "Tilgang", + "reservation_delete_dialog_action_keep_title": "Behold mellomlagrede meldinger og vedlegg", + "action_bar_reservation_add": "Reserver emne", + "action_bar_reservation_edit": "Endre reservasjon", + "action_bar_reservation_delete": "Fjern reservasjon", + "action_bar_reservation_limit_reached": "Grense nådd", + "account_basics_phone_numbers_dialog_description": "For å bruke ringevarslingsfunksjonen må du legge til og verifisere minst ett telefonnummer. Verifisering kan gjøres vis SMS eller oppringing.", + "account_basics_tier_interval_monthly": "månedlig", + "account_basics_tier_upgrade_button": "Oppgrader til Pro", + "account_usage_emails_title": "E-poster sendt", + "account_delete_description": "Slett kontoen din permanent", + "account_usage_calls_title": "Telefonsamtaler", + "account_upgrade_dialog_interval_monthly": "Månedlig", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverte emner", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige meldinger", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} daglige e-poster", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} daglige telefonsamtaler", + "account_upgrade_dialog_tier_selected_label": "Valgt", + "account_upgrade_dialog_tier_current_label": "Nåværende", + "account_upgrade_dialog_button_cancel": "Avbryt", + "account_upgrade_dialog_billing_contact_email": "For faktureringsspørsmål, vennligst kontakt oss direkte.", + "account_tokens_table_token_header": "Token", + "account_tokens_table_label_header": "Etikett", + "account_tokens_table_cannot_delete_or_edit": "Kan ikke redigere eller slette nåværende økt-token", + "account_tokens_table_create_token_button": "Opprett tilgangstoken", + "account_tokens_dialog_expires_unchanged": "La utløpsdato være uendret", + "account_tokens_dialog_expires_x_hours": "Token utløper om {{hours}} timer", + "account_tokens_delete_dialog_description": "Før du sletter et tilgangstoken, sørg for at ingen applikasjoner eller script bruker det. Denne handlingen kan ikke angres.", + "account_tokens_delete_dialog_submit_button": "Slett token permanent", + "prefs_users_description_no_sync": "Brukere og passord synkroniseres ikke til kontoen din.", + "prefs_reservations_dialog_title_delete": "Slett emnereservasjon", + "prefs_reservations_dialog_topic_label": "Emne", + "display_name_dialog_title": "Endre visningsnavn", + "reserve_dialog_checkbox_label": "Rserver emne og sett opp tilgang", + "publish_dialog_chip_call_label": "Telefonsamtale", + "account_basics_tier_free": "Gratis", + "account_basics_tier_basic": "Grunnleggende", + "account_basics_tier_canceled_subscription": "Abonnementet ditt ble avsluttet og blir degradert til en gratiskonto den {{date}}.", + "account_delete_dialog_description": "Dette vil slette kontoen din permanent, inkludert alle data som er lagret på serveren. Etter sletting vil brukernavnet ditt være utilgjengelig i 7 dager. Hvis du virkelig vil fortsette, vennligst bekreft ved å skrive passordet ditt i boksen under.", + "account_upgrade_dialog_proration_info": "Pro-rate: Når du oppgraderer mellom betalte kontotyper, vil prisforskjellen bli fakturert umiddelbart. Når du nedgraderer til en billigere kontotype, vil det allerede innbetalte beløpet brukes til å betale for fremtidige regningsperioder.", + "account_upgrade_dialog_reservations_warning_other": "Det valgte nivået tillater færre reserverte emner enn ditt nåvære nivå. Før du endrer nivå, vennligst slett minst {{count}} reservasjoner. Du kan slette reservasjoner i Innstillingene.", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} daglig melding", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pr. år. Fakturert månedlig.", + "account_upgrade_dialog_button_redirect_signup": "Registrer deg nå", + "account_upgrade_dialog_button_pay_now": "Betal nå og abonner", + "account_upgrade_dialog_button_cancel_subscription": "Avslutt abonnement", + "account_tokens_description": "Bruk tilgangstokener når du publiserer og abonnerer via ntfy-APIet, slik at du ikke trenger å sende innloggingsinformasjon for kontoen din. Se dokumentasjonen for å lære mer.", + "account_tokens_table_current_session": "Nåværende nettleserøkt", + "prefs_appearance_theme_system": "System (standard)", + "prefs_notifications_web_push_disabled_description": "Varslinger mottas når web-appen kjører (via WebSocket)", + "prefs_appearance_theme_title": "Tema", + "prefs_appearance_theme_dark": "Mørk modus", + "prefs_appearance_theme_light": "Lys modus", + "prefs_reservations_title": "Reserverte emner", + "prefs_reservations_table_click_to_subscribe": "Klikk for å abonnere", + "prefs_reservations_table_everyone_read_write": "Alle kan publisere og abonnere", + "prefs_reservations_table_not_subscribed": "Ikke abonnent", + "prefs_reservations_table_everyone_write_only": "Jeg kan publisere og abonnere, alle andre kan publisere", + "prefs_reservations_dialog_title_add": "Reserver emne", + "prefs_reservations_dialog_title_edit": "Rediger reservert emne", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reservert emne", + "reservation_delete_dialog_action_delete_title": "Slett mellomlagrede meldinger og vedlegg", + "nav_upgrade_banner_label": "Oppgrader til ntfy Pro", + "nav_upgrade_banner_description": "Reserver emner, flere meldinger & e-poster, og større vedlegg", + "account_delete_dialog_button_submit": "Slett konto permanent", + "account_basics_username_description": "Hei, det er deg ❤", + "account_basics_username_admin_tooltip": "Du er administrator", + "account_basics_password_title": "Passord", + "account_basics_password_description": "Endre passordet ditt", + "account_usage_title": "Forbruk", + "account_delete_dialog_button_cancel": "Avbryt", + "account_tokens_dialog_title_delete": "Slett tilgangstoken", + "account_tokens_dialog_label": "Etikett, f.eks. Radarr-varslinger", + "prefs_reservations_table": "Tabell over reserverte emner", + "prefs_reservations_edit_button": "Rediger tilgang til emne", + "prefs_reservations_delete_button": "Nullstill tilgang til emne", + "prefs_reservations_table_topic_header": "Emne", + "account_basics_title": "Konto", + "account_basics_phone_numbers_dialog_code_label": "Verifiseringskode", + "alert_notification_permission_denied_title": "Varslinger blokkert", + "alert_notification_permission_denied_description": "Vennligst reaktiver dem i nettleseren din", + "alert_notification_ios_install_required_title": "iOS-installasjon kreves", + "alert_notification_ios_install_required_description": "Klikk på Del-ikonet og Legg til hjemmeskjerm for å aktivere varslinger på iOS", + "action_bar_mute_notifications": "Demp varslinger", + "action_bar_unmute_notifications": "Avdemp varslinger", + "action_bar_profile_title": "Profil", + "action_bar_profile_logout": "Logg ut", + "action_bar_sign_in": "Logg inn", + "action_bar_sign_up": "Registrer deg", + "alert_not_supported_context_description": "Varslinger er kun støttet over HTTPS. Dette er en begrensning i Varslings-APIet.", + "notifications_actions_failed_notification": "Handling feilet", + "display_name_dialog_description": "Angi et alternativt navn for et emne som vises i abonneringslisten. Dette hjelper til med å enklere identifisere emner med kompliserte navn.", + "display_name_dialog_placeholder": "Visningnavn", + "publish_dialog_call_label": "Telefonsamtale", + "publish_dialog_call_item": "Ring telefonnummer {{number}}", + "publish_dialog_call_reset": "Fjern telefonsamtale", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Ingen verfiserte telefonnumre", + "publish_dialog_checkbox_markdown": "Formatter som Markdown", + "subscribe_dialog_subscribe_use_another_background_info": "Varslinger fra andre servere vil ikke bli tatt imot når webappen ikke er åpen", + "subscribe_dialog_subscribe_button_generate_topic_name": "Generer navn", + "subscribe_dialog_error_topic_already_reserved": "Emne allerede reservert", + "account_basics_username_title": "Brukernavn", + "account_basics_password_dialog_title": "Endre passord", + "account_basics_password_dialog_current_password_label": "Nåværende passord", + "account_basics_password_dialog_new_password_label": "Nytt passord", + "account_basics_password_dialog_confirm_password_label": "Bekreft passord", + "account_basics_password_dialog_button_submit": "Endre passord", + "account_basics_password_dialog_current_password_incorrect": "Passordet er feil", + "account_basics_phone_numbers_title": "Telefonnumre", + "account_basics_phone_numbers_description": "For telefonvarsling", + "account_basics_phone_numbers_no_phone_numbers_yet": "Ingen telefonnumre enda", + "account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopiert til utklippstavle", + "account_basics_phone_numbers_dialog_title": "Legg til telefonnummer", + "account_basics_phone_numbers_dialog_number_label": "Telefonnummer", + "account_basics_phone_numbers_dialog_number_placeholder": "f.eks. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Ring meg", + "account_basics_phone_numbers_dialog_code_placeholder": "f.eks. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Bekreft kode", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Ring", + "account_usage_of_limit": "av {{limit}}", + "account_usage_unlimited": "Ubegrenset", + "account_usage_limits_reset_daily": "Forbruksgrenser nullstilles hver dag ved midnatt (UTC)", + "account_basics_tier_title": "Kontotype", + "account_basics_tier_description": "Din kontos styrke", + "account_basics_tier_admin": "Administrator", + "account_basics_tier_admin_suffix_with_tier": "(med {{tier}} nivå)", + "account_basics_tier_admin_suffix_no_tier": "(ingen nivå)", + "account_basics_tier_paid_until": "Abonnement betalt til {{date}}, og vil bli fornyet automatisk", + "account_basics_tier_payment_overdue": "Betalingen din har forfalt. Vennligst oppdater betalingsmetoden din, hvis ikke blir kontoen din snart degradert.", + "account_basics_tier_manage_billing_button": "Behandle betalinger", + "account_usage_messages_title": "Publiserte meldinger", + "account_usage_calls_none": "Ingen telefonsamtaler kan foretas med denne kontoen", + "account_usage_reservations_none": "Ingen reserverte emner for denne kontoen", + "account_usage_attachment_storage_title": "Vedleggslagring", + "account_usage_basis_ip_description": "Forbruksstatistikk og -grenser for denne kontoen er basert på IP-adressen din, så det kan være de er delt med andre brukere. Forbruksgrenser vist over er omtrentlige, basert på eksisterende begrensninger.", + "account_delete_title": "Slett konto", + "account_delete_dialog_billing_warning": "Sletting av kontoen din avslutter også abonnementet og betalingene dine umiddelbart. Du vil ikke ha tilgang til betalingsportalen lenger.", + "account_upgrade_dialog_title": "Endre kontonivå", + "account_upgrade_dialog_interval_yearly": "Årlig", + "account_upgrade_dialog_interval_yearly_discount_save": "spar {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "spar inntil {{discount}}%", + "account_upgrade_dialog_cancel_warning": "Dette vil avslutte abonnementet ditt, og nedgradere kontoen din den {{date}}. På den datoen vil alle emnereservasjoner såvel som meldinger lagret på serveren bli slettet.", + "account_upgrade_dialog_reservations_warning_one": "Det valgte nivået tillater færre reserverte emner enn ditt nåvære nivå. Før du endrer nivå, vennligst slett minst én reservasjon. Du kan slette reservasjoner i Innstillingene.", + "account_upgrade_dialog_tier_features_no_reservations": "Ingen reserverte emner", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} daglig e-post", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} daglige telefonsamtaler", + "account_upgrade_dialog_tier_features_no_calls": "Ingen telefonsamtaler", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pr. fil", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total lagringsplass", + "account_upgrade_dialog_tier_price_per_month": "måned", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} fakturert årlig. Spar {{save}}.", + "account_upgrade_dialog_billing_contact_website": "For faktureringsspørsmål, vennligst se vår nettside.", + "account_upgrade_dialog_button_update_subscription": "Oppdater abonnement", + "account_tokens_title": "Tilgangstokener", + "account_tokens_table_last_access_header": "Sist aksessert", + "account_tokens_table_expires_header": "Utløper", + "account_tokens_table_never_expires": "Utløper aldri", + "account_tokens_dialog_title_edit": "Rediger tilgangstoken", + "account_tokens_dialog_button_create": "Opprett token", + "account_tokens_dialog_button_update": "Oppdater token", + "account_tokens_dialog_button_cancel": "Avbryt", + "account_tokens_dialog_expires_label": "Tilgangstoken utløper om", + "account_tokens_dialog_expires_x_days": "Token utløper om {{days}} dager", + "account_tokens_dialog_expires_never": "Token utløper aldri", + "prefs_notifications_web_push_title": "Bakgrunnsvarslinger", + "prefs_notifications_web_push_enabled_description": "Varslinger mottas send om web-appen ikke kjører (via Web Push)", + "prefs_notifications_web_push_enabled": "Aktivert for {{server}}", + "prefs_notifications_web_push_disabled": "Deaktivert", + "prefs_reservations_description": "Du kan reservere emnenavn for personlig bruk her. Reservasjon av et emne gir deg eierskap over emnet og lar deg definere tilgangsrettigheter for andre brukere av dette emnet.", + "prefs_reservations_limit_reached": "Du har nådd grensen for antall reserverte emner du kan ha.", + "prefs_reservations_add_button": "Legg til reservert emne", + "prefs_reservations_table_access_header": "Tilgang", + "prefs_reservations_table_everyone_read_only": "Jeg kan publisere og abonnere, alle andre kan abonnere", + "prefs_reservations_dialog_description": "Reservering av et emne gir deg eierskap over emnet, og lar deg definere tilgangsrettigheter for andre brukere av emnet.", + "reservation_delete_dialog_description": "Ved å fjerne en reservasjon gir du fra deg eierskapet over emnet, og gir dermed andre muligheten til å reservere det. Du kan beholde eller slette eksisterende meldinger og vedlegg.", + "reservation_delete_dialog_action_keep_description": "Meldinger og vedlegg som er mellomlagret på serveren vil bli synlige for alle som kjenner til emnenavnet.", + "reservation_delete_dialog_action_delete_description": "Mellomlagrede meldinger og vedlegg vil bli permanent slettet. Denne handlingen kan ikke angres.", + "reservation_delete_dialog_submit_button": "Slett reservasjon", + "error_boundary_button_reload_ntfy": "Last inn ntfy på nytt", + "web_push_subscription_expiring_title": "Varslinger vil bli satt på pause", + "web_push_subscription_expiring_body": "Åpne ntfy for å fortsette å motta varslinger", + "web_push_unknown_notification_title": "Ukjent varsel mottatt fra server", + "web_push_unknown_notification_body": "Du må muligens oppdatere ntfy ved å åpne web-appen", + "account_usage_attachment_storage_description": "{{filesize}} pr. fil, slettet etter {{expiry}}" } From ae9fa856766b3b12f8ffb9e8a2550c6dc7c1eeed Mon Sep 17 00:00:00 2001 From: qtm Date: Tue, 3 Dec 2024 16:52:37 +0000 Subject: [PATCH 076/378] Translated using Weblate (Russian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/ --- web/public/static/langs/ru.json | 98 ++++++++++++++++----------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/web/public/static/langs/ru.json b/web/public/static/langs/ru.json index a1f26d70..0c2eaae3 100644 --- a/web/public/static/langs/ru.json +++ b/web/public/static/langs/ru.json @@ -67,7 +67,7 @@ "subscribe_dialog_subscribe_title": "Подписаться на тему", "publish_dialog_button_cancel": "Отмена", "subscribe_dialog_subscribe_description": "Темы могут быть не защищены паролем, поэтому укажите сложное имя. После подписки Вы сможете отправлять уведомления используя PUT/POST-запросы.", - "prefs_users_description": "Добавляйте/удаляйте пользователей для защищенных тем. Обратите внимание, что имя пользователя и пароль хранятся в локальном хранилище браузера.", + "prefs_users_description": "Вы можете управлять пользователями для защищённых тем. Учтите, что имя учётные данные хранятся в локальном хранилище браузера.", "error_boundary_description": "Это не должно было случиться. Нам очень жаль.
Если Вы можете уделить минуту своего времени, пожалуйста сообщите об этом на GitHub, или дайте нам знать через Discord или Matrix.", "publish_dialog_email_placeholder": "Адрес для пересылки уведомления. Например, phil@example.com", "publish_dialog_attach_placeholder": "Прикрепите файл по URL. Например, https://f-droid.org/F-Droid.apk", @@ -96,36 +96,36 @@ "subscribe_dialog_subscribe_button_subscribe": "Подписаться", "subscribe_dialog_login_title": "Требуется авторизация", "subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.", - "subscribe_dialog_login_username_label": "Имя пользователя. Например, phil", + "subscribe_dialog_login_username_label": "Имя пользователя. Например, oleg", "subscribe_dialog_login_password_label": "Пароль", "common_back": "Назад", "subscribe_dialog_login_button_login": "Войти", "subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован", "subscribe_dialog_error_user_anonymous": "анонимный пользователь", "prefs_notifications_title": "Уведомления", - "prefs_notifications_sound_title": "Звук уведомления", - "prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении", + "prefs_notifications_sound_title": "Звук уведомлений", + "prefs_notifications_sound_description_none": "При получении уведомлений не звуки не проигрываются", "prefs_notifications_sound_no_sound": "Без звука", "prefs_notifications_min_priority_title": "Минимальный приоритет", - "prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от приоритета", + "prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от их приоритета", "prefs_notifications_min_priority_description_x_or_higher": "Показывать уведомления, если приоритет {{number}} ({{name}}) или выше", "prefs_notifications_min_priority_description_max": "Показывать уведомления, если приоритет равен 5 (максимальный)", "prefs_notifications_min_priority_any": "Любой приоритет", "prefs_notifications_min_priority_low_and_higher": "Низкий приоритет и выше", "prefs_notifications_min_priority_max_only": "Только максимальный приоритет", - "prefs_notifications_delete_after_title": "Удалить уведомления", + "prefs_notifications_delete_after_title": "Удаление уведомлений", "prefs_notifications_delete_after_never": "Никогда", "prefs_notifications_delete_after_three_hours": "Через три часа", - "prefs_notifications_sound_description_some": "Уведомления воспроизводят звук {{sound}}", + "prefs_notifications_sound_description_some": "При уведомлениях проигрывается звук {{sound}}", "prefs_notifications_min_priority_default_and_higher": "Стандартный приоритет и выше", "prefs_notifications_delete_after_one_day": "Через день", "prefs_notifications_delete_after_one_week": "Через неделю", "prefs_notifications_delete_after_one_month": "Через месяц", "prefs_notifications_delete_after_never_description": "Уведомления никогда не удаляются автоматически", - "prefs_notifications_delete_after_three_hours_description": "Уведомления автоматически удаляются через три часа", - "prefs_notifications_delete_after_one_day_description": "Уведомления автоматически удаляются через один день", - "prefs_notifications_delete_after_one_week_description": "Уведомления автоматически удаляются через неделю", - "prefs_notifications_delete_after_one_month_description": "Уведомления автоматически удаляются через месяц", + "prefs_notifications_delete_after_three_hours_description": "Уведомления удаляются автоматически через три часа", + "prefs_notifications_delete_after_one_day_description": "Уведомления удаляются автоматически через один день", + "prefs_notifications_delete_after_one_week_description": "Уведомления удаляются автоматически через неделю", + "prefs_notifications_delete_after_one_month_description": "Уведомления удаляются автоматически через месяц", "prefs_users_title": "Управление пользователями", "prefs_users_add_button": "Добавить пользователя", "prefs_users_table_user_header": "Пользователь", @@ -133,7 +133,7 @@ "prefs_users_dialog_title_add": "Добавить пользователя", "prefs_users_dialog_title_edit": "Редактировать пользователя", "prefs_users_dialog_base_url_label": "URL-адрес сервера. Например, https://ntfy.sh", - "prefs_users_dialog_username_label": "Имя пользователя. Например, phil", + "prefs_users_dialog_username_label": "Имя пользователя. Например, oleg", "prefs_users_dialog_password_label": "Пароль", "common_cancel": "Отмена", "common_add": "Добавить", @@ -157,11 +157,11 @@ "emoji_picker_search_clear": "Сбросить поиск", "account_upgrade_dialog_cancel_warning": "Это действие отменит Вашу подписку и переведет Вашую учетную запись на бесплатное обслуживание {{date}}. При наступлении этой даты, все резервирования и сообщения в кэше будут удалены.", "account_tokens_table_create_token_button": "Создать токен доступа", - "account_tokens_table_last_origin_tooltip": "с IP-адреса {{ip}}, нажмите для подробностей", + "account_tokens_table_last_origin_tooltip": "С IP-адреса {{ip}}, нажмите для подробностей", "account_tokens_dialog_title_edit": "Изменить токен доступа", "account_delete_dialog_button_cancel": "Отмена", "account_delete_dialog_billing_warning": "Удаление учетной записи также отменяет все платные подписки. У Вас не будет доступа к порталу оплаты.", - "account_delete_dialog_description": "Это действие безвозвратно удалит Вашу учетную запись, включая все Ваши данные хранящиеся на сервере. После удаления, Ваше имя пользователя не будет доступно для регистрации в течении 7 дней. Если Вы действительно хотите продолжить, пожалуйста введите Ваш пароль ниже.", + "account_delete_dialog_description": "Это действие безвозвратно удалит вашу учётную запись, включая все данные, хранящиеся на сервере. После удаления имя пользователя вашей учётной записи не будет доступно для регистрации в течение 7 дней. Если вы точно хотите продолжить, пожалуйста, введите свой пароль ниже.", "account_delete_dialog_label": "Пароль", "reservation_delete_dialog_action_keep_description": "Сообщения и вложения которые находятся в кэше сервера станут доступны всем, кто знает имя темы.", "prefs_reservations_table": "Список зарезервированных тем", @@ -173,7 +173,7 @@ "prefs_reservations_table_not_subscribed": "Не подписан", "prefs_reservations_table_everyone_deny_all": "Только я могу публиковать и подписываться", "prefs_reservations_table_everyone_read_write": "Все могут публиковать и подписываться", - "prefs_reservations_table_click_to_subscribe": "Нажмите чтобы подписаться", + "prefs_reservations_table_click_to_subscribe": "Нажмите, чтобы подписаться", "prefs_reservations_dialog_title_add": "Зарезервировать тему", "prefs_reservations_dialog_title_delete": "Удалить резервирование", "prefs_reservations_dialog_title_edit": "Изменение резервированной темы", @@ -202,7 +202,7 @@ "account_tokens_dialog_expires_never": "Токен никогда не истекает", "prefs_notifications_sound_play": "Воспроизводить выбранный звук", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервированных тем", - "account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. сообщений в день", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. писем в день", "account_basics_tier_free": "Бесплатный", "account_tokens_dialog_title_create": "Создать токен доступа", "account_tokens_dialog_title_delete": "Удалить токен доступа", @@ -215,11 +215,11 @@ "account_upgrade_dialog_tier_current_label": "Текущая", "account_upgrade_dialog_button_cancel": "Отмена", "prefs_users_edit_button": "Редактировать пользователя", - "account_basics_tier_upgrade_button": "Подписаться на Pro", + "account_basics_tier_upgrade_button": "Обновить до Pro", "account_basics_tier_paid_until": "Подписка оплачена до {{date}} и будет продляться автоматически", "account_basics_tier_change_button": "Изменить", - "account_delete_dialog_button_submit": "Безвозвратно удалить учетную запись", - "account_upgrade_dialog_title": "Изменить уровень учетной записи", + "account_delete_dialog_button_submit": "Безвозвратно удалить эту учётную запись", + "account_upgrade_dialog_title": "Изменить уровень учётной записи", "account_usage_basis_ip_description": "Статистика и ограничения на использование учитываются по IP-адресу, поэтому они могут совмещаться с другими пользователями. Уровни, указанные выше, примерно соответствуют текущим ограничениям.", "publish_dialog_topic_reset": "Сбросить тему", "account_basics_tier_admin_suffix_no_tier": "(без подписки)", @@ -231,9 +231,9 @@ "signup_form_toggle_password_visibility": "Показать/скрыть пароль", "signup_disabled": "Регистрация недоступна", "signup_error_username_taken": "Имя пользователя {{username}} уже занято", - "signup_title": "Создать учетную запись ntfy", - "signup_already_have_account": "Уже есть учетная запись? Войдите!", - "signup_error_creation_limit_reached": "Лимит на создание учетных записей исчерпан", + "signup_title": "Создать учётную запись ntfy", + "signup_already_have_account": "Уже есть учётная запись? Войдите!", + "signup_error_creation_limit_reached": "Исчерпано ограничение создания учётных записей", "login_form_button_submit": "Вход", "login_link_signup": "Регистрация", "login_disabled": "Вход недоступен", @@ -249,12 +249,12 @@ "message_bar_publish": "Опубликовать сообщение", "nav_button_muted": "Уведомления заглушены", "nav_button_connecting": "установка соединения", - "action_bar_account": "Учетная запись", - "login_title": "Вход в Вашу учетную запись ntfy", + "action_bar_account": "Учётная запись", + "login_title": "Войдите в учётную запись ntfy", "action_bar_reservation_limit_reached": "Лимит исчерпан", "action_bar_toggle_mute": "Заглушить/разрешить уведомления", - "nav_button_account": "Учетная запись", - "nav_upgrade_banner_label": "Купить подписку ntfy Pro", + "nav_button_account": "Учётная запись", + "nav_upgrade_banner_label": "Подписка ntfy Pro", "message_bar_show_dialog": "Открыть диалог публикации", "notifications_list": "Список уведомлений", "notifications_list_item": "Уведомление", @@ -279,12 +279,12 @@ "subscribe_dialog_subscribe_base_url_label": "URL-адрес сервера", "subscribe_dialog_subscribe_button_generate_topic_name": "Сгенерировать случайное имя", "subscribe_dialog_error_topic_already_reserved": "Тема уже зарезервирована", - "account_basics_title": "Учетная запись", + "account_basics_title": "Учётная запись", "account_basics_username_title": "Имя пользователя", - "account_basics_username_admin_tooltip": "Вы Администратор", + "account_basics_username_admin_tooltip": "Вы администратор", "account_basics_password_title": "Пароль", - "account_basics_username_description": "Это Вы! :)", - "account_basics_password_description": "Смена пароля учетной записи", + "account_basics_username_description": "Это вы! :)", + "account_basics_password_description": "Смена пароля учётной записи", "account_basics_password_dialog_title": "Смена пароля", "account_basics_password_dialog_current_password_label": "Текущий пароль", "account_basics_password_dialog_current_password_incorrect": "Введен неверный пароль", @@ -292,11 +292,11 @@ "account_usage_of_limit": "из {{limit}}", "account_usage_unlimited": "Неограниченно", "account_usage_limits_reset_daily": "Ограничения сбрасываются ежедневно в полночь (UTC)", - "account_basics_tier_description": "Уровень Вашей учетной записи", + "account_basics_tier_description": "Уровень вашей учётной записи", "account_basics_tier_admin": "Администратор", - "account_basics_tier_admin_suffix_with_tier": "(с {{tier}} подпиской)", - "account_basics_tier_payment_overdue": "У Вас задолженность по оплате. Пожалуйста проверьте метод оплаты, иначе Вы скоро потеряете преимущества Вашей подписки.", - "account_basics_tier_canceled_subscription": "Ваша подписка была отменена; учетная запись перейдет на бесплатное обслуживание {{date}}.", + "account_basics_tier_admin_suffix_with_tier": "(с подпиской {{tier}})", + "account_basics_tier_payment_overdue": "У вас имеется задолженность по оплате. Пожалуйста, проверьте метод оплаты, иначе скоро вы утратите преимущества подписки.", + "account_basics_tier_canceled_subscription": "Ваша подписка была отменена. Учётная запись перейдет на бесплатное обслуживание {{date}}.", "account_basics_tier_manage_billing_button": "Управление оплатой", "account_usage_messages_title": "Опубликованные сообщения", "account_usage_emails_title": "Отправленные электронные сообщения", @@ -305,8 +305,8 @@ "account_usage_attachment_storage_title": "Хранение вложений", "account_usage_attachment_storage_description": "{{filesize}} за файл, удаляются спустя {{expiry}}", "account_usage_cannot_create_portal_session": "Невозможно открыть портал оплаты", - "account_delete_title": "Удалить учетную запись", - "account_delete_description": "Безвозвратно удалить Вашу учетную запись", + "account_delete_title": "Удаление учётной записи", + "account_delete_description": "Безвозвратное удаление этой учётной записи", "account_upgrade_dialog_button_redirect_signup": "Зарегистрироваться", "account_upgrade_dialog_button_pay_now": "Оплатить и подписаться", "account_upgrade_dialog_button_cancel_subscription": "Отменить подписку", @@ -319,8 +319,8 @@ "account_tokens_table_expires_header": "Истекает", "account_tokens_dialog_label": "Название, например Radarr notifications", "prefs_reservations_title": "Зарезервированные темы", - "prefs_reservations_description": "Здесь Вы можете резервировать темы для личного пользования. Резервирование дает Вам возможность управлять темой и настраивать правила доступа к ней для пользователей.", - "prefs_reservations_limit_reached": "Вы исчерпали Ваш лимит на количество зарезервированных тем.", + "prefs_reservations_description": "Здесь вы можете резервировать темы для личного пользования. Резервирование дает возможность управления темой и настройки правил доступа к ней для других пользователей.", + "prefs_reservations_limit_reached": "Лимит количества зарезервированных тем исчерпан.", "prefs_reservations_add_button": "Добавить тему", "prefs_reservations_edit_button": "Настройка доступа", "prefs_reservations_delete_button": "Сбросить правила доступа", @@ -339,7 +339,7 @@ "account_basics_password_dialog_new_password_label": "Новый пароль", "account_basics_password_dialog_confirm_password_label": "Подтвердите пароль", "account_basics_password_dialog_button_submit": "Сменить пароль", - "account_basics_tier_title": "Тип учетной записи", + "account_basics_tier_title": "Тип учётной записи", "error_boundary_unsupported_indexeddb_description": "Веб-приложение ntfy использует IndexedDB, который не поддерживается Вашим браузером в приватном режиме.

Хотя это и не лучший вариант, использовать веб-приложение ntfy в приватном режиме не имеет особого смысла, так как все данные храняться в локальном хранилище браузера. Вы можете узнать больше в этом отчете на GitHub или связавшись с нами через Discord или Matrix.", "account_basics_tier_interval_monthly": "ежемесячно", "account_basics_tier_interval_yearly": "ежегодно", @@ -356,11 +356,11 @@ "publish_dialog_call_reset": "Удалить вызов", "account_basics_phone_numbers_dialog_description": "Для того что бы использовать возможность уведомлений о вызовах, нужно добавить и проверить хотя бы один номер телефона. Проверить можно используя SMS или звонок.", "account_basics_phone_numbers_dialog_title": "Добавить номер телефона", - "account_basics_phone_numbers_dialog_number_placeholder": "например +1222333444", - "account_basics_phone_numbers_dialog_code_placeholder": "например 123456", + "account_basics_phone_numbers_dialog_number_placeholder": "например, +72223334444", + "account_basics_phone_numbers_dialog_code_placeholder": "например, 123456", "account_basics_phone_numbers_dialog_verify_button_sms": "Отправить SMS", "account_usage_calls_title": "Совершённые вызовы", - "account_usage_calls_none": "Невозможно совершать вызовы с этим аккаунтом", + "account_usage_calls_none": "Невозможно совершать вызовы с этой учётной записью", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Нет проверенных номеров", "account_basics_phone_numbers_copied_to_clipboard": "Номер телефона скопирован в буфер обмена", "account_upgrade_dialog_tier_features_no_calls": "Нет вызовов", @@ -371,8 +371,8 @@ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервированная тема", "account_basics_phone_numbers_no_phone_numbers_yet": "Телефонных номеров пока нет", "publish_dialog_chip_call_label": "Звонок", - "account_upgrade_dialog_tier_features_emails_one": "{{emails}} ежедневное письмо", - "account_upgrade_dialog_tier_features_messages_one": "{{messages}} ежедневное сообщения", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} эл. письмо в день", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} сообщение в день", "account_basics_phone_numbers_description": "Для уведомлений о телефонных звонках", "publish_dialog_call_label": "Звонок", "account_basics_phone_numbers_dialog_channel_call": "Позвонить", @@ -383,8 +383,8 @@ "account_basics_phone_numbers_dialog_channel_sms": "SMS", "action_bar_mute_notifications": "Заглушить уведомления", "action_bar_unmute_notifications": "Разрешить уведомления", - "alert_notification_permission_denied_title": "Уведомления заблокированы", - "alert_notification_permission_denied_description": "Пожалуйста, разрешите их в своём браузере", + "alert_notification_permission_denied_title": "Уведомления не разрешены", + "alert_notification_permission_denied_description": "Пожалуйста, разрешите отправку уведомлений браузере", "alert_notification_ios_install_required_title": "iOS требует установку", "alert_notification_ios_install_required_description": "Нажмите на значок \"Поделиться\" и \"Добавить на главный экран\", чтобы включить уведомления на iOS", "error_boundary_button_reload_ntfy": "Перезагрузить ntfy", @@ -401,7 +401,7 @@ "notifications_actions_failed_notification": "Неудачное действие", "publish_dialog_checkbox_markdown": "Форматировать как Markdown", "subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто", - "prefs_appearance_theme_system": "Системный (по умолчанию)", - "prefs_appearance_theme_dark": "Ночной режим", - "prefs_appearance_theme_light": "Дневной режим" + "prefs_appearance_theme_system": "Как в системе (по умолчанию)", + "prefs_appearance_theme_dark": "Тёмная", + "prefs_appearance_theme_light": "Светлая" } From fc93de9a28c51d872eb489a1336d85c68d3fdb2f Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Sat, 4 Jan 2025 21:15:23 +0000 Subject: [PATCH 077/378] Translated using Weblate (Ukrainian) Currently translated at 99.7% (404 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/uk/ --- web/public/static/langs/uk.json | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/uk.json b/web/public/static/langs/uk.json index b09822dd..c51dfcb3 100644 --- a/web/public/static/langs/uk.json +++ b/web/public/static/langs/uk.json @@ -88,7 +88,7 @@ "action_bar_unsubscribe": "Відписатися", "message_bar_publish": "Опублікувати повідомлення", "nav_button_all_notifications": "Усі сповіщення", - "alert_not_supported_description": "Ваш браузер не підтримує сповіщення.", + "alert_not_supported_description": "Ваш браузер не підтримує сповіщення", "notifications_list": "Список сповіщень", "notifications_mark_read": "Позначити як прочитане", "notifications_delete": "Видалити", @@ -381,5 +381,27 @@ "reservation_delete_dialog_action_delete_title": "Видалення кешованих повідомлень і вкладень", "reservation_delete_dialog_action_keep_title": "Збереження кешованих повідомлень і вкладень", "reservation_delete_dialog_action_keep_description": "Повідомлення і вкладення, які кешуються на сервері, стають загальнодоступними для людей, які знають назву теми.", - "reservation_delete_dialog_action_delete_description": "Кешовані повідомлення та вкладення будуть видалені назавжди. Ця дія не може бути скасована." + "reservation_delete_dialog_action_delete_description": "Кешовані повідомлення та вкладення будуть видалені назавжди. Ця дія не може бути скасована.", + "subscribe_dialog_subscribe_use_another_background_info": "Сповіщення з інших серверів не надходитимуть, якщо вебзастосунок не відкрито", + "publish_dialog_checkbox_markdown": "Форматувати як Markdown", + "alert_notification_ios_install_required_description": "Натисніть піктограму \"Поділитися\" та \"Додати на головний екран\", щоб увімкнути сповіщення на iOS", + "prefs_appearance_theme_dark": "Темний режим", + "web_push_unknown_notification_title": "Отримано невідоме сповіщення від сервера", + "action_bar_mute_notifications": "Вимкнути сповіщення", + "action_bar_unmute_notifications": "Увімкнути сповіщення", + "alert_notification_permission_denied_title": "Сповіщення заблоковано", + "alert_notification_permission_denied_description": "Будь ласка, увімкніть їх повторно у своєму браузері", + "notifications_actions_failed_notification": "Невдала дія", + "prefs_notifications_web_push_title": "Фонові сповіщення", + "prefs_notifications_web_push_enabled_description": "Сповіщення надходитимуть навіть якщо вебзастосунок не запущений (за допомоги Web Push)", + "prefs_notifications_web_push_disabled_description": "Сповіщення надходитимуть якщо вебзастосунок запущений (за допомоги WebSocket)", + "prefs_notifications_web_push_enabled": "Увімкнено для {{server}}", + "prefs_notifications_web_push_disabled": "Вимкнено", + "prefs_appearance_theme_title": "Тема", + "prefs_appearance_theme_system": "Система (за замовчуванням)", + "prefs_appearance_theme_light": "Світлий режим", + "error_boundary_button_reload_ntfy": "Перезавантажити ntfy", + "web_push_subscription_expiring_title": "Сповіщення буде призупинено", + "web_push_subscription_expiring_body": "Відкрийте ntfy, щоб продовжити отримувати сповіщення", + "web_push_unknown_notification_body": "Можливо вам потрібно оновити ntfy шляхом відкриття вебзастосунку" } From 92de1b5a88c7772374899db45f43c7d2fba69296 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Thu, 9 Jan 2025 18:08:01 +0000 Subject: [PATCH 078/378] Translated using Weblate (Persian) Currently translated at 13.8% (56 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/ --- web/public/static/langs/fa.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/fa.json b/web/public/static/langs/fa.json index 9ef390d2..188dba45 100644 --- a/web/public/static/langs/fa.json +++ b/web/public/static/langs/fa.json @@ -32,5 +32,6 @@ "action_bar_reservation_edit": "تغییر رزرو", "action_bar_reservation_delete": "حذف رزرو", "action_bar_mute_notifications": "ساکت کردن اعلان ها", - "action_bar_clear_notifications": "پاک کردن تمام اعلان ها" + "action_bar_clear_notifications": "پاک کردن تمام اعلان ها", + "action_bar_toggle_action_menu": "گشودن يا بستن فهرست کنش" } From 79852fec5904c1cb40ff4a1f34524ac64896d026 Mon Sep 17 00:00:00 2001 From: Faraz Sadri Alamdari Date: Wed, 8 Jan 2025 23:49:02 +0000 Subject: [PATCH 079/378] Translated using Weblate (Persian) Currently translated at 13.8% (56 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/ --- web/public/static/langs/fa.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/fa.json b/web/public/static/langs/fa.json index 188dba45..e538b378 100644 --- a/web/public/static/langs/fa.json +++ b/web/public/static/langs/fa.json @@ -33,5 +33,26 @@ "action_bar_reservation_delete": "حذف رزرو", "action_bar_mute_notifications": "ساکت کردن اعلان ها", "action_bar_clear_notifications": "پاک کردن تمام اعلان ها", - "action_bar_toggle_action_menu": "گشودن يا بستن فهرست کنش" + "action_bar_toggle_action_menu": "گشودن يا بستن فهرست کنش", + "action_bar_profile_title": "پروفایل", + "action_bar_profile_settings": "تنظیمات", + "action_bar_profile_logout": "خروج", + "action_bar_sign_in": "ورود", + "action_bar_sign_up": "ثبت نام", + "message_bar_type_message": "یک پیام بنویسید", + "message_bar_error_publishing": "خطا در انتظار اعلان", + "message_bar_publish": "انتشار پیام", + "nav_button_all_notifications": "همه اعلان‌ها", + "nav_button_account": "حساب کاربری", + "nav_button_settings": "تنظیمات", + "nav_button_documentation": "مستندات", + "nav_button_publish_message": "انتشار اعلان", + "nav_button_muted": "اعلان بی‌صدا شد", + "nav_button_connecting": "در حال اتصال", + "nav_upgrade_banner_label": "ارتقا با ntfy پیشرفته", + "alert_notification_permission_required_title": "اعلان‌ها غیرفعال هستند", + "alert_notification_permission_required_description": "به مرورگر خود اجازه دهید تا اعلان‌های دسکتاپ را نمایش دهد", + "alert_notification_permission_denied_title": "اعلان‌ها مسدود هستند", + "alert_notification_ios_install_required_title": "لازم به نصب نسخه iOS است", + "alert_notification_ios_install_required_description": "برای فعال کردن اعلان‌ها در iOS، روی نماد اشتراک‌گذاری و افزودن به صفحه اصلی کلیک کنید" } From 04df6f1390564dd1d0453b584d61d341af6f4a93 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Thu, 9 Jan 2025 18:08:11 +0000 Subject: [PATCH 080/378] Translated using Weblate (Persian) Currently translated at 13.8% (56 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/ --- web/public/static/langs/fa.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/fa.json b/web/public/static/langs/fa.json index e538b378..4d46c422 100644 --- a/web/public/static/langs/fa.json +++ b/web/public/static/langs/fa.json @@ -34,7 +34,7 @@ "action_bar_mute_notifications": "ساکت کردن اعلان ها", "action_bar_clear_notifications": "پاک کردن تمام اعلان ها", "action_bar_toggle_action_menu": "گشودن يا بستن فهرست کنش", - "action_bar_profile_title": "پروفایل", + "action_bar_profile_title": "نمايه", "action_bar_profile_settings": "تنظیمات", "action_bar_profile_logout": "خروج", "action_bar_sign_in": "ورود", From 161ce468fe3de5e8a69184e3624f602ccfa79665 Mon Sep 17 00:00:00 2001 From: Marius Pop Date: Sun, 12 Jan 2025 19:36:27 +0000 Subject: [PATCH 081/378] Translated using Weblate (Romanian) Currently translated at 46.6% (189 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/ --- web/public/static/langs/ro.json | 73 ++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index c58aa7b1..1d950b38 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -27,8 +27,8 @@ "alert_notification_permission_required_title": "Notificările sunt dezactivate", "alert_notification_permission_required_button": "Permite acum", "alert_not_supported_title": "Notificările nu sunt acceptate", - "alert_not_supported_description": "Notificările nu sunt acceptate în browser.", - "alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări.", + "alert_not_supported_description": "Notificările nu sunt acceptate în browserul tău", + "alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări", "notifications_list": "Lista de notificări", "notifications_list_item": "Notificare", "notifications_mark_read": "Marchează ca citit", @@ -102,9 +102,9 @@ "publish_dialog_emoji_picker_show": "Alege un emoji", "notifications_loading": "Încărcare notificări…", "publish_dialog_priority_low": "Prioritate joasă", - "signup_form_username": "Nume de utilizator", - "signup_form_button_submit": "Înscrie-te", - "common_copy_to_clipboard": "Copiază în clipboard", + "signup_form_username": "Utilizator", + "signup_form_button_submit": "Înregistrare", + "common_copy_to_clipboard": "Copiază", "signup_form_toggle_password_visibility": "Schimbă vizibilitatea parolei", "signup_title": "Crează un cont ntfy", "signup_already_have_account": "Deja ai un cont? Autentifică-te!", @@ -126,5 +126,66 @@ "action_bar_reservation_add": "Rezervă topicul", "action_bar_mute_notifications": "Oprește notificările", "action_bar_unmute_notifications": "Pornește notificările", - "nav_topics_title": "Subiecte abonate" + "nav_topics_title": "Subiecte abonate", + "publish_dialog_chip_attach_url_label": "Atașează fișier prin URL", + "publish_dialog_call_label": "Apel telefonic", + "publish_dialog_button_cancel_sending": "Anulează trimiterea", + "subscribe_dialog_subscribe_title": "Abonează-te la subiect", + "subscribe_dialog_login_password_label": "Parolă", + "subscribe_dialog_login_button_login": "Autentificare", + "subscribe_dialog_error_user_not_authorized": "Utilizatorul {{username}} nu este autorizat", + "account_basics_title": "Cont", + "account_basics_username_title": "Nume de utilizator", + "account_basics_username_description": "Hei, ești tu ❤", + "subscribe_dialog_error_topic_already_reserved": "Subiectul este deja rezervat", + "publish_dialog_attached_file_title": "Fișier atașat:", + "publish_dialog_attached_file_filename_placeholder": "Nume fișier atașat", + "publish_dialog_attached_file_remove": "Elimină fișierul atașat", + "emoji_picker_search_placeholder": "Caută emoji", + "nav_button_muted": "Notificări dezactivate", + "alert_notification_permission_denied_title": "Notificările sunt blocate", + "alert_notification_ios_install_required_description": "Apasă pe butonul Partajare și Adăugați la ecranul principal pentru a porni notificările pe iOS", + "alert_notification_ios_install_required_title": "Instalare iOS necesară", + "alert_notification_permission_denied_description": "Repornește-le în browserul tău", + "alert_not_supported_context_description": "Notificările sunt acceptate doar prin HTTPS. Aceasta este o limitare a API-ului de notificări.", + "notifications_actions_failed_notification": "Acțiune nereușită", + "publish_dialog_email_placeholder": "Adresă către care se va redirecționa notificarea, ex. phil@example.com", + "publish_dialog_email_reset": "Șterge redirecționare email", + "publish_dialog_call_item": "Apelează numărul de telefon {{number}}", + "publish_dialog_attach_label": "URL atașament", + "publish_dialog_attach_placeholder": "Atașează fișier prin URL, ex. https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "Șterge atașament URL", + "publish_dialog_filename_label": "Nume fișier", + "publish_dialog_filename_placeholder": "Nume fișier atașament", + "publish_dialog_delay_label": "Întârziere", + "publish_dialog_call_reset": "Șterge apel telefonic", + "publish_dialog_delay_placeholder": "Întârzie livrarea, ex. {{unixTimestamp}}, {{relativeTime}}, sau \"{{naturalLanguage}}\" (doar engleză)", + "publish_dialog_delay_reset": "Șterge livrare întârziată", + "publish_dialog_other_features": "Alte funcționalități:", + "publish_dialog_chip_click_label": "Accesează URL-ul", + "publish_dialog_chip_email_label": "Redirecționează către email", + "publish_dialog_chip_call_label": "Apel telefonic", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nu există numere de telefon verificate", + "publish_dialog_chip_attach_file_label": "Atașează fișier local", + "publish_dialog_chip_delay_label": "Întârziere livrare", + "publish_dialog_chip_topic_label": "Schimbă subiectul", + "publish_dialog_details_examples_description": "Pentru exemple și o descriere detaliată a tuturor funcțiilor de trimitere, vă rugăm să consultați documentația.", + "publish_dialog_button_cancel": "Anulează", + "publish_dialog_button_send": "Trimite", + "publish_dialog_checkbox_markdown": "Formatează ca Markdown", + "publish_dialog_checkbox_publish_another": "Publică altul", + "publish_dialog_drop_file_here": "Trage fișierul aici", + "emoji_picker_search_clear": "Șterge căutarea", + "subscribe_dialog_subscribe_description": "Subiectele nu pot fi protejate prin parolă, așa că alege un nume care să nu fie ușor de ghicit. Odată abonat, poți utiliza metodele PUT/POST pentru a trimite notificări.", + "subscribe_dialog_subscribe_topic_placeholder": "Nume subiect, de exemplu, phil_alerts", + "subscribe_dialog_subscribe_use_another_label": "Foloseșste alt server", + "subscribe_dialog_subscribe_use_another_background_info": "Notificările de la alte servere nu vor fi primite atunci când aplicația web nu este deschisă", + "subscribe_dialog_subscribe_base_url_label": "URL serviciu", + "subscribe_dialog_subscribe_button_generate_topic_name": "Generează nume", + "subscribe_dialog_subscribe_button_cancel": "Anulează", + "subscribe_dialog_subscribe_button_subscribe": "Abonează-te", + "subscribe_dialog_login_title": "Autentificare necesară", + "subscribe_dialog_login_description": "Acest subiect este protejat prin parolă. Vă rugăm să introduceți numele de utilizator și parola pentru a vă abona.", + "subscribe_dialog_login_username_label": "Nume de utilizator, de exemplu, phil", + "subscribe_dialog_error_user_anonymous": "anonim" } From e76e6274a39c6775a85d19a27eb499698b028dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=A4=E0=AE=AE=E0=AE=BF=E0=AE=B4=E0=AF=8D=E0=AE=A8?= =?UTF-8?q?=E0=AF=87=E0=AE=B0=E0=AE=AE=E0=AF=8D?= Date: Mon, 20 Jan 2025 16:28:13 +0100 Subject: [PATCH 082/378] Added translation using Weblate (Tamil) --- web/public/static/langs/ta.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/ta.json diff --git a/web/public/static/langs/ta.json b/web/public/static/langs/ta.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/ta.json @@ -0,0 +1 @@ +{} From dd45fd90b77cbb678b67dfbade5e6fa7ed8e8f92 Mon Sep 17 00:00:00 2001 From: AtomicDude Date: Mon, 20 Jan 2025 17:30:59 +0000 Subject: [PATCH 083/378] Translated using Weblate (Romanian) Currently translated at 56.7% (230 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/ --- web/public/static/langs/ro.json | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index 1d950b38..df000eca 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -187,5 +187,46 @@ "subscribe_dialog_login_title": "Autentificare necesară", "subscribe_dialog_login_description": "Acest subiect este protejat prin parolă. Vă rugăm să introduceți numele de utilizator și parola pentru a vă abona.", "subscribe_dialog_login_username_label": "Nume de utilizator, de exemplu, phil", - "subscribe_dialog_error_user_anonymous": "anonim" + "subscribe_dialog_error_user_anonymous": "anonim", + "account_basics_tier_interval_monthly": "lunar", + "account_basics_password_dialog_title": "Schimbă parola", + "account_basics_password_dialog_current_password_label": "Parola actuală", + "account_basics_phone_numbers_copied_to_clipboard": "Numărul de telefon a fost copiat", + "account_basics_username_admin_tooltip": "Sunteți administrator", + "account_basics_tier_paid_until": "Abonamentul este plătit până la {{date}}, și se va reînnoi automat", + "account_basics_tier_payment_overdue": "Plata dvs. este restantă. Actualizați metoda de plată sau contul dvs. va fi retrogradat în curând.", + "account_basics_tier_interval_yearly": "anual", + "account_basics_tier_upgrade_button": "Upgrade la Pro", + "account_basics_phone_numbers_title": "Numere de telefon", + "account_basics_password_description": "Schimbă parola contului", + "account_basics_password_dialog_confirm_password_label": "Confirmă parola", + "account_basics_password_dialog_button_submit": "Schimbă parola", + "account_basics_password_dialog_current_password_incorrect": "Parola este incorectă", + "account_basics_phone_numbers_dialog_description": "Pentru a folosi funcția de notificare prin apel, trebuie să adăugați și să verificați cel puțin un număr de telefon. Verificare poate fi făcută prin SMS sau apel vocal.", + "account_basics_phone_numbers_description": "Pentru notificări prin apel", + "account_basics_phone_numbers_dialog_verify_button_sms": "Trimite SMS", + "account_basics_phone_numbers_no_phone_numbers_yet": "Încă nu există numere de telefon", + "account_basics_phone_numbers_dialog_title": "Adaugă număr de telefon", + "account_basics_phone_numbers_dialog_number_label": "Număr de telefon", + "account_basics_phone_numbers_dialog_number_placeholder": "e.x. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_call": "Sună-mă", + "account_basics_phone_numbers_dialog_code_label": "Cod de verificare", + "account_basics_phone_numbers_dialog_code_placeholder": "e.x. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Confirmă codul", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Apel", + "account_usage_title": "Utilizare", + "account_usage_unlimited": "Nelimitat", + "account_usage_limits_reset_daily": "Limitele de utilizare sunt resetate zilnic la miezul nopții (UTC)", + "account_basics_tier_title": "Tip de cont", + "account_usage_of_limit": "din {{limit}}", + "account_basics_tier_admin": "Administrator", + "account_basics_tier_admin_suffix_with_tier": "(cu nivelul {{tier}})", + "account_basics_tier_admin_suffix_no_tier": "(niciun nivel)", + "account_basics_tier_basic": "De bază", + "account_basics_tier_change_button": "Schimbă", + "account_basics_password_dialog_new_password_label": "Parola nouă", + "account_basics_password_title": "Parolă", + "account_basics_tier_description": "Nivelul de putere al contului", + "account_basics_tier_free": "Gratuit" } From ac983cd9bc70b15bd2fffa9ad3b74b1fb2684434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=A4=E0=AE=AE=E0=AE=BF=E0=AE=B4=E0=AF=8D=E0=AE=A8?= =?UTF-8?q?=E0=AF=87=E0=AE=B0=E0=AE=AE=E0=AF=8D?= Date: Tue, 21 Jan 2025 02:00:28 +0000 Subject: [PATCH 084/378] Translated using Weblate (Tamil) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ta/ --- web/public/static/langs/ta.json | 408 +++++++++++++++++++++++++++++++- 1 file changed, 407 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ta.json b/web/public/static/langs/ta.json index 0967ef42..11635c91 100644 --- a/web/public/static/langs/ta.json +++ b/web/public/static/langs/ta.json @@ -1 +1,407 @@ -{} +{ + "action_bar_account": "கணக்கு", + "action_bar_change_display_name": "காட்சி பெயரை மாற்றவும்", + "action_bar_show_menu": "மெனுவைக் காட்டு", + "action_bar_logo_alt": "ntfy லோகோ", + "action_bar_settings": "அமைப்புகள்", + "action_bar_reservation_add": "இருப்பு தலைப்பு", + "message_bar_publish": "செய்தியை வெளியிடுங்கள்", + "nav_topics_title": "சந்தா தலைப்புகள்", + "nav_button_all_notifications": "அனைத்து அறிவிப்புகளும்", + "nav_button_account": "கணக்கு", + "nav_button_settings": "அமைப்புகள்", + "nav_button_documentation": "ஆவணப்படுத்துதல்", + "nav_button_publish_message": "அறிவிப்பை வெளியிடுங்கள்", + "alert_not_supported_description": "உங்கள் உலாவியில் அறிவிப்புகள் ஆதரிக்கப்படவில்லை", + "alert_not_supported_context_description": "அறிவிப்புகள் HTTP களில் மட்டுமே ஆதரிக்கப்படுகின்றன. இது அறிவிப்புகள் பநிஇ இன் வரம்பு.", + "notifications_list": "அறிவிப்புகள் பட்டியல்", + "notifications_delete": "நீக்கு", + "notifications_copied_to_clipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", + "notifications_list_item": "அறிவிப்பு", + "notifications_mark_read": "படித்தபடி குறி", + "notifications_tags": "குறிச்சொற்கள்", + "notifications_priority_x": "முன்னுரிமை {{priority}}", + "notifications_actions_not_supported": "வலை பயன்பாட்டில் நடவடிக்கை ஆதரிக்கப்படவில்லை", + "notifications_none_for_topic_title": "இந்த தலைப்புக்கு நீங்கள் இதுவரை எந்த அறிவிப்புகளையும் பெறவில்லை.", + "notifications_actions_http_request_title": "Http {{method}} {{url}} க்கு அனுப்பவும்", + "notifications_actions_failed_notification": "தோல்வியுற்ற செயல்", + "notifications_none_for_topic_description": "இந்த தலைப்புக்கு அறிவிப்புகளை அனுப்ப, தலைப்பு முகவரி க்கு வைக்கவும் அல்லது இடுகையிடவும்.", + "notifications_loading": "அறிவிப்புகளை ஏற்றுகிறது…", + "publish_dialog_title_topic": "{{topic}} க்கு வெளியிடுங்கள்", + "publish_dialog_title_no_topic": "அறிவிப்பை வெளியிடுங்கள்", + "publish_dialog_progress_uploading": "பதிவேற்றுதல்…", + "publish_dialog_message_published": "அறிவிப்பு வெளியிடப்பட்டது", + "publish_dialog_attachment_limits_file_and_quota_reached": "{{fileSizeLimit}} கோப்பு வரம்பு மற்றும் ஒதுக்கீடு, {{remainingBytes}} மீதமுள்ளது", + "publish_dialog_attachment_limits_file_reached": "{{fileSizeLimit}} கோப்பு வரம்பை மீறுகிறது", + "publish_dialog_attachment_limits_quota_reached": "ஒதுக்கீட்டை மீறுகிறது, {{remainingBytes}} மீதமுள்ளவை", + "publish_dialog_progress_uploading_detail": "பதிவேற்றுவது {{loaded}}/{{{total}} ({{percent}}%)…", + "publish_dialog_priority_min": "மணித்துளி. முன்னுரிமை", + "publish_dialog_emoji_picker_show": "ஈமோசியைத் தேர்ந்தெடுங்கள்", + "publish_dialog_priority_low": "குறைந்த முன்னுரிமை", + "publish_dialog_priority_default": "இயல்புநிலை முன்னுரிமை", + "publish_dialog_priority_high": "அதிக முன்னுரிமை", + "publish_dialog_priority_max": "அதிகபட்சம். முன்னுரிமை", + "publish_dialog_base_url_label": "பணி முகவரி", + "publish_dialog_base_url_placeholder": "பணி முகவரி, எ.கா. https://example.com", + "publish_dialog_topic_label": "தலைப்பு பெயர்", + "publish_dialog_topic_placeholder": "தலைப்பு பெயர், எ.கா. phil_alerts", + "publish_dialog_topic_reset": "தலைப்பை மீட்டமைக்கவும்", + "publish_dialog_title_label": "தலைப்பு", + "publish_dialog_title_placeholder": "அறிவிப்பு தலைப்பு, எ.கா. வட்டு விண்வெளி எச்சரிக்கை", + "publish_dialog_message_label": "செய்தி", + "publish_dialog_message_placeholder": "இங்கே ஒரு செய்தியைத் தட்டச்சு செய்க", + "publish_dialog_tags_label": "குறிச்சொற்கள்", + "publish_dialog_tags_placeholder": "குறிச்சொற்களின் கமாவால் பிரிக்கப்பட்ட பட்டியல், எ.கா. எச்சரிக்கை, SRV1-Backup", + "publish_dialog_priority_label": "முன்னுரிமை", + "publish_dialog_click_label": "முகவரி ஐக் சொடுக்கு செய்க", + "publish_dialog_click_placeholder": "அறிவிப்பைக் சொடுக்கு செய்யும் போது திறக்கப்படும் முகவரி", + "publish_dialog_click_reset": "சொடுக்கு முகவரி ஐ அகற்று", + "publish_dialog_email_label": "மின்னஞ்சல்", + "publish_dialog_email_placeholder": "அறிவிப்பை அனுப்ப முகவரி, எ.கா. phil@example.com", + "publish_dialog_email_reset": "மின்னஞ்சலை முன்னோக்கி அகற்றவும்", + "publish_dialog_call_label": "தொலைபேசி அழைப்பு", + "publish_dialog_call_item": "தொலைபேசி எண்ணை அழைக்கவும் {{number}}", + "publish_dialog_call_reset": "தொலைபேசி அழைப்பை அகற்று", + "publish_dialog_attach_label": "இணைப்பு முகவரி", + "publish_dialog_attach_placeholder": "முகவரி ஆல் கோப்பை இணைக்கவும், எ.கா. https://f-droid.org/f-droid.apk", + "publish_dialog_attach_reset": "இணைப்பு முகவரி ஐ அகற்று", + "publish_dialog_filename_label": "கோப்புப்பெயர்", + "publish_dialog_filename_placeholder": "இணைப்பு கோப்பு பெயர்", + "publish_dialog_delay_label": "சுணக்கம்", + "publish_dialog_delay_placeholder": "நேரந்தவறுகை வழங்கல், எ.கா. {{unixTimestamp}}, {{relativeTime}}, அல்லது \"{{naturalLanguage}}\" (ஆங்கிலம் மட்டும்)", + "publish_dialog_delay_reset": "தாமதமான விநியோகத்தை அகற்று", + "publish_dialog_other_features": "பிற அம்சங்கள்:", + "publish_dialog_chip_click_label": "முகவரி ஐக் சொடுக்கு செய்க", + "publish_dialog_chip_call_label": "தொலைபேசி அழைப்பு", + "publish_dialog_chip_email_label": "மின்னஞ்சலுக்கு அனுப்பவும்", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "சரிபார்க்கப்பட்ட தொலைபேசி எண்கள் இல்லை", + "publish_dialog_chip_attach_url_label": "முகவரி மூலம் கோப்பை இணைக்கவும்", + "publish_dialog_details_examples_description": "எடுத்துக்காட்டுகள் மற்றும் அனைத்து அனுப்பும் அம்சங்களின் விரிவான விளக்கத்திற்கு, தயவுசெய்து ஆவணங்கள் ஐப் பார்க்கவும்.", + "publish_dialog_chip_attach_file_label": "உள்ளக கோப்பை இணைக்கவும்", + "publish_dialog_chip_delay_label": "நேரந்தவறுகை வழங்கல்", + "publish_dialog_chip_topic_label": "தலைப்பை மாற்றவும்", + "subscribe_dialog_subscribe_button_generate_topic_name": "பெயரை உருவாக்குங்கள்", + "subscribe_dialog_subscribe_use_another_background_info": "வலை பயன்பாடு திறக்கப்படாதபோது பிற சேவையகங்களிலிருந்து அறிவிப்புகள் பெறப்படாது", + "subscribe_dialog_subscribe_button_cancel": "ரத்துசெய்", + "subscribe_dialog_subscribe_button_subscribe": "குழுசேர்", + "subscribe_dialog_login_title": "உள்நுழைவு தேவை", + "account_basics_password_dialog_confirm_password_label": "கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "account_basics_password_dialog_current_password_incorrect": "கடவுச்சொல் தவறானது", + "account_basics_password_dialog_button_submit": "கடவுச்சொல்லை மாற்றவும்", + "account_basics_phone_numbers_title": "தொலைபேசி எண்கள்", + "account_basics_phone_numbers_dialog_description": "அழைப்பு அறிவிப்பு அம்சத்தைப் பயன்படுத்த, நீங்கள் குறைந்தது ஒரு தொலைபேசி எண்ணையாவது சேர்த்து சரிபார்க்க வேண்டும். சரிபார்ப்பு எச்எம்எச் அல்லது தொலைபேசி அழைப்பு வழியாக செய்யப்படலாம்.", + "account_basics_phone_numbers_description": "தொலைபேசி அழைப்பு அறிவிப்புகளுக்கு", + "account_basics_phone_numbers_no_phone_numbers_yet": "தொலைபேசி எண்கள் இதுவரை இல்லை", + "account_basics_phone_numbers_copied_to_clipboard": "தொலைபேசி எண் இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", + "account_basics_phone_numbers_dialog_title": "தொலைபேசி எண்ணைச் சேர்க்கவும்", + "account_basics_phone_numbers_dialog_number_placeholder": "எ.கா. +122333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "எச்எம்எச் அனுப்பு", + "account_basics_phone_numbers_dialog_code_placeholder": "எ.கா. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "குறியீட்டை உறுதிப்படுத்தவும்", + "account_basics_phone_numbers_dialog_verify_button_call": "என்னை அழைக்கவும்", + "account_basics_phone_numbers_dialog_code_label": "சரிபார்ப்பு குறியீடு", + "account_basics_phone_numbers_dialog_channel_sms": "எச்.எம்.எச்", + "account_basics_phone_numbers_dialog_channel_call": "அழைப்பு", + "account_usage_title": "பயன்பாடு", + "account_usage_unlimited": "வரம்பற்றது", + "account_usage_of_limit": "{{limit}} of", + "account_usage_limits_reset_daily": "பயன்பாட்டு வரம்புகள் நள்ளிரவில் தினமும் மீட்டமைக்கப்படுகின்றன (UTC)", + "account_basics_tier_title": "கணக்கு வகை", + "account_basics_tier_description": "உங்கள் கணக்கின் ஆற்றல் நிலை", + "account_basics_tier_admin": "நிர்வாகி", + "account_basics_tier_admin_suffix_with_tier": "({{tier}} அடுக்கு)", + "account_basics_tier_admin_suffix_no_tier": "(அடுக்கு இல்லை)", + "account_basics_tier_basic": "அடிப்படை", + "account_basics_tier_free": "இலவசம்", + "account_basics_tier_interval_monthly": "மாதாந்திர", + "account_basics_tier_interval_yearly": "ஆண்டுதோறும்", + "account_basics_tier_upgrade_button": "சார்புக்கு மேம்படுத்தவும்", + "account_basics_tier_change_button": "மாற்றம்", + "account_basics_tier_paid_until": "சந்தா {{date}} வரை செலுத்தப்படுகிறது, மேலும் தானாக புதுப்பிக்கப்படும்", + "account_basics_tier_canceled_subscription": "உங்கள் சந்தா ரத்து செய்யப்பட்டது மற்றும் {{date} at இல் இலவச கணக்கிற்கு தரமிறக்கப்படும்.", + "account_basics_tier_manage_billing_button": "பட்டியலிடல் நிர்வகிக்கவும்", + "account_basics_tier_payment_overdue": "உங்கள் கட்டணம் தாமதமானது. தயவுசெய்து உங்கள் கட்டண முறையைப் புதுப்பிக்கவும், அல்லது உங்கள் கணக்கு விரைவில் தரமிறக்கப்படும்.", + "account_usage_messages_title": "வெளியிடப்பட்ட செய்திகள்", + "account_usage_emails_title": "மின்னஞ்சல்கள் அனுப்பப்பட்டன", + "account_usage_calls_title": "தொலைபேசி அழைப்புகள் செய்யப்பட்டன", + "account_usage_calls_none": "இந்த கணக்கில் தொலைபேசி அழைப்புகள் எதுவும் செய்ய முடியாது", + "account_usage_reservations_title": "ஒதுக்கப்பட்ட தலைப்புகள்", + "account_usage_reservations_none": "இந்த கணக்கிற்கு ஒதுக்கப்பட்ட தலைப்புகள் இல்லை", + "account_usage_attachment_storage_title": "இணைப்பு சேமிப்பு", + "account_usage_attachment_storage_description": "கோப்பு {{filesize}} க்குப் பிறகு நீக்கப்பட்ட ஒரு கோப்பிற்கு {{expiry}}}}", + "account_usage_basis_ip_description": "இந்த கணக்கிற்கான பயன்பாட்டு புள்ளிவிவரங்கள் மற்றும் வரம்புகள் உங்கள் ஐபி முகவரியை அடிப்படையாகக் கொண்டவை, எனவே அவை மற்ற பயனர்களுடன் பகிரப்படலாம். மேலே காட்டப்பட்டுள்ள வரம்புகள் தற்போதுள்ள விகித வரம்புகளின் அடிப்படையில் தோராயங்கள்.", + "account_usage_cannot_create_portal_session": "பட்டியலிடல் போர்ட்டலைத் திறக்க முடியவில்லை", + "account_delete_title": "கணக்கை நீக்கு", + "account_delete_description": "உங்கள் கணக்கை நிரந்தரமாக நீக்கவும்", + "account_upgrade_dialog_cancel_warning": "இது உங்கள் சந்தாவை ரத்துசெய்யும் , மேலும் உங்கள் கணக்கை {{date} at இல் தரமிறக்குகிறது. அந்த தேதியில், தலைப்பு முன்பதிவு மற்றும் சேவையகத்தில் தற்காலிகமாக சேமிக்கப்பட்ட செய்திகளும் நீக்கப்படும் .", + "account_upgrade_dialog_proration_info": " புரோரேசன் : கட்டணத் திட்டங்களுக்கு இடையில் மேம்படுத்தும்போது, விலை வேறுபாடு உடனடியாக கட்டணம் வசூலிக்கப்படும் . குறைந்த அடுக்குக்கு தரமிறக்கும்போது, எதிர்கால பட்டியலிடல் காலங்களுக்கு செலுத்த இருப்பு பயன்படுத்தப்படும்.", + "account_upgrade_dialog_reservations_warning_one": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கை விட குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தது ஒரு முன்பதிவை நீக்கு . <இணைப்பு> அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", + "account_upgrade_dialog_reservations_warning_other": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கை விட குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தபட்சம் {{count}} முன்பதிவு ஐ நீக்கவும். <இணைப்பு> அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} ஒதுக்கப்பட்ட தலைப்புகள்", + "account_upgrade_dialog_tier_features_no_reservations": "ஒதுக்கப்பட்ட தலைப்புகள் இல்லை", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} நாள்தோறும் செய்தி", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} நாள்தோறும் செய்திகள்", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} நாள்தோறும் மின்னஞ்சல்", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} நாள்தோறும் மின்னஞ்சல்கள்", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} நாள்தோறும் தொலைபேசி அழைப்புகள்", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} நாள்தோறும் தொலைபேசி அழைப்புகள்", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} மொத்த சேமிப்பு", + "account_upgrade_dialog_tier_price_per_month": "மாதம்", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}}}}}}. மாதந்தோறும் பாடு.", + "account_upgrade_dialog_tier_features_no_calls": "தொலைபேசி அழைப்புகள் இல்லை", + "account_upgrade_dialog_tier_features_attachment_file_size": "கோப்பு {filesize}}} ஒரு கோப்பிற்கு", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} ஆண்டுதோறும் கட்டணம் செலுத்தப்படுகிறது. {{save}} சேமி.", + "account_upgrade_dialog_tier_selected_label": "தேர்ந்தெடுக்கப்பட்டது", + "account_upgrade_dialog_tier_current_label": "மின்னோட்ட்ம், ஓட்டம்", + "account_upgrade_dialog_billing_contact_email": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து <இணைப்பு> எங்களை தொடர்பு கொள்ளவும் நேரடியாக.", + "account_upgrade_dialog_button_cancel": "ரத்துசெய்", + "account_upgrade_dialog_billing_contact_website": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து எங்கள் <இணைப்பு> வலைத்தளம் ஐப் பார்க்கவும்.", + "account_upgrade_dialog_button_redirect_signup": "இப்போது பதிவுபெறுக", + "account_upgrade_dialog_button_pay_now": "இப்போது பணம் செலுத்தி குழுசேரவும்", + "account_upgrade_dialog_button_cancel_subscription": "சந்தாவை ரத்துசெய்", + "account_tokens_title": "டோக்கன்களை அணுகவும்", + "account_tokens_description": "NTFY பநிஇ வழியாக வெளியிடும் மற்றும் சந்தா செலுத்தும் போது அணுகல் டோக்கன்களைப் பயன்படுத்தவும், எனவே உங்கள் கணக்கு நற்சான்றிதழ்களை அனுப்ப வேண்டியதில்லை. மேலும் அறிய <இணைப்பு> ஆவணங்கள் ஐப் பாருங்கள்.", + "account_upgrade_dialog_button_update_subscription": "சந்தாவைப் புதுப்பிக்கவும்", + "account_tokens_table_token_header": "கிள்ளாக்கு", + "account_tokens_table_label_header": "சிட்டை", + "account_tokens_table_last_access_header": "கடைசி அணுகல்", + "account_tokens_table_expires_header": "காலாவதியாகிறது", + "account_tokens_table_never_expires": "ஒருபோதும் காலாவதியாகாது", + "account_tokens_table_cannot_delete_or_edit": "தற்போதைய அமர்வு டோக்கனைத் திருத்தவோ நீக்கவோ முடியாது", + "account_tokens_table_current_session": "தற்போதைய உலாவி அமர்வு", + "account_tokens_table_copied_to_clipboard": "அணுகல் கிள்ளாக்கு நகலெடுக்கப்பட்டது", + "account_tokens_table_create_token_button": "அணுகல் கிள்ளாக்கை உருவாக்கவும்", + "account_tokens_dialog_title_create": "அணுகல் கிள்ளாக்கை உருவாக்கவும்", + "account_tokens_table_last_origin_tooltip": "ஐபி முகவரி {{ip} இருந்து இலிருந்து, தேடலைக் சொடுக்கு செய்க", + "account_tokens_dialog_title_edit": "அணுகல் டோக்கனைத் திருத்தவும்", + "account_tokens_dialog_title_delete": "அணுகல் கிள்ளாக்கை நீக்கு", + "account_tokens_dialog_label": "சிட்டை, எ.கா. ராடார் அறிவிப்புகள்", + "account_tokens_dialog_button_create": "கிள்ளாக்கை உருவாக்கவும்", + "account_tokens_dialog_button_update": "கிள்ளாக்கைப் புதுப்பிக்கவும்", + "account_tokens_dialog_button_cancel": "ரத்துசெய்", + "account_tokens_dialog_expires_label": "அணுகல் கிள்ளாக்கு காலாவதியாகிறது", + "account_tokens_dialog_expires_unchanged": "காலாவதி தேதி மாறாமல் விடுங்கள்", + "account_tokens_dialog_expires_x_hours": "கிள்ளாக்கு {{hours}} மணிநேரங்களில் காலாவதியாகிறது", + "account_tokens_dialog_expires_x_days": "கிள்ளாக்கு {{days}} நாட்களில் காலாவதியாகிறது", + "account_tokens_dialog_expires_never": "கிள்ளாக்கு ஒருபோதும் காலாவதியாகாது", + "account_tokens_delete_dialog_title": "அணுகல் கிள்ளாக்கை நீக்கு", + "account_tokens_delete_dialog_description": "அணுகல் கிள்ளாக்கை நீக்குவதற்கு முன், பயன்பாடுகள் அல்லது ச்கிரிப்ட்கள் எதுவும் தீவிரமாகப் பயன்படுத்தவில்லை என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள். இந்த செயலை செயல்தவிர்க்க முடியாது .", + "account_tokens_delete_dialog_submit_button": "கிள்ளாக்கை நிரந்தரமாக நீக்கு", + "prefs_notifications_title": "அறிவிப்புகள்", + "prefs_notifications_sound_title": "அறிவிப்பு ஒலி", + "prefs_notifications_sound_description_none": "அறிவிப்புகள் வரும்போது எந்த ஒலியையும் இயக்காது", + "prefs_notifications_sound_description_some": "அறிவிப்புகள் வரும்போது {{sound}} ஒலியை இயக்குகின்றன", + "prefs_notifications_sound_no_sound": "ஒலி இல்லை", + "prefs_notifications_sound_play": "தேர்ந்தெடுக்கப்பட்ட ஒலி விளையாடுங்கள்", + "prefs_notifications_min_priority_title": "குறைந்தபட்ச முன்னுரிமை", + "prefs_notifications_min_priority_description_any": "முன்னுரிமையைப் பொருட்படுத்தாமல் அனைத்து அறிவிப்புகளையும் காட்டுகிறது", + "prefs_notifications_min_priority_description_x_or_higher": "முன்னுரிமை {{number}} ({{name}}) அல்லது அதற்கு மேல் இருந்தால் அறிவிப்புகளைக் காட்டு", + "prefs_notifications_min_priority_description_max": "முன்னுரிமை 5 (அதிகபட்சம்) என்றால் அறிவிப்புகளைக் காட்டு", + "prefs_notifications_min_priority_any": "எந்த முன்னுரிமையும்", + "prefs_notifications_min_priority_low_and_higher": "குறைந்த முன்னுரிமை மற்றும் அதிக", + "prefs_notifications_min_priority_default_and_higher": "இயல்புநிலை முன்னுரிமை மற்றும் அதிக", + "prefs_notifications_min_priority_high_and_higher": "அதிக முன்னுரிமை மற்றும் அதிக", + "prefs_notifications_min_priority_max_only": "அதிகபட்ச முன்னுரிமை மட்டுமே", + "prefs_notifications_delete_after_title": "அறிவிப்புகளை நீக்கு", + "prefs_notifications_delete_after_never": "ஒருபோதும்", + "prefs_notifications_delete_after_three_hours": "மூன்று மணி நேரம் கழித்து", + "prefs_notifications_delete_after_one_day": "ஒரு நாள் கழித்து", + "prefs_notifications_delete_after_one_week": "ஒரு வாரம் கழித்து", + "prefs_notifications_delete_after_one_month": "ஒரு மாதத்திற்குப் பிறகு", + "prefs_notifications_delete_after_never_description": "அறிவிப்புகள் ஒருபோதும் தானாக நீக்கப்படவில்லை", + "prefs_notifications_delete_after_three_hours_description": "அறிவிப்புகள் மூன்று மணி நேரத்திற்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_delete_after_one_day_description": "அறிவிப்புகள் ஒரு நாளுக்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_delete_after_one_week_description": "அறிவிப்புகள் ஒரு வாரத்திற்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_delete_after_one_month_description": "அறிவிப்புகள் ஒரு மாதத்திற்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_web_push_title": "பின்னணி அறிவிப்புகள்", + "prefs_notifications_web_push_enabled_description": "வலை பயன்பாடு இயங்காதபோது கூட அறிவிப்புகள் பெறப்படுகின்றன (வலை புச் வழியாக)", + "prefs_notifications_web_push_disabled_description": "வலை பயன்பாடு இயங்கும்போது அறிவிப்பு பெறப்படுகிறது (வெப்சாக்கெட் வழியாக)", + "prefs_notifications_web_push_enabled": "{{server} க்கு க்கு இயக்கப்பட்டது", + "prefs_notifications_web_push_disabled": "முடக்கப்பட்டது", + "prefs_users_title": "பயனர்களை நிர்வகிக்கவும்", + "prefs_users_description": "உங்கள் பாதுகாக்கப்பட்ட தலைப்புகளுக்கு பயனர்களை இங்கே சேர்க்கவும்/அகற்றவும். பயனர்பெயர் மற்றும் கடவுச்சொல் உலாவியின் உள்ளக சேமிப்பகத்தில் சேமிக்கப்பட்டுள்ளன என்பதை நினைவில் கொள்க.", + "prefs_users_description_no_sync": "பயனர்கள் மற்றும் கடவுச்சொற்கள் உங்கள் கணக்கில் ஒத்திசைக்கப்படவில்லை.", + "prefs_users_table": "பயனர்கள் அட்டவணை", + "prefs_users_edit_button": "பயனரைத் திருத்து", + "prefs_users_delete_button": "பயனரை நீக்கு", + "prefs_users_table_cannot_delete_or_edit": "உள்நுழைந்த பயனரை நீக்கவோ திருத்தவோ முடியாது", + "prefs_users_table_user_header": "பயனர்", + "prefs_users_table_base_url_header": "பணி முகவரி", + "prefs_users_dialog_title_add": "பயனரைச் சேர்க்கவும்", + "prefs_users_dialog_title_edit": "பயனரைத் திருத்து", + "prefs_users_dialog_base_url_label": "பணி முகவரி, எ.கா. https://ntfy.sh", + "prefs_users_dialog_username_label": "பயனர்பெயர், எ.கா. பில்", + "prefs_users_dialog_password_label": "கடவுச்சொல்", + "prefs_appearance_title": "தோற்றம்", + "prefs_appearance_language_title": "மொழி", + "prefs_appearance_theme_title": "கருப்பொருள்", + "prefs_appearance_theme_system": "கணினி (இயல்புநிலை)", + "prefs_appearance_theme_dark": "இருண்ட முறை", + "prefs_appearance_theme_light": "ஒளி பயன்முறை", + "prefs_reservations_title": "ஒதுக்கப்பட்ட தலைப்புகள்", + "prefs_reservations_description": "தனிப்பட்ட பயன்பாட்டிற்காக தலைப்பு பெயர்களை இங்கே முன்பதிவு செய்யலாம். ஒரு தலைப்பை முன்பதிவு செய்வது தலைப்பின் மீது உங்களுக்கு உரிமையை அளிக்கிறது, மேலும் தலைப்பில் பிற பயனர்களுக்கான அணுகல் அனுமதிகளை வரையறுக்க உங்களை அனுமதிக்கிறது.", + "prefs_reservations_limit_reached": "உங்கள் ஒதுக்கப்பட்ட தலைப்புகளின் வரம்பை நீங்கள் அடைந்தீர்கள்.", + "prefs_reservations_add_button": "ஒதுக்கப்பட்ட தலைப்பைச் சேர்க்கவும்", + "prefs_reservations_edit_button": "தலைப்பு அணுகலைத் திருத்தவும்", + "prefs_reservations_delete_button": "தலைப்பு அணுகலை மீட்டமைக்கவும்", + "prefs_reservations_table": "ஒதுக்கப்பட்ட தலைப்புகள் அட்டவணை", + "prefs_reservations_table_topic_header": "தலைப்பு", + "prefs_reservations_table_access_header": "அணுகல்", + "prefs_reservations_table_everyone_deny_all": "நான் மட்டுமே வெளியிட்டு குழுசேர முடியும்", + "prefs_reservations_table_everyone_read_only": "நான் வெளியிட்டு குழுசேரலாம், அனைவரும் குழுசேரலாம்", + "prefs_reservations_table_everyone_write_only": "நான் வெளியிட்டு குழுசேரலாம், எல்லோரும் வெளியிடலாம்", + "prefs_reservations_table_everyone_read_write": "எல்லோரும் வெளியிட்டு குழுசேரலாம்", + "prefs_reservations_table_not_subscribed": "குழுசேரவில்லை", + "prefs_reservations_table_click_to_subscribe": "குழுசேர சொடுக்கு செய்க", + "prefs_reservations_dialog_title_add": "இருப்பு தலைப்பு", + "prefs_reservations_dialog_title_edit": "ஒதுக்கப்பட்ட தலைப்பைத் திருத்து", + "prefs_reservations_dialog_title_delete": "தலைப்பு முன்பதிவை நீக்கு", + "prefs_reservations_dialog_description": "ஒரு தலைப்பை முன்பதிவு செய்வது தலைப்பின் மீது உங்களுக்கு உரிமையை அளிக்கிறது, மேலும் தலைப்பில் பிற பயனர்களுக்கான அணுகல் அனுமதிகளை வரையறுக்க உங்களை அனுமதிக்கிறது.", + "prefs_reservations_dialog_topic_label": "தலைப்பு", + "prefs_reservations_dialog_access_label": "அணுகல்", + "reservation_delete_dialog_description": "முன்பதிவை அகற்றுவது தலைப்பின் மீது உரிமையை அளிக்கிறது, மேலும் மற்றவர்கள் அதை முன்பதிவு செய்ய அனுமதிக்கிறது. ஏற்கனவே உள்ள செய்திகளையும் இணைப்புகளையும் வைத்திருக்கலாம் அல்லது நீக்கலாம்.", + "reservation_delete_dialog_action_keep_title": "தற்காலிக சேமிப்பு செய்திகள் மற்றும் இணைப்புகளை வைத்திருங்கள்", + "reservation_delete_dialog_action_keep_description": "சேவையகத்தில் தற்காலிக சேமிப்பில் உள்ள செய்திகள் மற்றும் இணைப்புகள் தலைப்புப் பெயரைப் பற்றிய அறிவுள்ளவர்களுக்கு பகிரங்கமாகத் தெரியும்.", + "reservation_delete_dialog_action_delete_title": "தற்காலிக சேமிப்பு செய்திகள் மற்றும் இணைப்புகளை நீக்கவும்", + "reservation_delete_dialog_submit_button": "முன்பதிவை நீக்கு", + "reservation_delete_dialog_action_delete_description": "தற்காலிக சேமிக்கப்பட்ட செய்திகள் மற்றும் இணைப்புகள் நிரந்தரமாக நீக்கப்படும். இந்த செயலை செயல்தவிர்க்க முடியாது.", + "priority_min": "மணித்துளி", + "priority_low": "குறைந்த", + "priority_high": "உயர்ந்த", + "priority_max": "அதிகபட்சம்", + "priority_default": "இயல்புநிலை", + "error_boundary_title": "ஓ, NTFY செயலிழந்தது", + "error_boundary_description": "இது வெளிப்படையாக நடக்கக்கூடாது. இதைப் பற்றி மிகவும் வருந்துகிறேன். .", + "error_boundary_button_copy_stack_trace": "அடுக்கு சுவடு நகலெடுக்கவும்", + "error_boundary_button_reload_ntfy": "Ntfy ஐ மீண்டும் ஏற்றவும்", + "error_boundary_stack_trace": "ச்டாக் சுவடு", + "error_boundary_gathering_info": "மேலும் தகவலை சேகரிக்கவும்…", + "error_boundary_unsupported_indexeddb_title": "தனியார் உலாவல் ஆதரிக்கப்படவில்லை", + "common_cancel": "ரத்துசெய்", + "common_save": "சேமி", + "common_add": "கூட்டு", + "common_back": "பின்", + "common_copy_to_clipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கவும்", + "signup_title": "ஒரு NTFY கணக்கை உருவாக்கவும்", + "signup_form_username": "பயனர்பெயர்", + "signup_form_password": "கடவுச்சொல்", + "signup_form_confirm_password": "கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "signup_form_button_submit": "பதிவு செய்க", + "signup_form_toggle_password_visibility": "கடவுச்சொல் தெரிவுநிலையை மாற்றவும்", + "signup_already_have_account": "ஏற்கனவே ஒரு கணக்கு இருக்கிறதா? உள்நுழைக!", + "signup_disabled": "கையொப்பம் முடக்கப்பட்டுள்ளது", + "signup_error_username_taken": "பயனர்பெயர் {{username}} ஏற்கனவே எடுக்கப்பட்டுள்ளது", + "signup_error_creation_limit_reached": "கணக்கு உருவாக்கும் வரம்பு எட்டப்பட்டது", + "login_title": "உங்கள் NTFY கணக்கில் உள்நுழைக", + "login_form_button_submit": "விடுபதிகை", + "login_link_signup": "பதிவு செய்க", + "login_disabled": "உள்நுழைவு முடக்கப்பட்டுள்ளது", + "action_bar_reservation_edit": "முன்பதிவை மாற்றவும்", + "action_bar_reservation_delete": "முன்பதிவை அகற்று", + "action_bar_reservation_limit_reached": "வரம்பு எட்டப்பட்டது", + "action_bar_send_test_notification": "சோதனை அறிவிப்பை அனுப்பவும்", + "action_bar_clear_notifications": "எல்லா அறிவிப்புகளையும் அழிக்கவும்", + "action_bar_mute_notifications": "முடக்கு அறிவிப்புகள்", + "action_bar_unmute_notifications": "ஊடுருவல் அறிவிப்புகள்", + "action_bar_unsubscribe": "குழுவிலகவும்", + "action_bar_toggle_mute": "முடக்கு/அசைவது அறிவிப்புகள்", + "action_bar_toggle_action_menu": "செயல் மெனுவைத் திறக்க/மூடு", + "action_bar_profile_title": "சுயவிவரம்", + "action_bar_profile_settings": "அமைப்புகள்", + "action_bar_profile_logout": "வெளியேற்றம்", + "action_bar_sign_in": "விடுபதிகை", + "action_bar_sign_up": "பதிவு செய்க", + "message_bar_type_message": "இங்கே ஒரு செய்தியைத் தட்டச்சு செய்க", + "message_bar_error_publishing": "பிழை வெளியீட்டு அறிவிப்பு", + "message_bar_show_dialog": "வெளியீட்டு உரையாடலைக் காட்டு", + "nav_button_subscribe": "தலைப்புக்கு குழுசேரவும்", + "nav_button_muted": "அறிவிப்புகள் முடக்கப்பட்டன", + "nav_button_connecting": "இணைத்தல்", + "nav_upgrade_banner_label": "Ntfy Pro க்கு மேம்படுத்தவும்", + "nav_upgrade_banner_description": "தலைப்புகள், கூடுதல் செய்திகள் மற்றும் மின்னஞ்சல்கள் மற்றும் பெரிய இணைப்புகளை முன்பதிவு செய்யுங்கள்", + "alert_notification_permission_required_title": "அறிவிப்புகள் முடக்கப்பட்டுள்ளன", + "alert_notification_permission_required_description": "டெச்க்டாப் அறிவிப்புகளைக் காண்பிக்க உங்கள் உலாவி இசைவு வழங்கவும்", + "alert_notification_permission_required_button": "இப்போது வழங்கவும்", + "alert_notification_permission_denied_title": "அறிவிப்புகள் தடுக்கப்பட்டுள்ளன", + "alert_notification_permission_denied_description": "தயவுசெய்து அவற்றை உங்கள் உலாவியில் மீண்டும் இயக்கவும்", + "alert_notification_ios_install_required_title": "ஐஇமு நிறுவல் தேவை", + "alert_notification_ios_install_required_description": "ஐஇமு இல் அறிவிப்புகளை இயக்க பகிர்வு ஐகானைக் சொடுக்கு செய்து முகப்புத் திரையில் சேர்க்கவும்", + "alert_not_supported_title": "அறிவிப்புகள் ஆதரிக்கப்படவில்லை", + "notifications_new_indicator": "புதிய அறிவிப்பு", + "notifications_attachment_image": "இணைப்பு படம்", + "notifications_attachment_copy_url_title": "இணைப்பு முகவரி ஐ இடைநிலைப்பலகைக்கு நகலெடுக்கவும்", + "notifications_attachment_copy_url_button": "முகவரி ஐ நகலெடுக்கவும்", + "notifications_attachment_open_title": "{{url}} க்குச் செல்லவும்", + "notifications_attachment_open_button": "திறந்த இணைப்பு", + "notifications_attachment_link_expires": "இணைப்பு காலாவதியாகிறது {{date}}", + "notifications_attachment_link_expired": "இணைப்பு காலாவதியான பதிவிறக்க", + "notifications_attachment_file_image": "பட கோப்பு", + "notifications_attachment_file_video": "வீடியோ கோப்பு", + "notifications_attachment_file_audio": "ஆடியோ கோப்பு", + "notifications_attachment_file_app": "ஆண்ட்ராய்டு பயன்பாட்டு கோப்பு", + "notifications_attachment_file_document": "பிற ஆவணம்", + "notifications_click_copy_url_title": "இடைநிலைப்பலகைக்கு இணைப்பு முகவரி ஐ நகலெடுக்கவும்", + "notifications_click_copy_url_button": "இணைப்பை நகலெடுக்கவும்", + "notifications_click_open_button": "இணைப்பை திற", + "notifications_actions_open_url_title": "{{url}} க்குச் செல்லவும்", + "notifications_none_for_any_title": "உங்களுக்கு எந்த அறிவிப்புகளும் கிடைக்கவில்லை.", + "notifications_none_for_any_description": "ஒரு தலைப்புக்கு அறிவிப்புகளை அனுப்ப, தலைப்பு முகவரி க்கு வைக்கவும் அல்லது இடுகையிடவும். உங்கள் தலைப்புகளில் ஒன்றைப் பயன்படுத்தி இங்கே ஒரு எடுத்துக்காட்டு.", + "notifications_no_subscriptions_title": "உங்களிடம் இன்னும் சந்தாக்கள் இல்லை என்று தெரிகிறது.", + "notifications_no_subscriptions_description": "ஒரு தலைப்பை உருவாக்க அல்லது குழுசேர \"{{linktext}}\" இணைப்பைக் சொடுக்கு செய்க. அதன்பிறகு, நீங்கள் புட் அல்லது இடுகை வழியாக செய்திகளை அனுப்பலாம், மேலும் நீங்கள் இங்கே அறிவிப்புகளைப் பெறுவீர்கள்.", + "notifications_example": "எடுத்துக்காட்டு", + "notifications_more_details": "மேலும் தகவலுக்கு, வலைத்தளம் அல்லது ஆவணங்கள் ஐப் பாருங்கள்.", + "display_name_dialog_title": "காட்சி பெயரை மாற்றவும்", + "display_name_dialog_description": "சந்தா பட்டியலில் காட்டப்படும் தலைப்புக்கு மாற்று பெயரை அமைக்கவும். சிக்கலான பெயர்களைக் கொண்ட தலைப்புகளை மிக எளிதாக அடையாளம் காண இது உதவுகிறது.", + "display_name_dialog_placeholder": "காட்சி பெயர்", + "reserve_dialog_checkbox_label": "தலைப்பை முன்பதிவு செய்து அணுகலை உள்ளமைக்கவும்", + "publish_dialog_button_cancel_sending": "அனுப்புவதை ரத்துசெய்", + "publish_dialog_button_cancel": "ரத்துசெய்", + "publish_dialog_button_send": "அனுப்பு", + "publish_dialog_checkbox_markdown": "மார்க் பேரூர் என வடிவம்", + "publish_dialog_checkbox_publish_another": "மற்றொன்றை வெளியிடுங்கள்", + "publish_dialog_attached_file_title": "இணைக்கப்பட்ட கோப்பு:", + "publish_dialog_attached_file_filename_placeholder": "இணைப்பு கோப்பு பெயர்", + "publish_dialog_attached_file_remove": "இணைக்கப்பட்ட கோப்பை அகற்று", + "publish_dialog_drop_file_here": "கோப்பை இங்கே விடுங்கள்", + "emoji_picker_search_placeholder": "ஈமோசியைத் தேடுங்கள்", + "emoji_picker_search_clear": "தேடலை அழி", + "subscribe_dialog_subscribe_title": "தலைப்புக்கு குழுசேரவும்", + "subscribe_dialog_subscribe_description": "தலைப்புகள் கடவுச்சொல் பாதுகாக்கப்பட்டதாக இருக்காது, எனவே யூகிக்க எளிதான பெயரைத் தேர்வுசெய்க. சந்தா செலுத்தியதும், நீங்கள் அறிவிப்புகளை வைக்கலாம்/இடுகையிடலாம்.", + "subscribe_dialog_subscribe_topic_placeholder": "தலைப்பு பெயர், எ.கா. phil_alerts", + "subscribe_dialog_subscribe_use_another_label": "மற்றொரு சேவையகத்தைப் பயன்படுத்தவும்", + "subscribe_dialog_subscribe_base_url_label": "பணி முகவரி", + "subscribe_dialog_login_description": "இந்த தலைப்பு கடவுச்சொல் பாதுகாக்கப்படுகிறது. குழுசேர பயனர்பெயர் மற்றும் கடவுச்சொல்லை உள்ளிடவும்.", + "subscribe_dialog_login_username_label": "பயனர்பெயர், எ.கா. பில்", + "subscribe_dialog_login_password_label": "கடவுச்சொல்", + "subscribe_dialog_login_button_login": "புகுபதிவு", + "subscribe_dialog_error_user_not_authorized": "பயனர் {{username}} அங்கீகரிக்கப்படவில்லை", + "subscribe_dialog_error_topic_already_reserved": "தலைப்பு ஏற்கனவே ஒதுக்கப்பட்டுள்ளது", + "subscribe_dialog_error_user_anonymous": "அநாமதேய", + "account_basics_title": "கணக்கு", + "account_basics_username_title": "பயனர்பெயர்", + "account_basics_username_description": "ஏய், அது நீங்கள் தான்", + "account_basics_username_admin_tooltip": "நீங்கள் நிர்வாகி", + "account_basics_password_title": "கடவுச்சொல்", + "account_basics_password_description": "உங்கள் கணக்கு கடவுச்சொல்லை மாற்றவும்", + "account_basics_password_dialog_title": "கடவுச்சொல்லை மாற்றவும்", + "account_basics_password_dialog_current_password_label": "தற்போதைய கடவுச்சொல்", + "account_basics_password_dialog_new_password_label": "புதிய கடவுச்சொல்", + "account_basics_phone_numbers_dialog_number_label": "தொலைபேசி எண்", + "account_delete_dialog_description": "இது சேவையகத்தில் சேமிக்கப்பட்டுள்ள அனைத்து தரவுகளும் உட்பட உங்கள் கணக்கை நிரந்தரமாக நீக்கும். நீக்கப்பட்ட பிறகு, உங்கள் பயனர்பெயர் 7 நாட்களுக்கு கிடைக்காது. நீங்கள் உண்மையிலேயே தொடர விரும்பினால், கீழே உள்ள பெட்டியில் உங்கள் கடவுச்சொல்லை உறுதிப்படுத்தவும்.", + "account_delete_dialog_label": "கடவுச்சொல்", + "account_delete_dialog_button_cancel": "ரத்துசெய்", + "account_delete_dialog_button_submit": "கணக்கை நிரந்தரமாக நீக்கு", + "account_delete_dialog_billing_warning": "உங்கள் கணக்கை நீக்குவது உடனடியாக உங்கள் பட்டியலிடல் சந்தாவை ரத்து செய்கிறது. உங்களுக்கு இனி பட்டியலிடல் டாச்போர்டுக்கு அணுகல் இருக்காது.", + "account_upgrade_dialog_title": "கணக்கு அடுக்கை மாற்றவும்", + "account_upgrade_dialog_interval_monthly": "மாதாந்திர", + "account_upgrade_dialog_interval_yearly": "ஆண்டுதோறும்", + "account_upgrade_dialog_interval_yearly_discount_save": "{{discount}}% சேமிக்கவும்", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "{{discount}}% வரை சேமிக்கவும்", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} முன்பதிவு செய்யப்பட்ட தலைப்பு", + "prefs_users_add_button": "பயனரைச் சேர்க்கவும்", + "error_boundary_unsupported_indexeddb_description": "NTFY வலை பயன்பாட்டிற்கு செயல்பட குறியீட்டு தேவை, மற்றும் உங்கள் உலாவி தனிப்பட்ட உலாவல் பயன்முறையில் IndexEDDB ஐ ஆதரிக்காது. எப்படியிருந்தாலும் தனிப்பட்ட உலாவல் பயன்முறையில் பயன்பாடு, ஏனென்றால் அனைத்தும் உலாவி சேமிப்பகத்தில் சேமிக்கப்படுகின்றன. இந்த அறிவிலிமையம் இதழில் இல் பற்றி நீங்கள் மேலும் படிக்கலாம் அல்லது டிச்கார்ட் அல்லது மேட்ரிக்ச் இல் எங்களுடன் பேசலாம்.", + "web_push_subscription_expiring_title": "அறிவிப்புகள் இடைநிறுத்தப்படும்", + "web_push_subscription_expiring_body": "தொடர்ந்து அறிவிப்புகளைப் பெற NTFY ஐத் திறக்கவும்", + "web_push_unknown_notification_title": "சேவையகத்திலிருந்து அறியப்படாத அறிவிப்பு பெறப்பட்டது", + "web_push_unknown_notification_body": "வலை பயன்பாட்டைத் திறப்பதன் மூலம் நீங்கள் NTFY ஐ புதுப்பிக்க வேண்டியிருக்கலாம்" +} From f31d777b69bb103dad147f65c261af5e425d8b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 6 Feb 2025 17:48:07 +0000 Subject: [PATCH 085/378] Translated using Weblate (Estonian) Currently translated at 10.6% (43 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index 65bfa6bc..11af0335 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -22,5 +22,24 @@ "common_add": "Lisa", "signup_form_button_submit": "Liitu", "signup_form_toggle_password_visibility": "Vaheta salasõna nähtavust", - "action_bar_account": "Kasutajakonto" + "action_bar_account": "Kasutajakonto", + "action_bar_sign_in": "Logi sisse", + "nav_button_documentation": "Dokumentatsioon", + "action_bar_profile_title": "Profiil", + "action_bar_profile_settings": "Seadistused", + "action_bar_sign_up": "Liitu", + "message_bar_type_message": "Sisesta oma sõnum siia", + "message_bar_error_publishing": "Viga teavituse avaldamisel", + "message_bar_show_dialog": "Näita avaldamisvaadet", + "message_bar_publish": "Avalda sõnum", + "nav_topics_title": "Tellitud teemad", + "nav_button_all_notifications": "Kõik teavitused", + "nav_button_account": "Kasutajakonto", + "nav_button_settings": "Seadistused", + "nav_button_publish_message": "Avalda teavitus", + "nav_button_subscribe": "Telli teema", + "nav_button_muted": "Teavitused on summutatud", + "nav_button_connecting": "loome ühendust", + "nav_upgrade_banner_label": "Uuenda ntfy Pro teenuseks", + "action_bar_profile_logout": "Logi välja" } From 047cc22dbaac4f2fd333215191e7aef2d954fd2f Mon Sep 17 00:00:00 2001 From: OZZY Date: Mon, 24 Feb 2025 16:13:50 +0000 Subject: [PATCH 086/378] Translated using Weblate (Arabic) Currently translated at 88.3% (358 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ar/ --- web/public/static/langs/ar.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ar.json b/web/public/static/langs/ar.json index b0ddadbe..158fa926 100644 --- a/web/public/static/langs/ar.json +++ b/web/public/static/langs/ar.json @@ -359,5 +359,6 @@ "account_basics_phone_numbers_dialog_verify_button_call": "اتصل بي", "account_basics_phone_numbers_dialog_code_label": "رمز التحقّق", "account_upgrade_dialog_tier_price_per_month": "شهر", - "prefs_appearance_theme_title": "الحُلّة" + "prefs_appearance_theme_title": "الحُلّة", + "subscribe_dialog_subscribe_use_another_background_info": "لن يتم استلام الاشعارات من الخوادم الخارجية عندما يكون تطبيق الويب مغلقاً" } From a92306b1811e46d497e9e1310af94bb9ceadcc51 Mon Sep 17 00:00:00 2001 From: Korab Arifi Date: Sat, 1 Mar 2025 20:43:21 +0100 Subject: [PATCH 087/378] Added translation using Weblate (Albanian) --- web/public/static/langs/sq.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/sq.json diff --git a/web/public/static/langs/sq.json b/web/public/static/langs/sq.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/sq.json @@ -0,0 +1 @@ +{} From 2eb5eb3e29fb529894d5536a95b155f7f6c34fb6 Mon Sep 17 00:00:00 2001 From: Korab Arifi Date: Sat, 1 Mar 2025 20:44:28 +0100 Subject: [PATCH 088/378] Translated using Weblate (Albanian) Currently translated at 10.1% (41 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sq/ --- web/public/static/langs/sq.json | 44 ++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/sq.json b/web/public/static/langs/sq.json index 0967ef42..836b553b 100644 --- a/web/public/static/langs/sq.json +++ b/web/public/static/langs/sq.json @@ -1 +1,43 @@ -{} +{ + "common_back": "Prapa", + "signup_form_username": "Emri i përdoruesit", + "signup_title": "Krijo një llogari ntfy", + "signup_form_toggle_password_visibility": "Ndrysho dukshmërinë e fjalëkalimit", + "common_save": "Ruaj", + "signup_form_confirm_password": "Konfirmo Fjalëkalimin", + "common_copy_to_clipboard": "Kopjo", + "signup_form_button_submit": "Regjistrohu", + "signup_already_have_account": "Keni tashmë llogari? Identifikohu!", + "signup_disabled": "Regjistrimi është i çaktivizuar", + "signup_error_username_taken": "Emri i përdoruesit {{username}} është marrë tashmë", + "signup_error_creation_limit_reached": "U arrit kufiri i krijimit të llogarisë", + "login_title": "Hyni në llogarinë tuaj ntfy", + "login_form_button_submit": "Identifikohu", + "login_disabled": "Identifikimi është i çaktivizuar", + "action_bar_show_menu": "Shfaq menunë", + "action_bar_settings": "Parametrat", + "action_bar_account": "Llogaria", + "action_bar_change_display_name": "Ndrysho emrin e shfaqur", + "action_bar_reservation_add": "Rezervo temën", + "action_bar_reservation_edit": "Ndrysho rezervimin", + "action_bar_reservation_delete": "Hiq rezervimin", + "action_bar_reservation_limit_reached": "U arrit kufiri", + "action_bar_send_test_notification": "Dërgo njoftim testues", + "action_bar_clear_notifications": "Pastro të gjitha njoftimet", + "action_bar_mute_notifications": "Heshti njoftimet", + "action_bar_unmute_notifications": "Lejo njoftimet", + "action_bar_unsubscribe": "Ç'abonohu", + "action_bar_toggle_mute": "Hesht/lejo njoftimet", + "action_bar_toggle_action_menu": "Hap/mbyll menynë e veprimit", + "action_bar_profile_title": "Profili", + "action_bar_profile_settings": "Parametrat", + "action_bar_profile_logout": "Dil", + "action_bar_sign_in": "Identifikohu", + "action_bar_sign_up": "Regjistrohu", + "message_bar_type_message": "Shkruaj një mesazh këtu", + "common_cancel": "Anulo", + "signup_form_password": "Fjalëkalimi", + "common_add": "Shto", + "login_link_signup": "Regjistrohu", + "action_bar_logo_alt": "logo e ntfy" +} From 609c9fa37dd4b45009cb581d6b3b010dafc897c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Podg=C3=B3rski?= Date: Mon, 3 Mar 2025 22:19:50 +0100 Subject: [PATCH 089/378] Translated using Weblate (Polish) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pl/ --- web/public/static/langs/pl.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/pl.json b/web/public/static/langs/pl.json index 20cbfe29..3f90cac4 100644 --- a/web/public/static/langs/pl.json +++ b/web/public/static/langs/pl.json @@ -404,5 +404,10 @@ "prefs_reservations_dialog_title_add": "Zarezerwuj temat", "reservation_delete_dialog_action_keep_title": "Zachowaj wiadomości i załącznik w pamięci cache", "reservation_delete_dialog_action_keep_description": "Wiadomości i załączniki które są zapisane w pamięci cache będą dostępne publicznie dla każdego znającego nazwę powiązanego z nimi tematu.", - "web_push_unknown_notification_title": "Nieznane powiadomienie otrzymane od serwera" + "web_push_unknown_notification_title": "Nieznane powiadomienie otrzymane od serwera", + "action_bar_unmute_notifications": "Włącz ponownie powiadomienia", + "prefs_appearance_theme_title": "Wygląd", + "prefs_reservations_dialog_description": "Zastrzeżenie tematu daje użytkownikowi prawo własności do tego tematu i umożliwia zdefiniowanie uprawnień dostępu do tego tematu dla innych użytkowników.", + "reservation_delete_dialog_description": "Usunięcie rezerwacji powoduje rezygnację z prawa własności do tematu i umożliwia innym jego zarezerwowanie. Istniejące wiadomości i załączniki można zachować lub usunąć.", + "web_push_unknown_notification_body": "Konieczne może być zaktualizowanie ntfy poprzez otwarcie aplikacji internetowej" } From 29cf4f16d198b3079f99bf95b86dbb26157cdaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eero=20H=C3=A4kkinen?= <+weblate@eero.xn--hkkinen-5wa.fi> Date: Thu, 20 Mar 2025 09:11:08 +0100 Subject: [PATCH 090/378] Translated using Weblate (Finnish) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/ --- web/public/static/langs/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json index ab2f6188..a777ec00 100644 --- a/web/public/static/langs/fi.json +++ b/web/public/static/langs/fi.json @@ -266,7 +266,7 @@ "alert_not_supported_title": "Ilmoituksia ei tueta", "account_tokens_dialog_button_cancel": "Peruuta", "subscribe_dialog_error_user_anonymous": "Anonyymi", - "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Tallenna {{save}}.", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Säästä {{save}}.", "prefs_notifications_min_priority_high_and_higher": "Korkea prioriteetti ja korkeammat", "account_usage_basis_ip_description": "Tämän tilin käyttötilastot ja rajoitukset perustuvat IP-osoitteeseesi, joten ne voidaan jakaa muiden käyttäjien kanssa. Yllä esitetyt rajat ovat likimääräisiä perustuen olemassa oleviin rajoituksiin.", "publish_dialog_priority_high": "Korkea prioriteetti", From 4e2a884da5d1126685b84d9786fd44a3ef30b1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eero=20H=C3=A4kkinen?= <+weblate@eero.xn--hkkinen-5wa.fi> Date: Thu, 20 Mar 2025 09:24:50 +0100 Subject: [PATCH 091/378] Translated using Weblate (Finnish) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/ --- web/public/static/langs/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json index a777ec00..4ddf2920 100644 --- a/web/public/static/langs/fi.json +++ b/web/public/static/langs/fi.json @@ -170,7 +170,7 @@ "account_basics_tier_description": "Tilisi taso", "account_basics_phone_numbers_description": "Puheluilmoituksia varten", "prefs_reservations_dialog_title_add": "Varaa topikki", - "account_basics_tier_free": "Vapaa", + "account_basics_tier_free": "Maksuton", "account_upgrade_dialog_cancel_warning": "Tämä peruuttaa tilauksesi ja alentaa tilisi {{date}}. Tuona päivänä topikit sekä palvelimen välimuistissa olevat viestit poistetaan.", "notifications_click_copy_url_button": "Kopioi linkki", "account_basics_tier_admin": "Admin", From 35e15cfd9d9ced2022b37d8dd57a9fb81db76e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s?= Date: Sat, 22 Mar 2025 19:09:50 +0100 Subject: [PATCH 092/378] Translated using Weblate (Hungarian) Currently translated at 53.5% (217 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/hu/ --- web/public/static/langs/hu.json | 40 ++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/web/public/static/langs/hu.json b/web/public/static/langs/hu.json index e4e0b85b..45fcd50d 100644 --- a/web/public/static/langs/hu.json +++ b/web/public/static/langs/hu.json @@ -1,7 +1,7 @@ { "action_bar_send_test_notification": "Teszt értesítés küldése", "action_bar_clear_notifications": "Összes értesítés törlése", - "alert_not_supported_description": "A böngésző nem támogatja az értesítések fogadását.", + "alert_not_supported_description": "A böngésződ nem támogatja az értesítések fogadását", "action_bar_settings": "Beállítások", "action_bar_unsubscribe": "Leiratkozás", "message_bar_type_message": "Írd ide az üzenetet", @@ -9,19 +9,19 @@ "nav_button_all_notifications": "Összes értesítés", "nav_topics_title": "Feliratkozott témák", "alert_notification_permission_required_title": "Az értesítések le vannak tiltva", - "alert_notification_permission_required_description": "Engedélyezd a böngészőnek, hogy asztali értesítéseket jeleníttessen meg.", + "alert_notification_permission_required_description": "Engedélyezd a böngésződnek, hogy asztali értesítéseket jelenítsen meg", "nav_button_settings": "Beállítások", "nav_button_documentation": "Dokumentáció", "nav_button_publish_message": "Értesítés küldése", "alert_notification_permission_required_button": "Engedélyezés", - "alert_not_supported_title": "Nem támogatott funkció", - "notifications_copied_to_clipboard": "Másolva a vágólapra", + "alert_not_supported_title": "Az értesítések nincsenek támogatva", + "notifications_copied_to_clipboard": "Vágólapra másolva", "notifications_tags": "Címkék", "notifications_attachment_copy_url_title": "Másolja vágólapra a csatolmány URL-ét", "notifications_attachment_copy_url_button": "URL másolása", "notifications_attachment_open_title": "Menjen a(z) {{url}} címre", "notifications_attachment_open_button": "Csatolmány megnyitása", - "notifications_attachment_link_expired": "A letöltési hivatkozás lejárt", + "notifications_attachment_link_expired": "A letöltési link lejárt", "notifications_attachment_link_expires": "A hivatkozás {{date}}-kor jár le", "nav_button_subscribe": "Feliratkozás témára", "notifications_click_copy_url_title": "Másolja vágólapra a hivatkozás URL-ét", @@ -187,5 +187,33 @@ "prefs_users_edit_button": "Felhasználó szerkesztése", "prefs_users_delete_button": "Felhasználó törlése", "error_boundary_unsupported_indexeddb_title": "Privát böngészés nem támogatott", - "subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL" + "subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL", + "signup_form_username": "Felhasználónév", + "signup_form_password": "Jelszó", + "signup_form_button_submit": "Regisztráció", + "login_form_button_submit": "Bejelentkezés", + "login_link_signup": "Regisztráció", + "login_disabled": "Bejelentkezés kikapcsolva", + "action_bar_change_display_name": "Megjelenített név módosítása", + "action_bar_profile_logout": "Kijelentkezés", + "action_bar_sign_in": "Bejelentkezés", + "action_bar_sign_up": "Regisztráció", + "action_bar_profile_title": "Profil", + "nav_button_account": "Fiók", + "common_copy_to_clipboard": "Másolás vágólapra", + "action_bar_reservation_limit_reached": "Limit elérve", + "login_title": "Jelentkezz be a ntfy felhasználódba", + "signup_title": "Hozz létre egy ntfy felhasználói fiókot", + "signup_form_confirm_password": "Jelszó megerősítése", + "signup_already_have_account": "Már van felhasználód? Jelentkezz be!", + "action_bar_account": "Fiók", + "action_bar_profile_settings": "Beállítások", + "signup_error_username_taken": "A felhasználónév {{username}} már foglalt", + "signup_error_creation_limit_reached": "Felhasználói regisztráció limit elérve", + "action_bar_mute_notifications": "Értesítések némítása", + "action_bar_unmute_notifications": "Értesítések némításának feloldása", + "alert_notification_permission_denied_title": "Az értesítések blokkolva vannak", + "alert_notification_permission_denied_description": "Kérjük kapcsold őket vissza a böngésződben", + "alert_notification_ios_install_required_title": "iOS telepítés szükséges", + "alert_not_supported_context_description": "Az értesítések kizárólag HTTPS-en keresztül támogatottak. Ez a Notifications API korlátozása." } From adfacf820e448bfc2da7e5ae007c10bcad25a667 Mon Sep 17 00:00:00 2001 From: Max Badran Date: Tue, 25 Mar 2025 17:23:16 +0100 Subject: [PATCH 093/378] Translated using Weblate (Ukrainian) Currently translated at 99.7% (404 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/uk/ --- web/public/static/langs/uk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/uk.json b/web/public/static/langs/uk.json index c51dfcb3..e3e04a19 100644 --- a/web/public/static/langs/uk.json +++ b/web/public/static/langs/uk.json @@ -403,5 +403,6 @@ "error_boundary_button_reload_ntfy": "Перезавантажити ntfy", "web_push_subscription_expiring_title": "Сповіщення буде призупинено", "web_push_subscription_expiring_body": "Відкрийте ntfy, щоб продовжити отримувати сповіщення", - "web_push_unknown_notification_body": "Можливо вам потрібно оновити ntfy шляхом відкриття вебзастосунку" + "web_push_unknown_notification_body": "Можливо вам потрібно оновити ntfy шляхом відкриття вебзастосунку", + "alert_notification_ios_install_required_title": "потрібно встановити на iOS" } From 70f0e7ccc7f6c37c2623bd4f55cd13be861d66c5 Mon Sep 17 00:00:00 2001 From: Tyxiel <128870773+Tyxiel@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:14:11 +0200 Subject: [PATCH 094/378] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/ --- web/public/static/langs/pt_BR.json | 66 +++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json index 9d4dba3a..ffe4131a 100644 --- a/web/public/static/langs/pt_BR.json +++ b/web/public/static/langs/pt_BR.json @@ -340,5 +340,69 @@ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} por ano. Cobrado mensalmente.", "account_upgrade_dialog_button_pay_now": "Pague agora para assinar", "account_tokens_table_expires_header": "Expira", - "prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta." + "prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta.", + "account_tokens_description": "Use tokens de acesso ao publicar e assinar por meio da API ntfy, para que você não precise enviar as credenciais da sua conta. Consulte a documentação para saber mais.", + "account_tokens_table_cannot_delete_or_edit": "Não é possível editar ou excluir o token da sessão atual", + "account_tokens_dialog_title_edit": "Editar token de acesso", + "account_tokens_dialog_title_delete": "Excluir token de acesso", + "prefs_reservations_table_everyone_read_write": "Todos podem publicar e se inscrever", + "prefs_reservations_table_everyone_read_only": "Posso publicar e me inscrever, todos podem se inscrever", + "prefs_reservations_limit_reached": "Você atingiu seu limite de tópicos reservados.", + "prefs_reservations_delete_button": "Redefinir o acesso ao tópico", + "prefs_reservations_edit_button": "Editar acesso ao tópico", + "prefs_reservations_table_everyone_write_only": "Eu posso publicar e me inscrever, todos podem publicar", + "prefs_reservations_table_not_subscribed": "Não inscrito", + "prefs_reservations_table_click_to_subscribe": "Clique para se inscrever", + "reservation_delete_dialog_action_keep_title": "Manter mensagens e anexos em cache", + "account_tokens_table_label_header": "Rótulo", + "account_tokens_table_last_origin_tooltip": "Do endereço IP {{ip}}, clique para pesquisar", + "account_tokens_dialog_title_create": "Criar token de acesso", + "account_tokens_delete_dialog_title": "Excluir token de acesso", + "account_tokens_dialog_label": "Rótulo, por exemplo, notificações de Radarr", + "account_tokens_dialog_expires_never": "O token nunca expira", + "prefs_reservations_dialog_title_edit": "Editar tópico reservado", + "prefs_notifications_web_push_enabled_description": "As notificações são recebidas mesmo quando o aplicativo Web não está em execução (via Web Push)", + "prefs_notifications_web_push_disabled_description": "As notificações são recebidas quando o aplicativo Web está em execução (via WebSocket)", + "account_upgrade_dialog_billing_contact_website": "Para perguntas sobre faturamento, consulte nosso website.", + "account_tokens_table_create_token_button": "Criar token de acesso", + "account_tokens_dialog_button_cancel": "Cancelar", + "account_tokens_dialog_button_update": "Atualizar token", + "prefs_reservations_table": "Tabela de tópicos reservados", + "prefs_reservations_table_everyone_deny_all": "Somente eu posso publicar e me inscrever", + "account_tokens_delete_dialog_description": "Antes de excluir um token de acesso, certifique-se de que nenhum aplicativo ou script o esteja usando ativamente. Esta ação não pode ser desfeita.", + "account_tokens_delete_dialog_submit_button": "Excluir token permanentemente", + "account_tokens_dialog_expires_x_hours": "O token expira em {{hours}} horas", + "account_tokens_dialog_expires_x_days": "O token expira em {{days}} dias", + "prefs_reservations_description": "Você pode reservar nomes de tópicos para uso pessoal aqui. A reserva de um tópico lhe dá propriedade sobre ele e permite que você defina permissões de acesso para outros usuários sobre o tópico.", + "prefs_reservations_dialog_access_label": "Acesso", + "account_upgrade_dialog_billing_contact_email": "Para questões de cobrança, entre em contato conosco diretamente.", + "account_tokens_dialog_button_create": "Criar token", + "account_tokens_dialog_expires_label": "O token de acesso expira em", + "account_tokens_dialog_expires_unchanged": "Deixar a data de validade inalterada", + "prefs_notifications_web_push_title": "Notificações em segundo plano", + "prefs_notifications_web_push_enabled": "Ativado para {{server}}", + "prefs_notifications_web_push_disabled": "Desativado", + "prefs_appearance_theme_title": "Tema", + "prefs_users_table_cannot_delete_or_edit": "Não é possível excluir ou editar o usuário conectado", + "prefs_appearance_theme_system": "Sistema (padrão)", + "prefs_appearance_theme_dark": "Modo escuro", + "prefs_appearance_theme_light": "Modo claro", + "prefs_reservations_title": "Tópicos reservados", + "prefs_reservations_add_button": "Adicionar tópico reservado", + "prefs_reservations_table_topic_header": "Tópico", + "prefs_reservations_table_access_header": "Acesso", + "prefs_reservations_dialog_title_add": "Reservar tópico", + "prefs_reservations_dialog_title_delete": "Excluir reserva de tópico", + "prefs_reservations_dialog_description": "A reserva de um tópico lhe dá propriedade sobre ele e permite definir permissões de acesso para outros usuários sobre o tópico.", + "prefs_reservations_dialog_topic_label": "Tópico", + "reservation_delete_dialog_description": "A remoção de uma reserva abre mão da propriedade sobre o tópico e permite que outros o reservem. Você pode manter ou excluir as mensagens e os anexos existentes.", + "reservation_delete_dialog_action_keep_description": "As mensagens e os anexos armazenados em cache no servidor ficarão visíveis publicamente para as pessoas que souberem o nome do tópico.", + "reservation_delete_dialog_action_delete_title": "Excluir mensagens e anexos armazenados em cache", + "reservation_delete_dialog_action_delete_description": "As mensagens e os anexos armazenados em cache serão excluídos permanentemente. Essa ação não pode ser desfeita.", + "reservation_delete_dialog_submit_button": "Excluir reserva", + "error_boundary_button_reload_ntfy": "Recarregar ntfy", + "web_push_subscription_expiring_title": "As notificações serão pausadas", + "web_push_subscription_expiring_body": "Abra o ntfy para continuar recebendo notificações", + "web_push_unknown_notification_title": "Notificação desconhecida recebida do servidor", + "web_push_unknown_notification_body": "Talvez seja necessário atualizar o ntfy abrindo o aplicativo da Web" } From 8777990d2d522b15db054c5e699a68d2196424b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 23 Apr 2025 21:11:18 +0200 Subject: [PATCH 095/378] Translated using Weblate (Estonian) Currently translated at 24.9% (101 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 60 ++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index 11af0335..7d2a5a85 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -41,5 +41,63 @@ "nav_button_muted": "Teavitused on summutatud", "nav_button_connecting": "loome ühendust", "nav_upgrade_banner_label": "Uuenda ntfy Pro teenuseks", - "action_bar_profile_logout": "Logi välja" + "action_bar_profile_logout": "Logi välja", + "notifications_list_item": "Teavitus", + "account_tokens_table_expires_header": "Aegub", + "notifications_attachment_file_document": "muu dokument", + "notifications_list": "Teavituste loend", + "notifications_delete": "Kustuta", + "notifications_copied_to_clipboard": "Kopeeritud lõikelauale", + "alert_notification_permission_denied_description": "Palun luba nad veebibrauseris uuesti", + "account_tokens_table_last_access_header": "Viimase kasutamise aeg", + "account_tokens_table_token_header": "Tunnusluba", + "account_tokens_table_last_origin_tooltip": "IP-aadressilt {{ip}}, klõpsi täpsema teabe nägemiseks", + "action_bar_reservation_add": "Reserveeri teema", + "action_bar_reservation_edit": "Muuda reserveeringut", + "action_bar_reservation_delete": "Eemalda reserveering", + "action_bar_reservation_limit_reached": "Ülempiir on käes", + "action_bar_send_test_notification": "Saata testteavitus", + "action_bar_clear_notifications": "Kustuta kõik teavitused", + "action_bar_mute_notifications": "Summuta teavitused", + "nav_upgrade_banner_description": "Reserveeri teemasid, rohkem sõnumeid ja e-kirju ning suuremad manused", + "action_bar_unmute_notifications": "Lõpeta teavituste summutamine", + "action_bar_unsubscribe": "Lõpeta tellimus", + "action_bar_toggle_mute": "Lülita teavituste summutamine sisse/välja", + "action_bar_toggle_action_menu": "Ava/sulge tegevuste menüü", + "notifications_mark_read": "Märgi loetuks", + "notifications_tags": "Sildid", + "notifications_priority_x": "{{priority}}. prioriteet", + "notifications_new_indicator": "Uus teavitus", + "notifications_attachment_image": "Pilt manusena", + "notifications_attachment_copy_url_title": "Kopeeri manuse võrguaadress lõikelauale", + "notifications_attachment_copy_url_button": "Kopeeri võrguaadress", + "notifications_attachment_open_title": "Ava {{url}} aadress", + "notifications_attachment_open_button": "Ava manus", + "notifications_attachment_link_expires": "link aegub {{date}}", + "notifications_attachment_link_expired": "allalaadimise link on aegunud", + "notifications_attachment_file_image": "pildifail", + "notifications_attachment_file_video": "videofail", + "notifications_attachment_file_audio": "helifail", + "notifications_attachment_file_app": "Androidi rakenduse fail", + "notifications_click_copy_url_title": "Kopeeri lingi võrguaadress lõikelauale", + "notifications_click_copy_url_button": "Kopeeri link", + "notifications_click_open_button": "Ava link", + "notifications_actions_open_url_title": "Ava {{url}} aadress", + "notifications_actions_not_supported": "Toiming pole veebirakenduses toetatud", + "alert_notification_permission_required_title": "Teavitused pole kasutusel", + "alert_notification_permission_required_description": "Anna oma brauserile õigused näidata töölauateavitusi", + "alert_notification_permission_required_button": "Luba nüüd", + "alert_notification_permission_denied_title": "Teavitused on blokeeritud", + "alert_notification_ios_install_required_title": "Vajalik on iOS-i paigaldamine", + "alert_not_supported_title": "Teavitused pole toetatud", + "alert_not_supported_description": "Teavitused pole sinu veebibrauseris toetatud", + "account_tokens_table_label_header": "Silt", + "account_tokens_table_never_expires": "Ei aegu iialgi", + "account_tokens_table_current_session": "Praegune brauserisessioon", + "account_tokens_table_copied_to_clipboard": "Ligipääsu tunnusluba on kopeeritud", + "account_tokens_table_cannot_delete_or_edit": "Praeguse sessiooni tunnusluba ei saa muuta ega kustutada", + "account_tokens_table_create_token_button": "Loo ligipääsuks vajalik tunnusluba", + "account_tokens_dialog_title_create": "Loo ligipääsuks vajalik tunnusluba", + "account_tokens_dialog_title_edit": "Muuda ligipääsuks vajalikku tunnusluba", + "account_tokens_dialog_title_delete": "Kustuta ligipääsuks vajalik tunnusluba" } From 3bf02d3cd9a29249c3d1c46e300d491483ee0eb1 Mon Sep 17 00:00:00 2001 From: Andrea Toska Date: Tue, 20 May 2025 14:18:08 +0200 Subject: [PATCH 096/378] Translated using Weblate (Albanian) Currently translated at 15.0% (61 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sq/ --- web/public/static/langs/sq.json | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/web/public/static/langs/sq.json b/web/public/static/langs/sq.json index 836b553b..c146c77d 100644 --- a/web/public/static/langs/sq.json +++ b/web/public/static/langs/sq.json @@ -1,7 +1,7 @@ { "common_back": "Prapa", "signup_form_username": "Emri i përdoruesit", - "signup_title": "Krijo një llogari ntfy", + "signup_title": "Krijo një llogari \"ntfy\"", "signup_form_toggle_password_visibility": "Ndrysho dukshmërinë e fjalëkalimit", "common_save": "Ruaj", "signup_form_confirm_password": "Konfirmo Fjalëkalimin", @@ -35,9 +35,29 @@ "action_bar_sign_in": "Identifikohu", "action_bar_sign_up": "Regjistrohu", "message_bar_type_message": "Shkruaj një mesazh këtu", - "common_cancel": "Anulo", + "common_cancel": "Anullo", "signup_form_password": "Fjalëkalimi", "common_add": "Shto", "login_link_signup": "Regjistrohu", - "action_bar_logo_alt": "logo e ntfy" + "action_bar_logo_alt": "logo e ntfy", + "message_bar_error_publishing": "Gabim duke postuar njoftimin", + "message_bar_show_dialog": "Trego dialogun e publikimit", + "message_bar_publish": "Publiko mesazhin", + "nav_topics_title": "Temat e abonuara", + "nav_button_all_notifications": "Të gjitha njoftimet", + "nav_button_account": "Llogaria", + "nav_button_settings": "Cilësimet", + "nav_button_publish_message": "Publiko njoftimin", + "nav_button_subscribe": "Abunohu tek tema", + "nav_button_connecting": "duke u lidhur", + "nav_upgrade_banner_label": "Përmirëso në ntfy Pro", + "nav_upgrade_banner_description": "Rezervoni tema, më shumë mesazhe dhe email-e, si dhe bashkëngjitje më të mëdha", + "nav_button_muted": "Njoftimet janë të fikura", + "alert_notification_permission_required_title": "Njoftimet janë të çaktivizuar", + "alert_notification_permission_required_description": "Jepni leje Browser-it tuaj për të shfaqur njoftimet në desktop", + "alert_notification_permission_denied_title": "Njoftimet janë të bllokuara", + "alert_notification_ios_install_required_title": "Instalimi i iOS-it detyrohet", + "alert_notification_permission_denied_description": "Ju lutemi riaktivizoni ato në Browser-in tuaj", + "nav_button_documentation": "Dokumentacion", + "alert_notification_permission_required_button": "Lejo tani" } From b2b9891a58c0c974b9f3dbfa3a58fd3d6c518ab0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 22 May 2025 21:43:45 -0400 Subject: [PATCH 097/378] Add Tamil language --- docs/releases.md | 5 +++++ web/src/components/Preferences.jsx | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index be676f6f..a76ec6a3 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1413,6 +1413,11 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) * Lots of other tiny docs updates, tanks to everyone who contributed! +**Languages** + +* Update new languages from Weblate. Thanks to all the contributors! +* Added Tamil (தமிழ்) as a new language to the web app + ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index 6770f282..c733c23c 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -592,6 +592,7 @@ const Language = () => { Suomi Svenska Türkçe + தமிழ் From 69cf77383458c38987d99df348bab38a8d997386 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 22 May 2025 21:56:28 -0400 Subject: [PATCH 098/378] Fix webpush command --- cmd/webpush.go | 6 +++--- main.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/webpush.go b/cmd/webpush.go index a5f66e60..249f91c8 100644 --- a/cmd/webpush.go +++ b/cmd/webpush.go @@ -44,16 +44,16 @@ func generateWebPushKeys(c *cli.Context) error { return err } - if outputFIle := c.String("output-file"); outputFIle != "" { + if outputFile := c.String("output-file"); outputFile != "" { contents := fmt.Sprintf(`--- web-push-public-key: %s web-push-private-key: %s `, publicKey, privateKey) - err = os.WriteFile(outputFIle, []byte(contents), 0660) + err = os.WriteFile(outputFile, []byte(contents), 0660) if err != nil { return err } - _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys written to %s.`, outputFIle) + _, err = fmt.Fprintf(c.App.ErrWriter, "Web Push keys written to %s.\n", outputFile) } else { _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: diff --git a/main.go b/main.go index d23072d5..4e01a0d6 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ If you want to chat, simply join the Discord server (https://discord.gg/cT7ECsZj the Matrix room (https://matrix.to/#/#ntfy:matrix.org). ntfy %s (%s), runtime %s, built at %s -Copyright (C) Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2 +Copyright (C) Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2 `, version, commit[:7], runtime.Version(), date) app := cmd.New() From 8f9dafce2051a14c0daec0bc61a273f46765cda8 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sat, 25 Jan 2025 21:25:59 -0700 Subject: [PATCH 099/378] change user password via accounts API --- docs/releases.md | 1 + server/server_admin.go | 6 +++++ server/server_admin_test.go | 46 +++++++++++++++++++++++++++++++++++++ server/types.go | 1 + 4 files changed, 54 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index a76ec6a3..959fbb83 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1381,6 +1381,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii)) * Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) * Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) +* You can now change passwords via the accounts API (thanks to [@wunter8](https://github.com/wunter8) for implementing) **Bug fixes + maintenance:** diff --git a/server/server_admin.go b/server/server_admin.go index ac295718..0e7e311e 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -49,6 +49,12 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit if err != nil && !errors.Is(err, user.ErrUserNotFound) { return err } else if u != nil { + if req.Force == true { + if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) + } return errHTTPConflictUserExists } var tier *user.Tier diff --git a/server/server_admin_test.go b/server/server_admin_test.go index c2f8f95a..70574efe 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -49,6 +49,52 @@ func TestUser_AddRemove(t *testing.T) { "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, rr.Code) + + // Check user was deleted + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "emma", users[1].Name) + require.Equal(t, user.Everyone, users[2].Name) +} + +func TestUser_ChangePassword(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + + // Create user via API + rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with first password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Change password via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two", "force":true}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure first password fails + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben-two"), + }) + require.Equal(t, 200, rr.Code) } func TestUser_AddRemove_Failures(t *testing.T) { diff --git a/server/types.go b/server/types.go index c6bdb4d1..1b95a73d 100644 --- a/server/types.go +++ b/server/types.go @@ -257,6 +257,7 @@ type apiUserAddRequest struct { Username string `json:"username"` Password string `json:"password"` Tier string `json:"tier"` + Force bool `json:"force"` // Used to change passwords/override existing user // Do not add 'role' here. We don't want to add admins via the API. } From ad7ab18fb737d22f3cbb434665cd49b0048dba44 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sat, 25 Jan 2025 21:37:23 -0700 Subject: [PATCH 100/378] prevent changing admin passwords --- server/server_admin.go | 3 +++ server/server_admin_test.go | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/server/server_admin.go b/server/server_admin.go index 0e7e311e..a2654db7 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -50,6 +50,9 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit return err } else if u != nil { if req.Force == true { + if u.IsAdmin() { + return errHTTPForbidden + } if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { return err } diff --git a/server/server_admin_test.go b/server/server_admin_test.go index 70574efe..c99ec549 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -59,7 +59,7 @@ func TestUser_AddRemove(t *testing.T) { require.Equal(t, user.Everyone, users[2].Name) } -func TestUser_ChangePassword(t *testing.T) { +func TestUser_ChangeUserPassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() @@ -97,6 +97,21 @@ func TestUser_ChangePassword(t *testing.T) { require.Equal(t, 200, rr.Code) } +func TestUser_DontChangeAdminPassword(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin)) + + // Try to change password via API + rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new", "force":true}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 403, rr.Code) +} + func TestUser_AddRemove_Failures(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() From 2b40ad9a129dabbcbe7a90c3be26c9071e50a0ea Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sat, 25 Jan 2025 21:47:11 -0700 Subject: [PATCH 101/378] make staticcheck happy --- server/server_admin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server_admin.go b/server/server_admin.go index a2654db7..d1cdefae 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -49,7 +49,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit if err != nil && !errors.Is(err, user.ErrUserNotFound) { return err } else if u != nil { - if req.Force == true { + if req.Force { if u.IsAdmin() { return errHTTPForbidden } From fa48639517bd65b4b3f20472a015e6f7af409ea2 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Thu, 22 May 2025 18:58:37 -0600 Subject: [PATCH 102/378] make POST create user and PUT update user --- server/server.go | 4 ++- server/server_admin.go | 49 +++++++++++++++++++++++++++++-------- server/server_admin_test.go | 14 +++++------ server/types.go | 3 +-- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/server/server.go b/server/server.go index c4af5ce8..90e52bf4 100644 --- a/server/server.go +++ b/server/server.go @@ -446,8 +446,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersGet)(w, r, v) - } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { + } else if r.Method == http.MethodPost && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersAdd)(w, r, v) + } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersUpdate)(w, r, v) } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersDelete)(w, r, v) } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath { diff --git a/server/server_admin.go b/server/server_admin.go index d1cdefae..e8e91320 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -39,7 +39,7 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit } func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } else if !user.AllowedUsername(req.Username) || req.Password == "" { @@ -49,15 +49,6 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit if err != nil && !errors.Is(err, user.ErrUserNotFound) { return err } else if u != nil { - if req.Force { - if u.IsAdmin() { - return errHTTPForbidden - } - if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { - return err - } - return s.writeJSON(w, newSuccessResponse()) - } return errHTTPConflictUserExists } var tier *user.Tier @@ -79,6 +70,44 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit } return s.writeJSON(w, newSuccessResponse()) } +func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } else if !user.AllowedUsername(req.Username) || req.Password == "" { + return errHTTPBadRequest.Wrap("username invalid, or password missing") + } + u, err := s.userManager.User(req.Username) + if err != nil && !errors.Is(err, user.ErrUserNotFound) { + return err + } else if u != nil { + if u.IsAdmin() { + return errHTTPForbidden + } + if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) + } + var tier *user.Tier + if req.Tier != "" { + tier, err = s.userManager.Tier(req.Tier) + if errors.Is(err, user.ErrTierNotFound) { + return errHTTPBadRequestTierInvalid + } else if err != nil { + return err + } + } + if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { + return err + } + if tier != nil { + if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil { + return err + } + } + return s.writeJSON(w, newSuccessResponse()) +} func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false) diff --git a/server/server_admin_test.go b/server/server_admin_test.go index c99ec549..194b0a18 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -20,7 +20,7 @@ func TestUser_AddRemove(t *testing.T) { })) // Create user via API - rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, rr.Code) @@ -67,7 +67,7 @@ func TestUser_ChangeUserPassword(t *testing.T) { require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) // Create user via API - rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, rr.Code) @@ -79,7 +79,7 @@ func TestUser_ChangeUserPassword(t *testing.T) { require.Equal(t, 200, rr.Code) // Change password via API - rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two", "force":true}`, map[string]string{ + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, rr.Code) @@ -106,7 +106,7 @@ func TestUser_DontChangeAdminPassword(t *testing.T) { require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin)) // Try to change password via API - rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new", "force":true}`, map[string]string{ + rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 403, rr.Code) @@ -121,19 +121,19 @@ func TestUser_AddRemove_Failures(t *testing.T) { require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) // Cannot create user with invalid username - rr := request(t, s, "PUT", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ + rr := request(t, s, "POST", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 400, rr.Code) // Cannot create user if user already exists - rr = request(t, s, "PUT", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ + rr = request(t, s, "POST", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) // Cannot create user with invalid tier - rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ + rr = request(t, s, "POST", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code) diff --git a/server/types.go b/server/types.go index 1b95a73d..fd1931f5 100644 --- a/server/types.go +++ b/server/types.go @@ -253,11 +253,10 @@ type apiStatsResponse struct { MessagesRate float64 `json:"messages_rate"` // Average number of messages per second } -type apiUserAddRequest struct { +type apiUserAddOrUpdateRequest struct { Username string `json:"username"` Password string `json:"password"` Tier string `json:"tier"` - Force bool `json:"force"` // Used to change passwords/override existing user // Do not add 'role' here. We don't want to add admins via the API. } From e36e4856c9d04b2ad0e8ad5d080809d45c7b6cd0 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Thu, 22 May 2025 19:57:57 -0600 Subject: [PATCH 103/378] allow changing password or tier with user PUT --- server/server_admin.go | 18 +++++++++------ server/server_admin_test.go | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/server/server_admin.go b/server/server_admin.go index e8e91320..7c78adb3 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -74,8 +74,10 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err - } else if !user.AllowedUsername(req.Username) || req.Password == "" { - return errHTTPBadRequest.Wrap("username invalid, or password missing") + } else if !user.AllowedUsername(req.Username) { + return errHTTPBadRequest.Wrap("username invalid") + } else if req.Password == "" && req.Tier == "" { + return errHTTPBadRequest.Wrap("need to provide at least one of \"password\" or \"tier\"") } u, err := s.userManager.User(req.Username) if err != nil && !errors.Is(err, user.ErrUserNotFound) { @@ -84,10 +86,15 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi if u.IsAdmin() { return errHTTPForbidden } - if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { + if req.Password != "" { + if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { + return err + } + } + } else { + if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { return err } - return s.writeJSON(w, newSuccessResponse()) } var tier *user.Tier if req.Tier != "" { @@ -98,9 +105,6 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi return err } } - if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { - return err - } if tier != nil { if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil { return err diff --git a/server/server_admin_test.go b/server/server_admin_test.go index 194b0a18..80da3224 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -57,6 +57,12 @@ func TestUser_AddRemove(t *testing.T) { require.Equal(t, "phil", users[0].Name) require.Equal(t, "emma", users[1].Name) require.Equal(t, user.Everyone, users[2].Name) + + // Reject invalid user change + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) } func TestUser_ChangeUserPassword(t *testing.T) { @@ -97,6 +103,46 @@ func TestUser_ChangeUserPassword(t *testing.T) { require.Equal(t, 200, rr.Code) } +func TestUser_ChangeUserTier(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier2", + })) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Equal(t, "tier1", users[1].Tier.Code) + + // Change user tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "tier": "tier2"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users again + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, "tier2", users[1].Tier.Code) +} + func TestUser_DontChangeAdminPassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() From 0fb60ae72d8387abf6280874d85379cb502d6bca Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Thu, 22 May 2025 20:01:50 -0600 Subject: [PATCH 104/378] test change user password and tier in single request --- server/server_admin_test.go | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/server/server_admin_test.go b/server/server_admin_test.go index 80da3224..98c268e9 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -143,6 +143,58 @@ func TestUser_ChangeUserTier(t *testing.T) { require.Equal(t, "tier2", users[1].Tier.Code) } +func TestUser_ChangeUserPasswordAndTier(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier2", + })) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Equal(t, "tier1", users[1].Tier.Code) + + // Change user password and tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben-two", "tier": "tier2"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure first password fails + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben-two"), + }) + require.Equal(t, 200, rr.Code) + + // Check new tier + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, "tier2", users[1].Tier.Code) +} + func TestUser_DontChangeAdminPassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() From 0581a9e6800fceae8775eb37237315979055dd06 Mon Sep 17 00:00:00 2001 From: dandersch <59270379+dandersch@users.noreply.github.com> Date: Fri, 23 May 2025 22:52:25 +0200 Subject: [PATCH 105/378] remove systemd user service cmds from postinst.sh --- scripts/postinst.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/postinst.sh b/scripts/postinst.sh index 37ea3304..0cf80d10 100755 --- a/scripts/postinst.sh +++ b/scripts/postinst.sh @@ -41,13 +41,16 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then systemctl restart ntfy-client.service >/dev/null || true fi fi - if systemctl --user is-active -q ntfy-client.service; then - echo "Restarting ntfy-client.service (user)..." - if [ -x /usr/bin/deb-systemd-invoke ]; then - deb-systemd-invoke --user try-restart ntfy-client.service >/dev/null || true - else - systemctl --user restart ntfy-client.service >/dev/null || true - fi - fi + + # inform user about systemd user service + echo + echo "------------------------------------------------------------------------" + echo "ntfy includes a systemd user service." + echo "To enable it, run following commands as your regular user (not as root):" + echo + echo " systemctl --user enable ntfy-client.service" + echo " systemctl --user start ntfy-client.service" + echo "------------------------------------------------------------------------" + echo fi fi From 45e1707d3bb4498b31230b5921e206bd3ba71a37 Mon Sep 17 00:00:00 2001 From: dandersch <59270379+dandersch@users.noreply.github.com> Date: Fri, 23 May 2025 23:00:45 +0200 Subject: [PATCH 106/378] remove systemd user daemon-reload from postinst.sh --- scripts/postinst.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/postinst.sh b/scripts/postinst.sh index 0cf80d10..6e706205 100755 --- a/scripts/postinst.sh +++ b/scripts/postinst.sh @@ -24,7 +24,6 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then # Restart services systemctl --system daemon-reload >/dev/null || true - systemctl --user daemon-reload >/dev/null || true if systemctl is-active -q ntfy.service; then echo "Restarting ntfy.service ..." if [ -x /usr/bin/deb-systemd-invoke ]; then From 80462f7ee5f6c065267eabc6e88d18b714a7e4cb Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 24 May 2025 08:58:44 -0400 Subject: [PATCH 107/378] Refine user API change --- server/server_admin.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/server_admin.go b/server/server_admin.go index 7c78adb3..92da1213 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -70,6 +70,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit } return s.writeJSON(w, newSuccessResponse()) } + func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { @@ -96,16 +97,12 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi return err } } - var tier *user.Tier if req.Tier != "" { - tier, err = s.userManager.Tier(req.Tier) - if errors.Is(err, user.ErrTierNotFound) { + if _, err = s.userManager.Tier(req.Tier); errors.Is(err, user.ErrTierNotFound) { return errHTTPBadRequestTierInvalid } else if err != nil { return err } - } - if tier != nil { if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil { return err } From 4dc3b38c95482d22781297b794e4dc9c67c4adc7 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 24 May 2025 09:31:57 -0400 Subject: [PATCH 108/378] Allow adding/changing user with password hash via v1/users API --- server/server_admin.go | 28 ++++++++++----- server/server_admin_test.go | 71 ++++++++++++++++++++++++++++++++++--- server/types.go | 1 + user/manager.go | 3 -- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/server/server_admin.go b/server/server_admin.go index fc149e7f..eb362956 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -42,8 +42,8 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err - } else if !user.AllowedUsername(req.Username) || req.Password == "" { - return errHTTPBadRequest.Wrap("username invalid, or password missing") + } else if !user.AllowedUsername(req.Username) || (req.Password == "" && req.Hash == "") { + return errHTTPBadRequest.Wrap("username invalid, or password/password_hash missing") } u, err := s.userManager.User(req.Username) if err != nil && !errors.Is(err, user.ErrUserNotFound) { @@ -60,7 +60,11 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit return err } } - if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser, false); err != nil { + password, hashed := req.Password, false + if req.Hash != "" { + password, hashed = req.Hash, true + } + if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil { return err } if tier != nil { @@ -77,8 +81,8 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi return err } else if !user.AllowedUsername(req.Username) { return errHTTPBadRequest.Wrap("username invalid") - } else if req.Password == "" && req.Tier == "" { - return errHTTPBadRequest.Wrap("need to provide at least one of \"password\" or \"tier\"") + } else if req.Password == "" && req.Hash == "" && req.Tier == "" { + return errHTTPBadRequest.Wrap("need to provide at least one of \"password\", \"password_hash\" or \"tier\"") } u, err := s.userManager.User(req.Username) if err != nil && !errors.Is(err, user.ErrUserNotFound) { @@ -87,13 +91,21 @@ func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *vi if u.IsAdmin() { return errHTTPForbidden } - if req.Password != "" { - if err := s.userManager.ChangePassword(req.Username, req.Password); err != nil { + if req.Hash != "" { + if err := s.userManager.ChangePassword(req.Username, req.Hash, true); err != nil { + return err + } + } else if req.Password != "" { + if err := s.userManager.ChangePassword(req.Username, req.Password, false); err != nil { return err } } } else { - if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { + password, hashed := req.Password, false + if req.Hash != "" { + password, hashed = req.Hash, true + } + if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil { return err } } diff --git a/server/server_admin_test.go b/server/server_admin_test.go index 1fdc740f..8925702e 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -65,12 +65,41 @@ func TestUser_AddRemove(t *testing.T) { require.Equal(t, 400, rr.Code) } +func TestUser_AddWithPasswordHash(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check that user can login with password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, user.RoleAdmin, users[0].Role) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) +} + func TestUser_ChangeUserPassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) // Create user via API rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ @@ -108,7 +137,7 @@ func TestUser_ChangeUserTier(t *testing.T) { defer s.closeDatabases() // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "tier1", })) @@ -148,7 +177,7 @@ func TestUser_ChangeUserPasswordAndTier(t *testing.T) { defer s.closeDatabases() // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "tier1", })) @@ -195,13 +224,45 @@ func TestUser_ChangeUserPasswordAndTier(t *testing.T) { require.Equal(t, "tier2", users[1].Tier.Code) } +func TestUser_ChangeUserPasswordWithHash(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with first password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "not-ben"), + }) + require.Equal(t, 200, rr.Code) + + // Change user password and tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) +} + func TestUser_DontChangeAdminPassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false)) // Try to change password via API rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{ diff --git a/server/types.go b/server/types.go index fd1931f5..16049ee4 100644 --- a/server/types.go +++ b/server/types.go @@ -256,6 +256,7 @@ type apiStatsResponse struct { type apiUserAddOrUpdateRequest struct { Username string `json:"username"` Password string `json:"password"` + Hash string `json:"hash"` Tier string `json:"tier"` // Do not add 'role' here. We don't want to add admins via the API. } diff --git a/user/manager.go b/user/manager.go index d691d42f..59c8d51f 100644 --- a/user/manager.go +++ b/user/manager.go @@ -868,10 +868,8 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err if !AllowedUsername(username) || !AllowedRole(role) { return ErrInvalidArgument } - var hash []byte var err error = nil - if hashed { hash = []byte(password) } else { @@ -880,7 +878,6 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err return err } } - userID := util.RandomStringPrefix(userIDPrefix, userIDLength) syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil { From eecd3245f0871e5327fe52819a9ef00ea843fb4c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 24 May 2025 09:36:16 -0400 Subject: [PATCH 109/378] Release notes --- docs/releases.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index 959fbb83..08a66752 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1381,7 +1381,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii)) * Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) * Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) -* You can now change passwords via the accounts API (thanks to [@wunter8](https://github.com/wunter8) for implementing) +* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@wunter8](https://github.com/wunter8) for implementing) +* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) **Bug fixes + maintenance:** From f4c37ccfb965f4af2efbed37c2f3fe735c2c286d Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 24 May 2025 14:22:02 -0400 Subject: [PATCH 110/378] Bump VIte --- docs/releases.md | 1 + go.mod | 6 +- go.sum | 12 +-- web/package-lock.json | 213 ++++++++++++++++++++---------------------- 4 files changed, 110 insertions(+), 122 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 08a66752..b6f6be19 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1387,6 +1387,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** * Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) +* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot) * Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!) * Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) * Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) diff --git a/go.mod b/go.mod index 05315a6c..0e1533d9 100644 --- a/go.mod +++ b/go.mod @@ -83,9 +83,9 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.36.0 // indirect diff --git a/go.sum b/go.sum index 6a26dec4..e1d6b2f0 100644 --- a/go.sum +++ b/go.sum @@ -157,12 +157,12 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= diff --git a/web/package-lock.json b/web/package-lock.json index 7a08b299..3f428c2e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.4.2", - "@mui/material": "*", + "@mui/material": "latest", "dexie": "^3.2.1", "dexie-react-hooks": "^1.1.1", "humanize-duration": "^3.27.3", @@ -20,8 +20,8 @@ "i18next-browser-languagedetector": "^6.1.4", "i18next-http-backend": "^1.4.0", "js-base64": "^3.7.2", - "react": "*", - "react-dom": "*", + "react": "latest", + "react-dom": "latest", "react-i18next": "^11.16.2", "react-infinite-scroll-component": "^6.1.0", "react-remark": "^2.1.0", @@ -2647,6 +2647,13 @@ "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", @@ -2719,9 +2726,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", - "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", "cpu": [ "arm" ], @@ -2733,9 +2740,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", - "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", "cpu": [ "arm64" ], @@ -2747,9 +2754,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", - "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", "cpu": [ "arm64" ], @@ -2761,9 +2768,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", - "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", "cpu": [ "x64" ], @@ -2775,9 +2782,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", - "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", "cpu": [ "arm64" ], @@ -2789,9 +2796,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", - "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", "cpu": [ "x64" ], @@ -2803,9 +2810,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", - "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", "cpu": [ "arm" ], @@ -2817,9 +2824,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", - "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", "cpu": [ "arm" ], @@ -2831,9 +2838,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", - "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", "cpu": [ "arm64" ], @@ -2845,9 +2852,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", "cpu": [ "arm64" ], @@ -2859,9 +2866,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", - "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", "cpu": [ "loong64" ], @@ -2873,9 +2880,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", - "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", "cpu": [ "ppc64" ], @@ -2887,9 +2894,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", - "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", "cpu": [ "riscv64" ], @@ -2901,9 +2908,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", - "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", "cpu": [ "riscv64" ], @@ -2915,9 +2922,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", - "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", "cpu": [ "s390x" ], @@ -2929,9 +2936,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", "cpu": [ "x64" ], @@ -2943,9 +2950,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", "cpu": [ "x64" ], @@ -2957,9 +2964,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", - "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", "cpu": [ "arm64" ], @@ -2971,9 +2978,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", - "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", "cpu": [ "ia32" ], @@ -2985,9 +2992,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", - "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", "cpu": [ "x64" ], @@ -3086,18 +3093,6 @@ "@types/unist": "^2" } }, - "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -3157,15 +3152,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", + "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -4109,9 +4105,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.157", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz", + "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", "dev": true, "license": "ISC" }, @@ -7385,9 +7381,9 @@ } }, "node_modules/rollup": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", - "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "dev": true, "license": "MIT", "dependencies": { @@ -7401,26 +7397,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.0", - "@rollup/rollup-android-arm64": "4.41.0", - "@rollup/rollup-darwin-arm64": "4.41.0", - "@rollup/rollup-darwin-x64": "4.41.0", - "@rollup/rollup-freebsd-arm64": "4.41.0", - "@rollup/rollup-freebsd-x64": "4.41.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", - "@rollup/rollup-linux-arm-musleabihf": "4.41.0", - "@rollup/rollup-linux-arm64-gnu": "4.41.0", - "@rollup/rollup-linux-arm64-musl": "4.41.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-musl": "4.41.0", - "@rollup/rollup-linux-s390x-gnu": "4.41.0", - "@rollup/rollup-linux-x64-gnu": "4.41.0", - "@rollup/rollup-linux-x64-musl": "4.41.0", - "@rollup/rollup-win32-arm64-msvc": "4.41.0", - "@rollup/rollup-win32-ia32-msvc": "4.41.0", - "@rollup/rollup-win32-x64-msvc": "4.41.0", + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" } }, @@ -8280,15 +8276,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", From 6fe3913aeedad166fab313c2baf41960d85296bc Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 24 May 2025 15:26:25 -0400 Subject: [PATCH 111/378] Increase Web Push expiration to 55/60 days, update configs --- docs/config.md | 14 +++++++++----- docs/releases.md | 2 ++ server/config.go | 4 ++-- server/webpush_store.go | 9 +++++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/config.md b/docs/config.md index e1808fc9..2cfda9a6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -878,8 +878,8 @@ a database to keep track of the browser's subscriptions, and an admin email addr - `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` - `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com` - `web-push-startup-queries` is an optional list of queries to run on startup` -- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `7d`) -- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `9d`) +- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `55d`) +- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `60d`) Limitations: @@ -906,8 +906,8 @@ web-push-file: /var/cache/ntfy/webpush.db web-push-email-address: sysadmin@example.com ``` -The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days, -and will automatically expire after 9 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), +The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 55 days, +and will automatically expire after 60 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), subscriptions are also removed automatically. The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription @@ -1382,7 +1382,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 | | `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | | `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | -| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). | +| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). | | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | | `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) | @@ -1435,6 +1435,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions | | `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address | | `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup | +| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 60d | Web Push: Duration after which a subscription is considered stale and will be deleted. This is to prevent stale subscriptions. | +| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 55d | Web Push: Duration after which a warning is sent to subscribers that their subscription will expire soon. This is to prevent stale subscriptions. | | `log-format` | `NTFY_LOG_FORMAT` | *string* | `text` | Defines the output format, can be text or json | | `log-file` | `NTFY_LOG_FILE` | *string* | - | Defines the filename to write logs to. If this is not set, ntfy logs to stderr | | `log-level` | `NTFY_LOG_LEVEL` | *string* | `info` | Defines the default log level, can be one of trace, debug, info, warn or error | @@ -1537,5 +1539,7 @@ OPTIONS: --web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE] --web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS] --web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES] + --web-push-expiry-duration value, --web_push_expiry_duration value automatically expire unused subscriptions after this time (default: "60d") [$NTFY_WEB_PUSH_EXPIRY_DURATION] + --web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value send web push warning notification after this time before expiring unused subscriptions (default: "55d") [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION] --help, -h show help ``` diff --git a/docs/releases.md b/docs/releases.md index b6f6be19..dabe9c2b 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1383,6 +1383,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) * Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@wunter8](https://github.com/wunter8) for implementing) * You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) +* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29)) **Bug fixes + maintenance:** @@ -1395,6 +1396,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) * WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) * Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) +* Make sure WebPush subscription topics are actually deleted (no ticket) **Documentation:** diff --git a/server/config.go b/server/config.go index 7267ce9d..877b249a 100644 --- a/server/config.go +++ b/server/config.go @@ -26,8 +26,8 @@ const ( // Defines default Web Push settings const ( - DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour - DefaultWebPushExpiryDuration = 9 * 24 * time.Hour + DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour + DefaultWebPushExpiryDuration = 60 * 24 * time.Hour ) // Defines all global and per-visitor limits diff --git a/server/webpush_store.go b/server/webpush_store.go index 62a35f7d..db0304be 100644 --- a/server/webpush_store.go +++ b/server/webpush_store.go @@ -79,8 +79,9 @@ const ( deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscription WHERE user_id = ?` deleteWebPushSubscriptionByAgeQuery = `DELETE FROM subscription WHERE updated_at <= ?` // Full table scan! - insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)` - deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?` + insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)` + deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?` + deleteWebPushSubscriptionTopicWithoutSubscription = `DELETE FROM subscription_topic WHERE subscription_id NOT IN (SELECT id FROM subscription)` ) // Schema management queries @@ -271,6 +272,10 @@ func (c *webPushStore) RemoveSubscriptionsByUserID(userID string) error { // RemoveExpiredSubscriptions removes all subscriptions that have not been updated for a given time period func (c *webPushStore) RemoveExpiredSubscriptions(expireAfter time.Duration) error { _, err := c.db.Exec(deleteWebPushSubscriptionByAgeQuery, time.Now().Add(-expireAfter).Unix()) + if err != nil { + return err + } + _, err = c.db.Exec(deleteWebPushSubscriptionTopicWithoutSubscription) return err } From df7dd9c498086617f5350c1da042a0d37ada825a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 24 May 2025 15:55:02 -0400 Subject: [PATCH 112/378] Fix weebpush test --- server/server_webpush_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go index ab7a20c4..f7379511 100644 --- a/server/server_webpush_test.go +++ b/server/server_webpush_test.go @@ -212,7 +212,7 @@ func TestServer_WebPush_Expiry(t *testing.T) { addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") requireSubscriptionCount(t, s, "test-topic", 1) - _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix()) + _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-55*24*time.Hour).Unix()) require.Nil(t, err) s.pruneAndNotifyWebPushSubscriptions() @@ -222,7 +222,7 @@ func TestServer_WebPush_Expiry(t *testing.T) { return received.Load() }) - _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix()) + _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-60*24*time.Hour).Unix()) require.Nil(t, err) s.pruneAndNotifyWebPushSubscriptions() From 5eb84f759bdf3468a86601eab7d6dd70c7dff81c Mon Sep 17 00:00:00 2001 From: leukosaima <187358+leukosaima@users.noreply.github.com> Date: Sun, 25 May 2025 00:20:55 -0400 Subject: [PATCH 113/378] Add ntfyrr project --- docs/integrations.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/integrations.md b/docs/integrations.md index 5e550b53..7452c70d 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -45,7 +45,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Watchtower](https://containrrr.dev/watchtower/) ⭐ - Automating Docker container base image updates (see [integration example](examples.md#watchtower-shoutrrr)) - [Jellyfin](https://jellyfin.org/) ⭐ - The Free Software Media System (see [integration example](examples.md#)) -- [Overseer](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook)) +- [Overseerr](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook)) - [Tautulli](https://github.com/Tautulli/Tautulli) ⭐ - Monitoring and tracking tool for Plex (integration [via webhook](https://github.com/Tautulli/Tautulli/wiki/Notification-Agents-Guide#webhook)) - [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy)) - [Proxmox-Ntfy](https://github.com/qtsone/proxmox-ntfy) - Python script that monitors Proxmox tasks and sends notifications using the Ntfy service. @@ -170,6 +170,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript) - [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) - [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell) +- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. ## Blog + forum posts From f40023aa232696637b0cd50d08b4f146091d0013 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 12:09:57 -0400 Subject: [PATCH 114/378] APNs fix --- server/server_firebase.go | 97 +++++++++++++++++----------------- server/server_firebase_test.go | 2 + 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/server/server_firebase.go b/server/server_firebase.go index 2b01add2..99f1fb28 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -138,41 +138,7 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro } apnsConfig = createAPNSAlertConfig(m, data) case messageEvent: - allowForward := true if auther != nil { - allowForward = auther.Authorize(nil, m.Topic, user.PermissionRead) == nil - } - if allowForward { - data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": m.Event, - "topic": m.Topic, - "priority": fmt.Sprintf("%d", m.Priority), - "tags": strings.Join(m.Tags, ","), - "click": m.Click, - "icon": m.Icon, - "title": m.Title, - "message": m.Message, - "content_type": m.ContentType, - "encoding": m.Encoding, - } - if len(m.Actions) > 0 { - actions, err := json.Marshal(m.Actions) - if err != nil { - return nil, err - } - data["actions"] = string(actions) - } - if m.Attachment != nil { - data["attachment_name"] = m.Attachment.Name - data["attachment_type"] = m.Attachment.Type - data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size) - data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) - data["attachment_url"] = m.Attachment.URL - } - apnsConfig = createAPNSAlertConfig(m, data) - } else { // If "anonymous read" for a topic is not allowed, we cannot send the message along // via Firebase. Instead, we send a "poll_request" message, asking the client to poll. // @@ -180,23 +146,42 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro // fields are set, the iOS app fails to decode the message. // // See https://github.com/binwiederhier/ntfy/pull/1345 - data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": pollRequestEvent, - "topic": m.Topic, - "priority": fmt.Sprintf("%d", m.Priority), - "tags": "", - "click": "", - "icon": "", - "title": "", - "message": newMessageBody, - "content_type": m.ContentType, - "encoding": m.Encoding, - "poll_id": m.ID, + if err := auther.Authorize(nil, m.Topic, user.PermissionRead); err != nil { + m = toPollRequest(m) } - apnsConfig = createAPNSAlertConfig(m, data) } + data = map[string]string{ + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": m.Event, + "topic": m.Topic, + "priority": fmt.Sprintf("%d", m.Priority), + "tags": strings.Join(m.Tags, ","), + "click": m.Click, + "icon": m.Icon, + "title": m.Title, + "message": m.Message, + "content_type": m.ContentType, + "encoding": m.Encoding, + } + if len(m.Actions) > 0 { + actions, err := json.Marshal(m.Actions) + if err != nil { + return nil, err + } + data["actions"] = string(actions) + } + if m.Attachment != nil { + data["attachment_name"] = m.Attachment.Name + data["attachment_type"] = m.Attachment.Type + data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size) + data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) + data["attachment_url"] = m.Attachment.URL + } + if m.PollID != "" { + data["poll_id"] = m.PollID + } + apnsConfig = createAPNSAlertConfig(m, data) } var androidConfig *messaging.AndroidConfig if m.Priority >= 4 { @@ -290,3 +275,17 @@ func maybeTruncateAPNSBodyMessage(s string) string { } return s } + +// toPollRequest converts a message to a poll request message. +// +// This empties all the fields that are not needed for a poll request and just sets the required fields, +// most importantly, the PollID. +func toPollRequest(m *message) *message { + pr := newPollRequestMessage(m.Topic, m.ID) + pr.ID = m.ID + pr.Time = m.Time + pr.Priority = m.Priority // Keep priority + pr.ContentType = m.ContentType + pr.Encoding = m.Encoding + return pr +} diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 8d88fcf0..2f5b7287 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -240,6 +240,8 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) { "content_type": "", "poll_id": m.ID, }, fbm.Data) + require.Equal(t, "", fbm.APNS.Payload.Aps.Alert.Title) + require.Equal(t, "New message", fbm.APNS.Payload.Aps.Alert.Body) } func TestToFirebaseMessage_PollRequest(t *testing.T) { From 0de1990c0115f04620f73b9106830397fc8f4f64 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 12:23:02 -0400 Subject: [PATCH 115/378] Increase number of access tokens per user to 60 --- docs/config.md | 2 +- docs/releases.md | 1 + user/manager.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index 2cfda9a6..d68ddb27 100644 --- a/docs/config.md +++ b/docs/config.md @@ -295,7 +295,7 @@ want to use a dedicated token to publish from your backup host, and one from you but not yet implemented. The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire -automatically (or never expire). Each user can have up to 20 tokens (hardcoded). +automatically (or never expire). Each user can have up to 60 tokens (hardcoded). **Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details): ``` diff --git a/docs/releases.md b/docs/releases.md index dabe9c2b..1ef7235c 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1397,6 +1397,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) * Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) * Make sure WebPush subscription topics are actually deleted (no ticket) +* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308)) **Documentation:** diff --git a/user/manager.go b/user/manager.go index 59c8d51f..814ee827 100644 --- a/user/manager.go +++ b/user/manager.go @@ -28,7 +28,7 @@ const ( userHardDeleteAfterDuration = 7 * 24 * time.Hour tokenPrefix = "tk_" tokenLength = 32 - tokenMaxCount = 20 // Only keep this many tokens in the table per user + tokenMaxCount = 60 // Only keep this many tokens in the table per user tag = "user_manager" ) From 42af71e546eb99f5385958d4faea14ca2ead67ec Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 12:27:21 -0400 Subject: [PATCH 116/378] Fix test --- user/manager_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user/manager_test.go b/user/manager_test.go index c81b8cab..35994b47 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -678,10 +678,10 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { require.NotEmpty(t, token.Value) philTokens = append(philTokens, token.Value) - // Create 22 tokens for ben (only 20 allowed!) + // Create 62 tokens for ben (only 60 allowed!) baseTime := time.Now().Add(24 * time.Hour) benTokens := make([]string, 0) - for i := 0; i < 22; i++ { // + for i := 0; i < 62; i++ { // token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) require.Nil(t, err) require.NotEmpty(t, token.Value) @@ -700,7 +700,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { require.Equal(t, ErrUnauthenticated, err) // Ben: The other tokens should still work - for i := 2; i < 22; i++ { + for i := 2; i < 62; i++ { userWithToken, err := a.AuthenticateToken(benTokens[i]) require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i]) require.Equal(t, "ben", userWithToken.Name) @@ -720,7 +720,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { require.Nil(t, err) require.True(t, rows.Next()) require.Nil(t, rows.Scan(&benCount)) - require.Equal(t, 20, benCount) + require.Equal(t, 60, benCount) var philCount int rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID) From 9f72eb804d393f89a3dba57098a9c7089c10edc4 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 12:32:16 -0400 Subject: [PATCH 117/378] Computers are fast --- user/manager_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/manager_test.go b/user/manager_test.go index 35994b47..89f35e3c 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -14,7 +14,7 @@ import ( "time" ) -const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources +const minBcryptTimingMillis = int64(40) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources func TestManager_FullScenario_Default_DenyAll(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) From 7f86108379dd6c1163fb0c1f68016edcddb4ea66 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 12:57:02 -0400 Subject: [PATCH 118/378] Update docs --- docs/releases.md | 1 + docs/subscribe/cli.md | 44 ++++++++++++++++++++++++++++++++----------- scripts/postinst.sh | 11 ----------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 1ef7235c..179c9d6a 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1384,6 +1384,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@wunter8](https://github.com/wunter8) for implementing) * You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) * Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29)) +* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch)) **Bug fixes + maintenance:** diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 819ff6ab..78e160c8 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -190,6 +190,10 @@ Here's an example config file that subscribes to three different topics, executi === "~/.config/ntfy/client.yml (Linux)" ```yaml + default-host: https://ntfy.sh + default-user: phill + default-password: mypass + subscribe: - topic: echo-this command: 'echo "Message received: $message"' @@ -210,9 +214,12 @@ Here's an example config file that subscribes to three different topics, executi fi ``` - === "~/Library/Application Support/ntfy/client.yml (macOS)" ```yaml + default-host: https://ntfy.sh + default-user: phill + default-password: mypass + subscribe: - topic: echo-this command: 'echo "Message received: $message"' @@ -226,6 +233,10 @@ Here's an example config file that subscribes to three different topics, executi === "%AppData%\ntfy\client.yml (Windows)" ```yaml + default-host: https://ntfy.sh + default-user: phill + default-password: mypass + subscribe: - topic: echo-this command: 'echo Message received: %message%' @@ -264,19 +275,30 @@ will be used, otherwise, the subscription settings will override the defaults. ### Using the systemd service You can use the `ntfy-client` systemd services to subscribe to multiple topics just like in the example above. -You have the option of either enabling `ntfy-client` as a system service (see -[here](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service)) -or user service (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/user/ntfy-client.service)). -The services are automatically installed (but not started) if you install the deb/rpm/AUR package. -The system service ensures that ntfy is run at startup (useful for servers), -while the user service starts ntfy only after the user has logged in. The user service is recommended for personal machine use. -To configure `ntfy-client` as a system service it, edit `/etc/ntfy/client.yml` and run `sudo systemctl restart ntfy-client`. +You have the option of either enabling `ntfy-client` as a **system service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service)) +or **user service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/user/ntfy-client.service)). Neither system service nor user service are enabled or started by default, so you have to do that yourself. -To configure `ntfy-client` as a user service it, edit `~/.config/ntfy/client.yml` and run `systemctl --user restart ntfy-client` (without sudo). +**System service:** The `ntfy-client` systemd system service runs as the `ntfy` user. When enabled, it is started at system boot. To configure it as a system +service, edit `/etc/ntfy/client.yml` and then enable/start the service (as root), like so: -!!! info - The system service runs as user `ntfy`, meaning that typical Linux permission restrictions apply. It also means that the system service cannot run commands in your X session as the primary machine user (unlike the user service). +``` +sudo systemctl enable ntfy-client +sudo systemctl restart ntfy-client +``` + +The system service runs as user `ntfy`, meaning that typical Linux permission restrictions apply. It also means that the system service cannot run commands in your X session as the primary machine user (unlike the user service). + +**User service:** The `ntfy-client` user service is run when the user logs into their desktop environment. To enable/start it, edit `~/.config/ntfy/client.yml` and +run the following commands (without sudo!): + +``` +systemctl --user enable ntfy-client +systemctl --user restart ntfy-client +``` + +Unlike the system service, the user service can interact with the user's desktop environment, and run commands like `notify-send` to display desktop notifications. +It can also run commands that require access to the user's home directory, such as `gnome-calculator`. ### Authentication Depending on whether the server is configured to support [access control](../config.md#access-control), some topics diff --git a/scripts/postinst.sh b/scripts/postinst.sh index 6e706205..d923e7f8 100755 --- a/scripts/postinst.sh +++ b/scripts/postinst.sh @@ -40,16 +40,5 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then systemctl restart ntfy-client.service >/dev/null || true fi fi - - # inform user about systemd user service - echo - echo "------------------------------------------------------------------------" - echo "ntfy includes a systemd user service." - echo "To enable it, run following commands as your regular user (not as root):" - echo - echo " systemctl --user enable ntfy-client.service" - echo " systemctl --user start ntfy-client.service" - echo "------------------------------------------------------------------------" - echo fi fi From 635ec88c4fc769911c3aaf5de5fdccdf53612ecd Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 15:28:59 -0400 Subject: [PATCH 119/378] Update release log --- docs/releases.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/releases.md b/docs/releases.md index 179c9d6a..dd7cecb0 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1399,6 +1399,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) * Make sure WebPush subscription topics are actually deleted (no ticket) * Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308)) +* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler)) **Documentation:** From af176610532142682c2d2b3670cacb108d3fb583 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 25 May 2025 20:13:13 -0400 Subject: [PATCH 120/378] Typos, server.yml additions --- docs/releases.md | 6 +++--- server/server.yml | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index dd7cecb0..292f8803 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1381,10 +1381,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii)) * Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) * Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) -* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@wunter8](https://github.com/wunter8) for implementing) + * Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing) * You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) * Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29)) -* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch)) +* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch)) **Bug fixes + maintenance:** @@ -1419,7 +1419,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) * Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode)) * Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) -* Lots of other tiny docs updates, tanks to everyone who contributed! +* Lots of other tiny docs updates, thanks to everyone who contributed! **Languages** diff --git a/server/server.yml b/server/server.yml index 7329d37e..bb508cb4 100644 --- a/server/server.yml +++ b/server/server.yml @@ -146,7 +146,7 @@ # Web Push support (background notifications for browsers) # -# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users +# If enabled, allows the ntfy web app to receive push notifications, even when the web app is closed. When enabled, users # can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push # endpoint, which will then forward it to the browser. # @@ -155,15 +155,19 @@ # # - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 # - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 -# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` -# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com` +# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. /var/cache/ntfy/webpush.db +# - web-push-email-address is the admin email address send to the push provider, e.g. sysadmin@example.com # - web-push-startup-queries is an optional list of queries to run on startup` +# - web-push-expiry-warning-duration defines the duration after which unused subscriptions are sent a warning (default is 55d`) +# - web-push-expiry-duration defines the duration after which unused subscriptions will expire (default is 60d) # # web-push-public-key: # web-push-private-key: # web-push-file: # web-push-email-address: # web-push-startup-queries: +# web-push-expiry-warning-duration: "55d" +# web-push-expiry-duration: "60d" # If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header. # From b4f15ec9d4d4f69f5cb0e07605079a7f170626f2 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 26 May 2025 13:41:26 -0400 Subject: [PATCH 121/378] Integrations --- docs/integrations.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/integrations.md b/docs/integrations.md index 7452c70d..c7da21f9 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -40,6 +40,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring - [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool - [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. +- [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader +- [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform ## Integration via HTTP/SMTP/etc. @@ -83,7 +85,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had ## CLIs + GUIs - [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events -- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy +- [ntfy-desktop](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy +- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications - [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte - [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic - [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop @@ -174,6 +177,15 @@ I've added a ⭐ to projects or posts that have a significant following, or had ## Blog + forum posts +- [Device notifications via HTTP with ntfy](https://alistairshepherd.uk/writing/ntfy/) - alistairshepherd.uk - 6/2025 +- [Notifications about (almost) anything with ntfy.sh](https://hamatti.org/posts/notifications-about-almost-anything-with-ntfy-sh/) - hamatti.org - 6/2025 +- [I set up a self-hosted notification service for everything, and I'll never look back](https://www.xda-developers.com/set-up-self-hosted-notification-service/) ⭐ - xda-developers.com - 5/2025 +- [How to Set Up Ntfy: Self-Hosted Push Notifications Made Easy](https://www.youtube.com/watch?v=wDJDiAYZ3H0) - youtube.com (sass drew) - 1/2025 +- [The NTFY is a game-changer FREE solution for IT people](https://www.youtube.com/watch?v=NtlztHT-sRw) - youtube.com (Valters Tech Turf) - 1/2025 +- [Notify: A Powerful Tool for Real-Time Notifications (ntfy.sh)](https://www.youtube.com/watch?v=XXTTeVfGBz0) - youtube.com (LinuxCloudHacks) - 12/2025 +- [Push notifications with ntfy and n8n](https://www.youtube.com/watch?v=DKG1R3xYvwQ) - youtube.com (Oskar) - 10/2024 +- [Setup ntfy for selfhosted notifications with Cloudflare Tunnel](https://medium.com/@svenvanginkel/setup-ntfy-for-selfhosted-notifications-with-cloudflare-tunnel-e342f470177d) - medium.com (Sven van Ginkel) - 10/2024 +- [Self-Host NTFY - How It Works & Easy Setup Guide](https://www.youtube.com/watch?v=79wHc_jfrJE) ⭐ - youtube.com (Techdox)- 9/2024 - [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024 - [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024 - [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024 From 061677a78be6a365a90de29b9b190cbcf3d8c49c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 29 May 2025 20:14:54 -0400 Subject: [PATCH 122/378] Bump version --- docs/install.md | 60 ++++++++++++------------- docs/releases.md | 113 +++++++++++++++++++++++++---------------------- 2 files changed, 90 insertions(+), 83 deletions(-) diff --git a/docs/install.md b/docs/install.md index 45870505..e71bac52 100644 --- a/docs/install.md +++ b/docs/install.md @@ -30,37 +30,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.tar.gz - tar zxvf ntfy_2.11.0_linux_amd64.tar.gz - sudo cp -a ntfy_2.11.0_linux_amd64/ntfy /usr/local/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_amd64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.tar.gz + tar zxvf ntfy_2.12.0_linux_amd64.tar.gz + sudo cp -a ntfy_2.12.0_linux_amd64/ntfy /usr/local/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.tar.gz - tar zxvf ntfy_2.11.0_linux_armv6.tar.gz - sudo cp -a ntfy_2.11.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.tar.gz + tar zxvf ntfy_2.12.0_linux_armv6.tar.gz + sudo cp -a ntfy_2.12.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.tar.gz - tar zxvf ntfy_2.11.0_linux_armv7.tar.gz - sudo cp -a ntfy_2.11.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.tar.gz + tar zxvf ntfy_2.12.0_linux_armv7.tar.gz + sudo cp -a ntfy_2.12.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.tar.gz - tar zxvf ntfy_2.11.0_linux_arm64.tar.gz - sudo cp -a ntfy_2.11.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.tar.gz + tar zxvf ntfy_2.12.0_linux_arm64.tar.gz + sudo cp -a ntfy_2.12.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -110,7 +110,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -118,7 +118,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -126,7 +126,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -134,7 +134,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -144,28 +144,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -195,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. -To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_darwin_all.tar.gz), +To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz), extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_darwin_all.tar.gz > ntfy_2.11.0_darwin_all.tar.gz -tar zxvf ntfy_2.11.0_darwin_all.tar.gz -sudo cp -a ntfy_2.11.0_darwin_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz > ntfy_2.12.0_darwin_all.tar.gz +tar zxvf ntfy_2.12.0_darwin_all.tar.gz +sudo cp -a ntfy_2.12.0_darwin_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_2.11.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_2.12.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -224,7 +224,7 @@ brew install ntfy ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_windows_amd64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). diff --git a/docs/releases.md b/docs/releases.md index 292f8803..5004b866 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,66 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). +### ntfy server v2.12.0 +Released May 29, 2025 + +This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few +new features and bug fixes as well. + +Thanks to everyone who contributed to this release, and special thanks to [@wunter8](https://github.com/wunter8) for his continued +user support in Discord/Matrix/GitHub! You rock, man! + +**Features:** + +* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) +* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii)) +* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) +* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) +* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing) +* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) +* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29)) +* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch)) + +**Bug fixes + maintenance:** + +* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) +* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot) +* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!) +* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) +* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) +* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) +* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) +* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) +* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) +* Make sure WebPush subscription topics are actually deleted (no ticket) +* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308)) +* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler)) + +**Documentation:** + +* Lots of new integrations and projects. Amazing! + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [UptimeObserver](https://uptimeobserver.com) + * [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) + * [Monibot](https://monibot.io/) + * [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) + * [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) + * [ntfy-run](https://github.com/quantum5/ntfy-run) + * [Clipboard IO](https://github.com/jim3692/clipboard-io) + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) +* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) +* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) +* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) +* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode)) +* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) +* Lots of other tiny docs updates, thanks to everyone who contributed! + +**Languages** + +* Update new languages from Weblate. Thanks to all the contributors! +* Added Tamil (தமிழ்) as a new language to the web app + ### ntfy server v2.11.0 Released May 13, 2024 @@ -1373,59 +1433,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy server v2.12.0 (UNRELEASED) - -**Features:** - -* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) -* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii)) -* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) -* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) - * Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing) -* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) -* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29)) -* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch)) - -**Bug fixes + maintenance:** - -* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) -* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot) -* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!) -* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) -* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) -* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) -* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) -* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) -* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) -* Make sure WebPush subscription topics are actually deleted (no ticket) -* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308)) -* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler)) - -**Documentation:** - -* Lots of new integrations and projects. Amazing! - * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - * [UptimeObserver](https://uptimeobserver.com) - * [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) - * [Monibot](https://monibot.io/) - * [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - * [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - * [ntfy-run](https://github.com/quantum5/ntfy-run) - * [Clipboard IO](https://github.com/jim3692/clipboard-io) - * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - * [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) -* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) -* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) -* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) -* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode)) -* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) -* Lots of other tiny docs updates, thanks to everyone who contributed! - -**Languages** - -* Update new languages from Weblate. Thanks to all the contributors! -* Added Tamil (தமிழ்) as a new language to the web app - ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** From dc797f8594b32933b90fc7f0af69d7dd2a560ef9 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 29 May 2025 20:35:22 -0400 Subject: [PATCH 123/378] Fix release noteFix release notess --- docs/releases.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 5004b866..a1035310 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -40,16 +40,16 @@ user support in Discord/Matrix/GitHub! You rock, man! **Documentation:** * Lots of new integrations and projects. Amazing! - * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - * [UptimeObserver](https://uptimeobserver.com) - * [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) - * [Monibot](https://monibot.io/) - * [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - * [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - * [ntfy-run](https://github.com/quantum5/ntfy-run) - * [Clipboard IO](https://github.com/jim3692/clipboard-io) - * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - * [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [UptimeObserver](https://uptimeobserver.com) + * [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) + * [Monibot](https://monibot.io/) + * [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) + * [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) + * [ntfy-run](https://github.com/quantum5/ntfy-run) + * [Clipboard IO](https://github.com/jim3692/clipboard-io) + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) * Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) * Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) * Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) From 2cb4d089abdb1ac4a3161b729040ce35b9a0d27c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 31 May 2025 15:33:21 -0400 Subject: [PATCH 124/378] Merge client ip header --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/test.yaml | 2 +- .gitignore | 1 + .goreleaser.yml | 15 + Dockerfile-build | 5 +- client/user/ntfy-client.service | 10 + cmd/serve.go | 22 +- cmd/user.go | 37 +- cmd/webpush.go | 23 +- cmd/webpush_test.go | 7 + docs/config.md | 21 +- docs/develop.md | 2 +- docs/examples.md | 59 + docs/install.md | 62 +- docs/integrations.md | 43 +- docs/publish.md | 6 +- docs/releases.md | 80 +- .../img/mobile-screenshot-notification.png | Bin 0 -> 72277 bytes docs/subscribe/api.md | 12 +- docs/subscribe/cli.md | 61 +- go.mod | 116 +- go.sum | 308 +- log/log.go | 2 +- main.go | 2 +- scripts/postinst.sh | 2 +- server/config.go | 10 +- server/message_cache.go | 17 + server/message_cache_test.go | 5 + server/server.go | 34 +- server/server.yml | 10 +- server/server_account.go | 4 +- server/server_account_test.go | 24 +- server/server_admin.go | 59 +- server/server_admin_test.go | 244 +- server/server_firebase.go | 99 +- server/server_firebase_test.go | 23 +- server/server_payments_test.go | 18 +- server/server_test.go | 84 +- server/server_twilio_test.go | 8 +- server/server_webpush_test.go | 8 +- server/types.go | 16 +- server/util.go | 58 +- server/webpush_store.go | 9 +- user/manager.go | 31 +- user/manager_test.go | 84 +- web/package-lock.json | 5517 ++++++++++------- web/package.json | 4 +- web/public/static/langs/ar.json | 3 +- web/public/static/langs/bg.json | 2 +- web/public/static/langs/bn.json | 1 + web/public/static/langs/de.json | 24 +- web/public/static/langs/et.json | 79 +- web/public/static/langs/fa.json | 24 +- web/public/static/langs/fi.json | 12 +- web/public/static/langs/gl.json | 24 +- web/public/static/langs/hu.json | 40 +- web/public/static/langs/id.json | 2 +- web/public/static/langs/it.json | 89 +- web/public/static/langs/ja.json | 31 +- web/public/static/langs/nb_NO.json | 216 +- web/public/static/langs/pl.json | 7 +- web/public/static/langs/pt.json | 63 +- web/public/static/langs/pt_BR.json | 127 +- web/public/static/langs/ro.json | 117 +- web/public/static/langs/ru.json | 98 +- web/public/static/langs/sq.json | 63 + web/public/static/langs/ta.json | 407 ++ web/public/static/langs/uk.json | 27 +- web/public/static/langs/vi.json | 16 +- web/src/components/Notifications.jsx | 1 + web/src/components/Preferences.jsx | 1 + 72 files changed, 5585 insertions(+), 3157 deletions(-) create mode 100644 client/user/ntfy-client.service create mode 100644 docs/static/img/mobile-screenshot-notification.png create mode 100644 web/public/static/langs/bn.json create mode 100644 web/public/static/langs/sq.json create mode 100644 web/public/static/langs/ta.json diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b6dc8ddb..72b9e360 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.22.x' + go-version: '1.24.x' - name: Install node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 80155e5b..70a70552 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,7 +12,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.22.x' + go-version: '1.24.x' - name: Install node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b0f99ffd..cfd9d754 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,7 +9,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.22.x' + go-version: '1.24.x' - name: Install node uses: actions/setup-node@v3 with: diff --git a/.gitignore b/.gitignore index 7cbb52ac..cf10bc33 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ node_modules/ __pycache__ web/dev-dist/ venv/ +cmd/key-file.yaml diff --git a/.goreleaser.yml b/.goreleaser.yml index 062cce1f..fa423a86 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -90,6 +90,8 @@ nfpms: type: "config|noreplace" - src: client/ntfy-client.service dst: /lib/systemd/system/ntfy-client.service + - src: client/user/ntfy-client.service + dst: /lib/systemd/user/ntfy-client.service - dst: /var/cache/ntfy type: dir - dst: /var/cache/ntfy/attachments @@ -119,6 +121,7 @@ archives: - server/ntfy.service - client/client.yml - client/ntfy-client.service + - client/user/ntfy-client.service - id: ntfy_windows builds: @@ -197,3 +200,15 @@ docker_manifests: - *arm64v8_image - *armv7_image - *armv6_image + - name_template: "binwiederhier/ntfy:v{{ .Major }}" + image_templates: + - *amd64_image + - *arm64v8_image + - *armv7_image + - *armv6_image + - name_template: "binwiederhier/ntfy:v{{ .Major }}.{{ .Minor }}" + image_templates: + - *amd64_image + - *arm64v8_image + - *armv7_image + - *armv6_image \ No newline at end of file diff --git a/Dockerfile-build b/Dockerfile-build index 4530ec47..78f2d5d9 100644 --- a/Dockerfile-build +++ b/Dockerfile-build @@ -1,4 +1,4 @@ -FROM golang:1.22-bullseye as builder +FROM golang:1.24-bullseye as builder ARG VERSION=dev ARG COMMIT=unknown @@ -44,6 +44,8 @@ RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server FROM alpine +ARG VERSION=dev + LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com" LABEL org.opencontainers.image.url="https://ntfy.sh/" LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/" @@ -52,6 +54,7 @@ LABEL org.opencontainers.image.vendor="Philipp C. Heckel" LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0" LABEL org.opencontainers.image.title="ntfy" LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST" +LABEL org.opencontainers.image.version="$VERSION" COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy diff --git a/client/user/ntfy-client.service b/client/user/ntfy-client.service new file mode 100644 index 00000000..0a9598ee --- /dev/null +++ b/client/user/ntfy-client.service @@ -0,0 +1,10 @@ +[Unit] +Description=ntfy client +After=network.target + +[Service] +ExecStart=/usr/bin/ntfy subscribe --config "%h/.config/ntfy/client.yml" --from-config +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/cmd/serve.go b/cmd/serve.go index b8004a56..51465c9d 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -101,6 +101,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}), ) var cmdServe = &cli.Command{ @@ -141,6 +143,8 @@ func execServe(c *cli.Context) error { webPushFile := c.String("web-push-file") webPushEmailAddress := c.String("web-push-email-address") webPushStartupQueries := c.String("web-push-startup-queries") + webPushExpiryDurationStr := c.String("web-push-expiry-duration") + webPushExpiryWarningDurationStr := c.String("web-push-expiry-warning-duration") cacheFile := c.String("cache-file") cacheDurationStr := c.String("cache-duration") cacheStartupQueries := c.String("cache-startup-queries") @@ -228,6 +232,14 @@ func execServe(c *cli.Context) error { if err != nil { return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr) } + webPushExpiryDuration, err := util.ParseDuration(webPushExpiryDurationStr) + if err != nil { + return fmt.Errorf("invalid web push expiry duration: %s", webPushExpiryDurationStr) + } + webPushExpiryWarningDuration, err := util.ParseDuration(webPushExpiryWarningDurationStr) + if err != nil { + return fmt.Errorf("invalid web push expiry warning duration: %s", webPushExpiryWarningDurationStr) + } // Convert sizes to bytes messageSizeLimit, err := util.ParseSize(messageSizeLimitStr) @@ -306,6 +318,8 @@ func execServe(c *cli.Context) error { if messageSizeLimit > 5*1024*1024 { return errors.New("message-size-limit cannot be higher than 5M") } + } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { + return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") } // Backwards compatibility @@ -404,7 +418,7 @@ func execServe(c *cli.Context) error { conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.BehindProxy = behindProxy - conf.ProxyClientIPHeader = proxyClientIPHeader + conf.ProxyForwardedHeader = proxyClientIPHeader conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact @@ -420,6 +434,8 @@ func execServe(c *cli.Context) error { conf.WebPushFile = webPushFile conf.WebPushEmailAddress = webPushEmailAddress conf.WebPushStartupQueries = webPushStartupQueries + conf.WebPushExpiryDuration = webPushExpiryDuration + conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration // Set up hot-reloading of config go sigHandlerConfigReload(config) @@ -427,9 +443,9 @@ func execServe(c *cli.Context) error { // Run server s, err := server.New(conf) if err != nil { - log.Fatal(err.Error()) + log.Fatal("%s", err.Error()) } else if err := s.Run(); err != nil { - log.Fatal(err.Error()) + log.Fatal("%s", err.Error()) } log.Info("Exiting.") return nil diff --git a/cmd/user.go b/cmd/user.go index af3afe54..e6867b11 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -42,7 +42,7 @@ var cmdUser = &cli.Command{ Name: "add", Aliases: []string{"a"}, Usage: "Adds a new user", - UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME", + UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD_HASH=... ntfy user add [--role=admin|user] USERNAME", Action: execUserAdd, Flags: []cli.Flag{ &cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(user.RoleUser), Usage: "user role"}, @@ -55,12 +55,13 @@ granted otherwise by the auth-default-access setting). An admin user has read an topics. Examples: - ntfy user add phil # Add regular user phil - ntfy user add --role=admin phil # Add admin user phil - NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts) + ntfy user add phil # Add regular user phil + ntfy user add --role=admin phil # Add admin user phil + NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts) + NTFY_PASSWORD_HASH=... ntfy user add phil # Add user, using env variable to set password hash (for scripts) -You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if -you are creating users via scripts. +You may set the NTFY_PASSWORD environment variable to pass the password, or NTFY_PASSWORD_HASH to pass +directly the bcrypt hash. This is useful if you are creating users via scripts. `, }, { @@ -79,7 +80,7 @@ Example: Name: "change-pass", Aliases: []string{"chp"}, Usage: "Changes a user's password", - UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME", + UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME\nNTFY_PASSWORD_HASH=... ntfy user change-pass USERNAME", Action: execUserChangePass, Description: `Change the password for the given user. @@ -89,9 +90,10 @@ it twice. Example: ntfy user change-pass phil NTFY_PASSWORD=.. ntfy user change-pass phil + NTFY_PASSWORD_HASH=.. ntfy user change-pass phil -You may set the NTFY_PASSWORD environment variable to pass the new password. This is -useful if you are updating users via scripts. +You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass +directly the bcrypt hash. This is useful if you are updating users via scripts. `, }, @@ -174,7 +176,12 @@ variable to pass the new password. This is useful if you are creating/updating u func execUserAdd(c *cli.Context) error { username := c.Args().Get(0) role := user.Role(c.String("role")) - password := os.Getenv("NTFY_PASSWORD") + password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH") + + if !hashed { + password = os.Getenv("NTFY_PASSWORD") + } + if username == "" { return errors.New("username expected, type 'ntfy user add --help' for help") } else if username == userEveryone || username == user.Everyone { @@ -200,7 +207,7 @@ func execUserAdd(c *cli.Context) error { } password = p } - if err := manager.AddUser(username, password, role); err != nil { + if err := manager.AddUser(username, password, role, hashed); err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role) @@ -230,7 +237,11 @@ func execUserDel(c *cli.Context) error { func execUserChangePass(c *cli.Context) error { username := c.Args().Get(0) - password := os.Getenv("NTFY_PASSWORD") + password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH") + + if !hashed { + password = os.Getenv("NTFY_PASSWORD") + } if username == "" { return errors.New("username expected, type 'ntfy user change-pass --help' for help") } else if username == userEveryone || username == user.Everyone { @@ -249,7 +260,7 @@ func execUserChangePass(c *cli.Context) error { return err } } - if err := manager.ChangePassword(username, password); err != nil { + if err := manager.ChangePassword(username, password, hashed); err != nil { return err } fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username) diff --git a/cmd/webpush.go b/cmd/webpush.go index ec66f083..249f91c8 100644 --- a/cmd/webpush.go +++ b/cmd/webpush.go @@ -4,9 +4,16 @@ package cmd import ( "fmt" + "os" "github.com/SherClockHolmes/webpush-go" "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +var flagsWebPush = append( + []cli.Flag{}, + altsrc.NewStringFlag(&cli.StringFlag{Name: "output-file", Aliases: []string{"f"}, Usage: "write VAPID keys to this file"}), ) func init() { @@ -26,6 +33,7 @@ var cmdWebPush = &cli.Command{ Usage: "Generate VAPID keys to enable browser background push notifications", UsageText: "ntfy webpush keys", Category: categoryServer, + Flags: flagsWebPush, }, }, } @@ -35,7 +43,19 @@ func generateWebPushKeys(c *cli.Context) error { if err != nil { return err } - _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: + + if outputFile := c.String("output-file"); outputFile != "" { + contents := fmt.Sprintf(`--- +web-push-public-key: %s +web-push-private-key: %s +`, publicKey, privateKey) + err = os.WriteFile(outputFile, []byte(contents), 0660) + if err != nil { + return err + } + _, err = fmt.Fprintf(c.App.ErrWriter, "Web Push keys written to %s.\n", outputFile) + } else { + _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: web-push-public-key: %s web-push-private-key: %s @@ -44,5 +64,6 @@ web-push-email-address: See https://ntfy.sh/docs/config/#web-push for details. `, publicKey, privateKey) + } return err } diff --git a/cmd/webpush_test.go b/cmd/webpush_test.go index 51926ca1..01e1a7a1 100644 --- a/cmd/webpush_test.go +++ b/cmd/webpush_test.go @@ -14,6 +14,13 @@ func TestCLI_WebPush_GenerateKeys(t *testing.T) { require.Contains(t, stderr.String(), "Web Push keys generated.") } +func TestCLI_WebPush_WriteKeysToFile(t *testing.T) { + app, _, _, stderr := newTestApp() + require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--output-file=key-file.yaml")) + require.Contains(t, stderr.String(), "Web Push keys written to key-file.yaml") + require.FileExists(t, "key-file.yaml") +} + func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error { webPushArgs := []string{ "ntfy", diff --git a/docs/config.md b/docs/config.md index c92493fb..3c441fc4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -50,6 +50,7 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file: listen-http: ":2586" cache-file: "/var/cache/ntfy/cache.db" attachment-cache-dir: "/var/cache/ntfy/attachments" + behind-proxy: true ``` === "server.yml (ntfy.sh config)" @@ -294,7 +295,7 @@ want to use a dedicated token to publish from your backup host, and one from you but not yet implemented. The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire -automatically (or never expire). Each user can have up to 20 tokens (hardcoded). +automatically (or never expire). Each user can have up to 60 tokens (hardcoded). **Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details): ``` @@ -866,7 +867,7 @@ it'll show `New message` as a popup. ## Web Push [Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030)) allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed. -When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the +When enabled, the user can enable **background notifications** for their topics in the web app under Settings. Once enabled by the user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then forward it to the browser. @@ -877,7 +878,9 @@ a database to keep track of the browser's subscriptions, and an admin email addr - `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 - `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` - `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com` -- `web-push-startup-queries` is an optional list of queries to run on startup` +- `web-push-startup-queries` is an optional list of queries to run on startup` +- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `55d`) +- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `60d`) Limitations: @@ -904,8 +907,8 @@ web-push-file: /var/cache/ntfy/webpush.db web-push-email-address: sysadmin@example.com ``` -The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days, -and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), +The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 55 days, +and will automatically expire after 60 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), subscriptions are also removed automatically. The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription @@ -1380,10 +1383,10 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 | | `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | | `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | -| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | +| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). | | `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | | `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | -| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) | +| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) | | `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) | | `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) | | `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | @@ -1433,6 +1436,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions | | `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address | | `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup | +| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 60d | Web Push: Duration after which a subscription is considered stale and will be deleted. This is to prevent stale subscriptions. | +| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 55d | Web Push: Duration after which a warning is sent to subscribers that their subscription will expire soon. This is to prevent stale subscriptions. | | `log-format` | `NTFY_LOG_FORMAT` | *string* | `text` | Defines the output format, can be text or json | | `log-file` | `NTFY_LOG_FILE` | *string* | - | Defines the filename to write logs to. If this is not set, ntfy logs to stderr | | `log-level` | `NTFY_LOG_LEVEL` | *string* | `info` | Defines the default log level, can be one of trace, debug, info, warn or error | @@ -1535,5 +1540,7 @@ OPTIONS: --web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE] --web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS] --web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES] + --web-push-expiry-duration value, --web_push_expiry_duration value automatically expire unused subscriptions after this time (default: "60d") [$NTFY_WEB_PUSH_EXPIRY_DURATION] + --web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value send web push warning notification after this time before expiring unused subscriptions (default: "55d") [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION] --help, -h show help ``` diff --git a/docs/develop.md b/docs/develop.md index e343503b..43ac2d4f 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -384,7 +384,7 @@ strictly based off of my development on this app. There may be other versions of ### Apple setup !!! info - Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required + Along with this step, the [PLIST Deployment](#plist-config) step is also required for these changes to take effect in the iOS app. 1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add) diff --git a/docs/examples.md b/docs/examples.md index d6f83f30..10bb014a 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -31,6 +31,12 @@ GitHub have been hopeless. In case it ever becomes available, I want to know imm */6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi ``` +You can also use [`ntfy-run`](https://github.com/quantum5/ntfy-run) to send the output of your cronjob in the +notification, so that you know exactly why it failed: + +``` +0 0 * * * ntfy-run -n https://ntfy.sh/backups --success-priority low --failure-tags warning ~/backup-computer +``` ## Low disk space alerts Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but @@ -634,3 +640,56 @@ or by simply providing traccar with a valid username/password combination. phil mypass ``` + +## Terminal Notifications for Long-Running Commands + +This example provides a simple way to send notifications using [ntfy.sh](https://ntfy.sh) when a terminal command completes. It includes success or failure indicators based on the command's exit status. + +Store your ntfy.sh bearer token securely if access control is enabled: + + ```sh + echo "your_bearer_token_here" > ~/.ntfy_token + chmod 600 ~/.ntfy_token + ``` + +Add the following function and alias to your `.bashrc` or `.bash_profile`: + + ```sh + # Function for alert notifications using ntfy.sh + notify_via_ntfy() { + local exit_status=$? # Capture the exit status before doing anything else + local token=$(< ~/.ntfy_token) # Securely read the token + local status_icon="$([ $exit_status -eq 0 ] && echo magic_wand || echo warning)" + local last_command=$(history | tail -n1 | sed -e 's/^[[:space:]]*[0-9]\{1,\}[[:space:]]*//' -e 's/[;&|][[:space:]]*alert$//') + + curl -s -X POST "https://n.example.dev/alerts" \ + -H "Authorization: Bearer $token" \ + -H "Title: Terminal" \ + -H "X-Priority: 3" \ + -H "Tags: $status_icon" \ + -d "Command: $last_command (Exit: $exit_status)" + + echo "Tags: $status_icon" + echo "$last_command (Exit: $exit_status)" + } + + # Add an "alert" alias for long running commands using ntfy.sh + alias alert='notify_via_ntfy' + ``` + +Now you can run any long-running command and append `alert` to notify when it completes: + +```sh +sleep 10; alert +``` +![ntfy notifications on mobile device](static/img/mobile-screenshot-notification.png) + +**Notification Sent** with a success 🪄 (`magic_wand`) or failure ⚠️ (`warning`) tag. + +To test failure notifications: + +```sh +false; alert # Always fails (exit 1) +ls --invalid; alert # Invalid option +cat nonexistent_file; alert # File not found +``` \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index 0c823571..e71bac52 100644 --- a/docs/install.md +++ b/docs/install.md @@ -30,37 +30,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.tar.gz - tar zxvf ntfy_2.11.0_linux_amd64.tar.gz - sudo cp -a ntfy_2.11.0_linux_amd64/ntfy /usr/local/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_amd64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.tar.gz + tar zxvf ntfy_2.12.0_linux_amd64.tar.gz + sudo cp -a ntfy_2.12.0_linux_amd64/ntfy /usr/local/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.tar.gz - tar zxvf ntfy_2.11.0_linux_armv6.tar.gz - sudo cp -a ntfy_2.11.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.tar.gz + tar zxvf ntfy_2.12.0_linux_armv6.tar.gz + sudo cp -a ntfy_2.12.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.tar.gz - tar zxvf ntfy_2.11.0_linux_armv7.tar.gz - sudo cp -a ntfy_2.11.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.tar.gz + tar zxvf ntfy_2.12.0_linux_armv7.tar.gz + sudo cp -a ntfy_2.12.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.tar.gz - tar zxvf ntfy_2.11.0_linux_arm64.tar.gz - sudo cp -a ntfy_2.11.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.tar.gz + tar zxvf ntfy_2.12.0_linux_arm64.tar.gz + sudo cp -a ntfy_2.12.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -110,7 +110,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -118,7 +118,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -126,7 +126,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -134,7 +134,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -144,28 +144,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -195,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. -To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_darwin_all.tar.gz), +To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz), extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_darwin_all.tar.gz > ntfy_2.11.0_darwin_all.tar.gz -tar zxvf ntfy_2.11.0_darwin_all.tar.gz -sudo cp -a ntfy_2.11.0_darwin_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz > ntfy_2.12.0_darwin_all.tar.gz +tar zxvf ntfy_2.12.0_darwin_all.tar.gz +sudo cp -a ntfy_2.12.0_darwin_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_2.11.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_2.12.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -224,7 +224,7 @@ brew install ntfy ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_windows_amd64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). @@ -540,7 +540,7 @@ kubectl apply -k /ntfy cpu: 150m memory: 150Mi volumeMounts: - - mountPath: /etc/ntfy/server.yml + - mountPath: /etc/ntfy subPath: server.yml name: config-volume # generated vie configMapGenerator from kustomization file - mountPath: /var/cache/ntfy diff --git a/docs/integrations.md b/docs/integrations.md index a593bf36..c7da21f9 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -4,9 +4,21 @@ There are quite a few projects that work with ntfy, integrate ntfy, or have been I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community. +## Table of Contents + +- [Official integrations](#official-integrations) +- [Integration via HTTP/SMTP/etc.](#integration-via-httpsmtpetc) +- [UnifiedPush integrations](#unifiedpush-integrations) +- [Libraries](#libraries) +- [CLIs + GUIs](#clis-guis) +- [Projects + scripts](#projects-scripts) +- [Blog + forum posts](#blog-forum-posts) +- [Alternative ntfy servers](#alternative-ntfy-servers) + ## Official integrations - [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification +- [Home Assistant](https://www.home-assistant.io/integrations/ntfy) ⭐ - Home Assistant is an open-source platform for automating and controlling smart home devices. - [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs - [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform - [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool @@ -26,16 +38,21 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server - [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring - [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring +- [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool +- [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. +- [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader +- [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform ## Integration via HTTP/SMTP/etc. - [Watchtower](https://containrrr.dev/watchtower/) ⭐ - Automating Docker container base image updates (see [integration example](examples.md#watchtower-shoutrrr)) - [Jellyfin](https://jellyfin.org/) ⭐ - The Free Software Media System (see [integration example](examples.md#)) -- [Overseer](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook)) +- [Overseerr](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook)) - [Tautulli](https://github.com/Tautulli/Tautulli) ⭐ - Monitoring and tracking tool for Plex (integration [via webhook](https://github.com/Tautulli/Tautulli/wiki/Notification-Agents-Guide#webhook)) - [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy)) - [Proxmox-Ntfy](https://github.com/qtsone/proxmox-ntfy) - Python script that monitors Proxmox tasks and sends notifications using the Ntfy service. - [Scrutiny](https://github.com/AnalogJ/scrutiny) - WebUI for smartd S.M.A.R.T monitoring. Scrutiny includes shoutrrr/ntfy integration ([see integration README](https://github.com/AnalogJ/scrutiny?tab=readme-ov-file#notifications)) +- [UptimeObserver](https://uptimeobserver.com) - Uptime Monitoring tool for Websites, APIs, SSL Certificates, DNS, Domain Names and Ports. [Integration Guide](https://support.uptimeobserver.com/integrations/ntfy/) ## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations @@ -63,17 +80,21 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go) - [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP) - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) +- [aiontfy](https://github.com/tr4nt0r/aiontfy) - Asynchronous client library for publishing and subscribing to ntfy (Python) ## CLIs + GUIs - [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events -- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy +- [ntfy-desktop](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy +- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications - [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte - [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic - [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop - [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy - [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications - [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11 +- [Ntfy_CSV_Reminders](https://github.com/thiswillbeyourgithub/Ntfy_CSV_Reminders) - A Python tool that sends random-timing phone notifications for recurring tasks by using daily probability checks based on CSV-defined frequencies. +- [Daily Fact Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - Generate [llm](https://github.com/simonw/llm) generated fact every day about any topic you're interested in. ## Projects + scripts @@ -82,6 +103,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js) - [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh) - [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell) +- [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) - ntfy.sh relay for Alertmanager (Go) - [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell) - [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs) - [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell) @@ -146,9 +168,24 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) - [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell) +- [ntfy-run](https://github.com/quantum5/ntfy-run) - Tool to run a command, capture its output, and send it to ntfy (Rust) +- [Clipboard IO](https://github.com/jim3692/clipboard-io) - End to end encrypted clipboard +- [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript) +- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) +- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell) +- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. ## Blog + forum posts +- [Device notifications via HTTP with ntfy](https://alistairshepherd.uk/writing/ntfy/) - alistairshepherd.uk - 6/2025 +- [Notifications about (almost) anything with ntfy.sh](https://hamatti.org/posts/notifications-about-almost-anything-with-ntfy-sh/) - hamatti.org - 6/2025 +- [I set up a self-hosted notification service for everything, and I'll never look back](https://www.xda-developers.com/set-up-self-hosted-notification-service/) ⭐ - xda-developers.com - 5/2025 +- [How to Set Up Ntfy: Self-Hosted Push Notifications Made Easy](https://www.youtube.com/watch?v=wDJDiAYZ3H0) - youtube.com (sass drew) - 1/2025 +- [The NTFY is a game-changer FREE solution for IT people](https://www.youtube.com/watch?v=NtlztHT-sRw) - youtube.com (Valters Tech Turf) - 1/2025 +- [Notify: A Powerful Tool for Real-Time Notifications (ntfy.sh)](https://www.youtube.com/watch?v=XXTTeVfGBz0) - youtube.com (LinuxCloudHacks) - 12/2025 +- [Push notifications with ntfy and n8n](https://www.youtube.com/watch?v=DKG1R3xYvwQ) - youtube.com (Oskar) - 10/2024 +- [Setup ntfy for selfhosted notifications with Cloudflare Tunnel](https://medium.com/@svenvanginkel/setup-ntfy-for-selfhosted-notifications-with-cloudflare-tunnel-e342f470177d) - medium.com (Sven van Ginkel) - 10/2024 +- [Self-Host NTFY - How It Works & Easy Setup Guide](https://www.youtube.com/watch?v=79wHc_jfrJE) ⭐ - youtube.com (Techdox)- 9/2024 - [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024 - [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024 - [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024 @@ -246,6 +283,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - ugeek.github.io - 11/2021 - [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021 - [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021 +- [ntfy on The Canary in the Cage Podcast](https://odysee.com/@TheCanaryInTheCage:b/The-Canary-in-the-Cage-Episode-42:1?r=4gitYjTacQqPEjf22874USecDQYJ5y5E&t=3062) - odysee.com - 1/2025 +- [NtfyPwsh - A PowerShell Module to Send Ntfy Messages](https://ptmorris1.github.io/posts/NtfyPwsh/) - github.io - 5/2025 ## Alternative ntfy servers diff --git a/docs/publish.md b/docs/publish.md index 460fcd35..25bff035 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1007,7 +1007,7 @@ Here's an **easier example with a shorter JSON payload**: === "Command line (curl)" ``` - # To use { and } in the URL without encoding, we need to turn of + # To use { and } in the URL without encoding, we need to turn off # curl's globbing using --globoff curl \ @@ -1243,7 +1243,7 @@ all the supported fields: | `priority` | - | *int (one of: 1, 2, 3, 4, or 5)* | `4` | Message [priority](#message-priority) with 1=min, 3=default and 5=max | | `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications | | `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) | -| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-url) | +| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-a-url) | | `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted | | `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) | | `filename` | - | *string* | `file.jpg` | File name of the attachment | @@ -3094,7 +3094,7 @@ may be read/write protected so that only users with the correct credentials can To publish/subscribe to protected topics, you can: * Use [username & password](#username-password) via Basic auth, e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` -* Use [access tokens](#bearer-auth) via Bearer/Basic auth, e.g. `Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2` +* Use [access tokens](#access-tokens) via Bearer/Basic auth, e.g. `Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2` * or use either with the [`auth` query parameter](#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw` !!! warning diff --git a/docs/releases.md b/docs/releases.md index 69222b82..a1035310 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,66 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). +### ntfy server v2.12.0 +Released May 29, 2025 + +This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few +new features and bug fixes as well. + +Thanks to everyone who contributed to this release, and special thanks to [@wunter8](https://github.com/wunter8) for his continued +user support in Discord/Matrix/GitHub! You rock, man! + +**Features:** + +* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) +* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii)) +* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus)) +* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8)) +* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing) +* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing) +* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29)) +* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch)) + +**Bug fixes + maintenance:** + +* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341)) +* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot) +* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!) +* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy)) +* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska)) +* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause)) +* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt)) +* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing) +* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing) +* Make sure WebPush subscription topics are actually deleted (no ticket) +* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308)) +* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler)) + +**Documentation:** + +* Lots of new integrations and projects. Amazing! + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [UptimeObserver](https://uptimeobserver.com) + * [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) + * [Monibot](https://monibot.io/) + * [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) + * [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) + * [ntfy-run](https://github.com/quantum5/ntfy-run) + * [Clipboard IO](https://github.com/jim3692/clipboard-io) + * [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) + * [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) +* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) +* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) +* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) +* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode)) +* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) +* Lots of other tiny docs updates, thanks to everyone who contributed! + +**Languages** + +* Update new languages from Weblate. Thanks to all the contributors! +* Added Tamil (தமிழ்) as a new language to the web app + ### ntfy server v2.11.0 Released May 13, 2024 @@ -689,7 +749,7 @@ minute or so, due to competing stats gathering (personal installations will like **Features:** -* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#wal-for-message-cache) (no ticket) +* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#message-cache) (no ticket) * ntfy CLI can now [wait for a command or PID](subscribe/cli.md#wait-for-pidcommand) before publishing ([#263](https://github.com/binwiederhier/ntfy/issues/263), thanks to the [original ntfy](https://github.com/dschep/ntfy) for the idea) * Trace: Log entire HTTP request to simplify debugging (no ticket) * Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3)) @@ -1373,24 +1433,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy server v2.12.0 (UNRELEASED) - -**Features:** - -* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi)) - -**Bug fixes + maintenance:** - -* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [pcouy](https://github.com/pcouy)) - -**Documentation:** - -* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice)) -* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190)) -* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan)) -* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode)) -* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity)) - ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** diff --git a/docs/static/img/mobile-screenshot-notification.png b/docs/static/img/mobile-screenshot-notification.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9147fc119bbd99d691f6eb9a39b41598744ba9 GIT binary patch literal 72277 zcmeFYRal(Q(l0u=1a}V%6WrY)Fu1$BySoH}y9EgD4k37O2=4Cg?rzDR|2p5<-*+y~ z#k$xR`*~L1j8wnX{Z?03SN*CwQb|D)6`2Sb005v$OMz7Y0B8~b0E!b48uAb2;twgv zhog_0mYa&PC%Kc0qlLAdIk}s+lR3G$m$d}|;I*P3GmRR^f(ZVb{u>i5sv*X-fXZR( z31~laBpbNvA~NWo-2hUTUR)U)$0(~D#Vg3C1biU)#DVT8cdL=_O3mt**u0ZDnIM}3 z*gw2Xzlg6B{8>z{&skdN2>>7?T0@4`l9%H%b+l(PHghyFXY#Ulg3Jj35D@lqGB&j} zcOy43x3qQ;1YWjv0m-e+1c92I@+|UBV&+!XQa&!`sy+&8rarc&ykvNJilSTeKn^71mXuraf-F+zGUx_Udf z8GA80xKjSZ;{V|RHg`33v37E^c61>Bho`ZLqr00R5D1}@{}=Or#QeXxJGlOr5Fp}U z{)fWM%EZF_KfAkGTm1iY|A+G5-T!%*Ps!TL+)fK@ZEx=23Ymf+kd>92mm z$Xr~!|DyiGici|!*wS3n+RV-Be?0f!6ghJT%YR7!pJxR3F{*loC`Szbd{w*2*M_m6A*S{r!f2;FqL`4@t%}4poUw-6tlwra)jpK*(uTCwcreVgw@xh1 z-K#@qbtWz@J;fr72<`LNEvPGKq_5OW&_@m&t!*!5KVFCYa{qd%FKc_NTad1!)@`u( z5Y20+GjG0bcz)?Z|9evG2^AQT=WgVB^q1q^jR*_|XSq(aJgs|ux&FH_s4eXLthct{ zPjtb4cGhz7h3!Ym`I=m1X-&*%v=;u_sIG2}^GoWneil=GfP=nRFJ7|yroMxTpX4Ja zYjtoy`s>{r)7ROF<$=PRDziuq-E^LnW2B_jXS#gf=#Z zYJBt9jW0DZUS5iPbBuO`JJgST%m?}9%frb&(D!qFp-yL|`eW^Qj_cw6?hNYT9*KmG7p6hC`S>-qH$KSq-PJ-=e%0CkD zcg~CmQJaB}k4qk1;m0vvF}&KE-GTvIAXB@KhdA98fwP_dbcnE4a%A>ukD(Su>T8So zk2S|m`Wo*y86P%bvv^qzmlYB)*RFI5u6Uk2zdZJ-UM`YfbzW-BPu2J@yUwhq$M}Y$ z<2Oe!Go6&Bkq^bN@=Hvalz$)lK*`t4zzugTbnt?0?#4U>~rK3e9UNnc|HW3m z&C7f4)@}d;M7Ql(R+$dEk+}V=5NnCN5eS)Bh(1k?^kH;&n}u54qUWi~fBZqlx+Pm869g0+gWtp&k=iN=A8PhAc^bbcoR&Mz!m$k{A zyGoOvbq5{^aZN3E-tAf0b$mZgf1^tHBwhDHZxsv!#fAnqb-nnEqW@grU?h4y;eC|4BLd`w z0iej6EIZSfF;oP}Joc?S53Mxdlls;Q$|Roi%pdI_WB1Cm@^1rlQ6rkjemXD=Rnxwm zYz$qwzl#{%d5oG#Cw03bQDD9lh|&w2#01GJHOIQxd)jpm-~Pnji%R}m0kd!nM^;&!lF!o{OKrcpmR>^~h|#a=^3SDNnT$MG`m?TbhMFd)5# zK01NJC77l4mu?V0m(v%nez=o2!#prp6j>s+JT%40!M%|Xsz>f6kJRV;Q=b#U zu0F|8A>;e=c-p0Or{kaqqEWemJFQ&(fbeP#J^iDn&alD!Uh_I!a>*JxEEK*?tIcd#Z`lWxw^c+z6u7sB_$=`J{!>7?bV< zzqPNV)=}kJEC|zhRw`btS!QDgnoZ1kJM@*M_Dl7_8G{oepm!G!-IkrN_siH4o0WA8 z@$?H)dUnsv@+2E-G4s79tvOB{$?tBX{R%I}6yEV2mwWfi8Q-7`2M3S;uqA7;FMmjI zy7fzB+@-WNi4?28KM>|U1dH@Vo__6yA6d`{SR#!XzZGRNV5!9pJh)@K!cV-1Bb=$# zxCmYJeE8<^6@hzPxvki_Cf)Of&qp%&C$i3|B6Hm=rJ+YBKsU9=$+U|CIX?cuVL$MT z5rrUVd4bkuy_0F-=cA3IZcXrD#h)C4U-q?sv8>xV5MP>%~O7Ahe5wkOSs{=43pO(bHIctgfA>&SYCOK zug_EU1jZ41g{yaAmVS^b*l6){To=;ch>f_jabERY9EV7mj>f8`*_^76? z{-#nnUomgfVNkPpnsr+(nLHOyU@E8lO$vk+35E^|WEdUD(K4~h(5TiJ-FGQgDc7jX zD7hE7_fswVeufStcW`CCFe(rHBtNQwM-HUK{Ph`{?HyKdyy`l5D2K7&;<)KXbsdXC zp=5L=ZUZ?5ZK;xfDUB4dL6;x)5|ysKU0>I+&Qo?m(^AZEu$#8FAe>6munp->W`HIN z3psS>+6#n*SpdgCIMd;u$n;XhQRv>1Ca-+`p;q9|ur%v@(eVB!~ zeWsk}qo`P^tq1=voI6b!8QEeeG3P2MkK6_bzzXOPE)R47cb^W53v@R{g<~_u9yo3N zTVESb9^Y+z%T`#cS3~tqQFT{aigPgSbWiG2wu{aB=k6>NCFa=F4k;7E;hu|O%z#Xu z77(+YdD%i6gCtm7IceAon1~1q?L%W8T@9OiIKkIJR(wvroql zNo}7CeaAbkZ~I_iqD--wHoPwmqgDS18?k*w1s-x}V`HOFhXcs*5^T~ju%S{bp8#}% zJ*=hUNoD-3PgtiVUNp)wi#kj}x5WxGPGOHPU3Hj4BP98N!dMDtFSRgi&?L=jv$jC>B?iWM<2Q5PZbFtC!60(YLIUT} z`Co>%edZc`0T44`s0@N2bzW&1BNL|&8BZehVSKX5_PY6Ans)Pf*w)^QYmn2!s>p9a zC_DP);?Jjhy=%ku zcCE&?YW4dr!Ctnztj^aonGVe6y}!|u zf{U8;|s1-mC<83mV)gDC3u!do)xT+5&yrLA>Ya`FC*>M{$pa|{U~A>H8nM@)f&59Bz=zI#EFVXlrl3h zF>!WQmijEX+r7A*wBBXpJ7;fItM6^Ot1;D$)kK7iR<$(X*+aB!!Z_PI9$;ip@h*5@ z?-JjgIR@Po^b&`uE_kDTNkHAy^WB4Oo&i1t?_d<(^CqP+w%{I|=;>+0`#$f;KYG3s zcJ1Zo;-a$Wc`o9#Piiu6AM4!9iw+;0oSb}ndke81i03D131-@4A3w$q?h4`zYouc$ z!ctLD;o;$p?AIB=1ytj;u#+v?h}ij*3Gm?kvO-z_`&l2mIC6dAZ9*jMgIBn*Sd?0U zHThf*XEV)tw_zH`FoyHiw1RyBFaP%?kk|RSiRHEP>)(xaA$wJ7zeovNk6)f|BXQG{ zdUcCg36$N{#%aPI^&O@A5yv5hdSGx+{D7W&tNCHoxUq1B_M$8wfiX2Tl|%lt%CZoX&2qQP1k5m)F{=Wg{+Kwz+puJh*COOVS+%w4X!k z7`?U|UA1~(W&g=`^{eB(X#@!wn1qZBEmX7_NOX-K5V1~+7hS9}?92bT=K^9HuC9J^ zhStkRcm%+m&B6su@f4~at%no>UT z3)h|N=O44*s8cI(yp)p=5@JRuu1*AHQ~91Er!dWvu9rd%YLnWffq^j3M@<@>D2c)i zP>}vHA7;~0@6lY(_AUDH6M{wL^XHAM>l|G2k&o=Dz+HZ|0nX*txpUA@u z`bp87;aw{gNTMVv`@m=$mL#W_GF1XQ`yo#%P4GC&t05V-&I0+@UQ*L8qE&Nm=6CLD zm7=sG34^+uwUkCQmU-Vy*!Ft`f!e1a5s{x{hSS;hgBR`U9PtL?5`?Ca-~uI@xl?!M z${qrUlTnaTfTXh|dKDW01eh^28_6TYVOkgz!6B!WtJ>lzmqCod;=+P$W5y$HLiSAY z+P>E47MBo{0`v^QU4{%aXzOL(>L+6ahEh_(Zz%Zg5%&sfu*gs(6lP!u1Nk-ogJOIs zR#utL>_}rqMI;86BmmlCG^kvA^=s^nd`YGho=Z<0XL$}-sQp)I`>mm8d}f&GL}X=26&SxIKRB{-zebGhMUR`4N+8ay_e~790pvPltiN z)}mocilLYqmwULGU(OggC32qO$9Jt$VFLnekJdBs@(9;_+5MNA7&a&DE+umnoVpX5 zi>*uhpjJuGW_lf}7C-G|4BVUXl5QtwgrV?$+j4y8^~4$`1wmb2jcI~$sw`KtLyWF) zc}R+x7oA+V`gX7Vgf9C#Ka!C>AgcB7c6s<0rba`<|ISxzFX$ zb)F!C?>-$HVh{R#5B;&e8ZA7cjxnMnS14wV6~ROb51S5 zD!S=_6K^bLX<_-PsSF8QxvFObs3UoK2eZJk{H&&bqT{SUC|IU~q|F4Z0iU8I*G!ln zqSD4b=-$C5AMEXs)T=k;{k8X9iBqB}qKTR{gd0DlUe}u2F*mHjZ%!Fe!EUK5&P8aj z8q_^_s-sFI`rP_*^6H|~AO!+SC;0;O{4~4ge3ip)LU$@_IyIQmG&I=J1)MlA)QAi} z<5b~8@sviVF?IZTIZct_#X=+HamqX2#&XURAtw)s!iSSxSa%E9`w_6=HeiyB--m?w zol1@{4n>^aNL^HV5)NJjAWa#|Gtapr|Aws&xQafrkT>lnGzGRR)8Lg)Uy?x~5n5$L z(KtK6R1Jho=Gvk_VR9-gU5#o#RbeY0*IO)_CpOg#cr(!&S2v+$%pp?OXbOK9T?JX_ zdT)`be2voqH;ikpQ5XY&QDETIkKlYvk*Tz939$TZMn*b9#y3!`tNbhv=Am9HXn-IY zv`2!euC8xr7!L+Qf2-Q0%43>`IB!C^id4yvAmPv`BOsh6e6E*HLkHIv2NAZN*;ho_ zq*jY5jo zflllS@dE~2W@0<-{F*)t257oxYXb{Z%J1H8CtW8>OG=th0zP6M9vm3>UH9v^c_3MwJQ_Xt2|)so zXmRQK=MmeQ4h_1L-_u9C zkkRUWS6l_zTqZt*ew8$4u+2Dd6RW$8J<;yrQ;K2*6|vWvakxC%`yyTKq*NBGct2ho z-#G!xXab8WZ1VNeGYz%0C(HYIr+d=%lMAiWSqYJNHraW(a>yhK_8Vj4nI=hF4u5zB zh6b|%$gf_zTuP<3TZO*d4*cF(4@aje$QRZ2e|)nns{d($&H~Nz>x&^Bt|}*==bu8K zY+01Xl8SY#rcbYraEm0i_}m>ezdxk$yDj_~C5?gAsgNsE&$9b1ep(WVL6fKJ)(19i zl&>7vqAFs_3TmqLOgrF^i0&(l9x%;4(ETl8O%pb*vB$?0tVX?649fW(*Qv@Ucqr`w35nSJ`2t^l2+A~m{V*rNTh zlj70bO^>T?(|A&B!pTt4TAlJnE12=`b{}@U*a>Xl&1^pD)}6wvwnqU(zH39)=7ab| z%Td0qy_=P+tOU)SyfAMQ7U1R9`bZy1?`goPY?x?fgPA)8N~F2$tw$uwTr+32qU7>Bo|$zDe87PJ2TNG;)%D6*qr1a0)P zjGGP9ZE9=4LnaqA8fg^)g&LmH(MGM>D?QON5Xp)X8a(a9f*bF5b=^-~>5WE1@4|wU zKmfBBNHeSgPKppwmWi>g&BH>K#Y*nO25w_OJ3(tF*%GOr&gU;F0e^=V=G*13(O5&F zLUB^T!OZ-a_TnJX(;BXzqZr9(N;g{DVeCoOR2=XhvMjSiJ0aJa!m!m*Z_}PBGW!k-t_7O5r;G(JxHd%cW%s^QA+x7JeRjq2>X zv@~z}@Ds%fJ@;Xk&taHqG2l&f`aUGb3oo}>PWFf)1Y$@*Cj=1$4YW{6vjMQn0>?$8 z#wB(U5&H;P!iL1X;#pxq9bybC4O14VPdWf-h^K|cx{*<2YPUd+;UEV^?I03s@^aq| zNKl2riN;PNxZR(a(*54BZl!#6)C+)?LPZyLeLKciY;SOKy*~3jL|lHC?BwJ3Jx|dz zU`!jm_IgW{j3edw`7x*}d{o~Vv!nta>pm$s0`IgqR(2innv+uy&gJSQcE52xC8Hu# zv&xA^Ly_h>oU*jY*ag zx}DbA@Gal+u&&W}(}Y!JHf9zr^W|EHl6A+d)L?96w-mQDk_m8Wd_BtZEQOlJOyFR| zP|68-IX?XIAljnG`#eG$=Ja~r%sPMp$Y0h%O2jUA54c!EaO}KCTYS2@y1KT^@z6xO zi>1MfK0Q5+7kYSFTKb-VJq*4%ntqLx+FdCrP0v$en3ynN#QSjVWwJPTp~V24U|5U* zXlQ8Sr=_0Q2$G;F#wwywW5FtE&Yv#UprfM3#>7PQ8yXmF3{_vQHY+OQ04o+x>+9>q z#Key8B2pp{18p;vU;KoGxw$nf=YMZsJv}{5O(ig~@gAm+$yk^-YE*toj=a9El>+_F zh;Hxf%o%3DjqZnp_*`76BrsdQE%bUTvb{D5`64DSwD}K%Z zq-oR_yPy)T`$C`9%5k`ee&WPn2wkaQyU_cQfFj2(BqsXF+q`bUHC{sH7+2Pd_ zFC6rZ3M?FXA9@P9o#|j|RPFR-#V*YLlz;qTlYDOSQrvtLvazk@LjbyB+G`Kt3yIHQ zeCwLC&9C^EGrCovVTc zzHwXf=0hMRCUq$5r1hwIOe#GEKJEvH3(JKlZeA|pIU7^&{(o+08D0kA5jgUMmGy9`-JkW%yUZ=*okka%(=fJ|1 zDp5&?Aw{9%h0pY?b}(0pDtX!N0iv5e_l?{V5`%@)M-o4rUdA$CAI^QRdJu5Mhs{zq z{cgt^Z5QK6cvWg3T6xZ?oto==K@5TP(nfQH{9hoBQBKzPtEI1t4dxea`-_|vl%4DY z5)u*?78ZDEX;A4B-(*T3&sOQBu`sA72u*bqhv%h&TzVOgmo_nC6_T1D;au|%3+|JQ zJPm1Ewb^>n@(LRP8tME%XmFpn0iI)O6SfoXchDR)T1Zt8Tc8vPhv;5+DDMZv==8Dt z_gN@$B5Cw$dEp+G2Syw^LSS% z4dn_vNA)e)hoiBC5iM@`{tceB+}O#2?2cUCKaln1!E}6O;_HE6ucq+=Hwstlwwg5t zznmzDVyTjc%ggnfN%=SgzJ*w<$O03ePC6b(kSPn=E<0Z#($m)&iO9_QyCoL()EG_{ zHMj%7i0+5j#)sgKZ(AetlOKQmJ-WNQ+aFDXq%s)rbhWi93W5>PC&tG~d0j#x9M*oZ za&VZ9d=~{z=<0ko?C{}C9`^hDx|y4kLsekLHcaq3tQg?({<M*Tyw8;D}%!q5+>3!TwnnpDQ?PDV+{fck8BQXh?cws^fWtxy+!dDqJ-zYZmLcLGvt zzwJCfciaXgzjK+-GEb;L1)6&>2Cp#aXQzqku-_7U* z8T9xH3n`-tFszb5kX%(HxMCg|2yIN249PP1*Q`$S_F-c7*|$z z_a_nfG78qj&`>>4R_#Cx0kztJ;}5wwl>VJWD~`HOWw3!JjE+CJ5IlIUjTk z4LQiOc52jt3*6q`He=%z7Vff~%*o_--0*)~$HvBv`1SKAxzmcJg#}SG zOdLa-N_km@&!0DMmbnlrETkYA$lm>DfOw%7tAKz%JN?l~eGg~pEq^;-lZHV6fj>*8 zAN}91?@t#uy>?N99=z_3rpt$sx~ewS6dc?9?|s&NHEQ$ZAbX8Ix9ja@Sf&_HvF~HR zULrpl=~|cPZIhRJ#qY9E_6XxuT+X%Q2D1QDxe#HUF`RSe48ji)w| z9wQmzUeXYhRS48)z$IwGjC}_n1#OqmE@%|+a3$UieQ4e5x@ZA*}5DP2|M5-Je3!SQm8 zeNw=Gr%$%UZ+lje86e7r`FDXkEFw{)^rJk;h$ThAl^mp;UGF``%{mGNOpGX?fgIB@L(AsBpC`Oo z2uu(G-(*lBCRnOPZ`h*V)z$TI@=IRDimIX@L@%U{f)L_1TqpIP{Q``Qx8I&`nLD3$ zYW}{|(ALH0@tIa>R5q>~PS0-mIx?m77RzcH_}m9|ka`6pIQ0bMSIOlhi${ zbZYIGWvPq(iuiK$7b*xlCh%UE(hmv1!>Gm`c%^WUp>YN6zl-v^^fYxvjeP}`F64&v zW9e&-M@`D>{Z^yYH$1#Cx2!!bs zLxJ)syz_ePx_p>*X3nC8{+F4bwx_X!!v(dle}KmYd}$b97Ywmy1~rmE<$)rLr|<}` zUGk)(#<_?K1w}>J@x!1ARtcU*cI5svgFwh^3PdP2cmJZ7Ee|?;s z7>68xu`zLA(`ot1_k+)le;}8JAn77L9XSCq{NvxFvagrbrkFL}fx|Q>CWGrbVes$IGwsZY_VpHXPT$4qO1X1S$XlWTR&8 z-2Kom4*noZ&#r3P8)?OW^;^Xpm<**0Vi{`z^K99MxFh7MPYp{t*3?N&d>;A zVq$8Atfeno-P^sM!*yBX7MhxxrKP3j+Dv%SHJvXu0%Tu~^20^*m1?yh-ttF6LZr~% zfOW%ulWD!V`e$hYU+>_s;AR+0?QDHn8&rd@W`D6Kz2vgHSMF`G8U#h^! zXuiSk>+$#_QqcFoE1!^6*9pFwMPAWkhldn_z%3mZBCgtrfdIWVXZHg-a|4%DO&sIv z(W&?^!luWW7^r&UP`}!&n9xC$VNnq@ggu+;&i1+qqy$#9gAtV3z4u4oBg!)1lDi2P znUrpxUhuF~6DcZ#W>O6~;T;orvock3!ymlfz1Zyj$rw85aJ|WA7nb_2JE7kCNYf8+i((~2wioHK^7Fh9u*cE+AZ4^ zZ74CZfJHhfg2$AHD>dM)c5uV{H9JkQ9W?6FVW0@YM*sV}_u2|a3MSA^v!4ZHy6r?DZ8`+_H3CIIw33}5_J*piAWOe&#vmZX#(@(} z-fhFBSuux5;7pN6bipjYL|+2n;T@KJ)qK>Ix(sDqZf&>YP8OeacnHaW{*2;J7(wjA0C?AjIYB}I2Q%J7 zNG*Vy8-t+07$A~{09oj9%{5U9{8K>zG=m%-V1Eg z+|3Z$l_c~2rN)<}P>M-Rt1=k>u|}h^TrJ9$2*A>`{HkQP+1Jp0&`Qw&*Ev9NWk`CQ9w4Tjor2Fs93qE7d!c1SPMEEPo~M%uystr>C511u|X^ck#MmZaG z7NP4nc!gY83`Y)SQ*7+3)n+1$*&8BSFQ$pK4q%6joxRO3@1C64J|oQl92lx?LPE+c zzgme@1t~4syLH&pRp1g&uWKFv)na~dQPa>}=DKA$tUmPfx7DWw`YAGyVH5f`x9W*T zt}-d)A<1`jc3yn=GF9RopM$zYaWYBeV!(kFd9mEPB$Qo2D7KRUk{(*xo8!Sm8bmRT z;falpEzej8{PYPrL`wiGHk3OV(`)^gGoW3vbOvhOGdiY$|cjF`-pcAK!Q3)3Daf35=*q+g%)9UJYS)fK`d`D%>fe zRZD>4Y8AkQQnNjof6nwI{f-2;*XFod>MYd!P5bXtfz=h9e2fZE7j1&a`dQSQF*8?= z8iPf%+H#>T!%D>33m9oGg?=5%sNl94#NrS4st>n)G?n#UmKEzEEmX#f}{yswyVD%W4-W#ibj&%=9bnClc9>P|O!d30sOx|8C0^ z2V?FeIy!puqdjus8kkEg`asS{FC9Qoc(9k9mA+F9{zzo_(6J_VVd`ffzhBwH2CZIU z!@dlA-@FG)9BR`jK^%HQHNk4C`D5H!J(K9;2YBcqEqO`+j>3#+OJ`~gd5{4~Wi&Qm ztB4|~LX;{wn?xG$Tg9S7Wwwlhvpf;{E90qMb-%MAf~>$A3en|@J89D9IF!YmXZn`w zijIJNvF77ZxB=L%Ms#N*U!Y@boOZw}O2?Za=wUza^bw=I#GnS!;^X5z57OsIp#+zB zIuI4Ykq@$TAUQ>=ldyncp2LBz;Nr^fZK+1}_*6y|Jsz2l4cRe?6g(+f->i zgYCFZD`l5nUVIqvW_7oI@mFGJVBfhIm9z{EOf^!_KXXL)N@{90TuD zfk3H7hdnedQf0O3+2cnc@qu3h;x2pujKdTnz4lHoZwFosM7k)way4G*ozD6XqaS1| z0cG>*fC34EC@4&5o9-YH3T=}Z7#r9R-0T%HG^eqQ*2!P_@Bb1YTGgX_wuOD7x*wG< z(h{;}wcvAA{9LME%I9$Ti<5NSciy%eF0-Y%?3=$n!uuO_@~{)nS+hP<#M-+75^tL} zUy@I+?1VgWZkzQ|r+$Jy@rU@*`SqZ+{kTIHUct-JUHEyMMhI{?}Uaqbu0 znR%k`1QNzC*zOjp){SszKO2F5fJWVOqg@;biQ%(#rTb1FZ1&Iu&R$?#f=@>`n+MXv zyz4>ZIiaC}#*Aib{U-N{bsRHGLk{l12&CQJH4K90dX`L<4XZUEslfMZD&kHU@VBvzVDE2rOO)3mm;Sjoq*@9Ew@2GvH{5mK z2qZ^5(<}-!S5Sw z(+X%hPr_T%Atww3*Ac{~y9i5>Y$<4?z5n#{L66-JE|y7Ke}Lfgx6E23h3*U|6S~8< zsi5>@eeBQL$Oz`BMh?dI#@?|20+^`?om|8pL-7+XJrUpb=Sv>?-}U#RWUun5r_UTh zBoW3&xhyUg`<#Q^k15zY)Sv1uu(q-wIHPuIct%S-%yC^uMSKqC&$>Dl8XUeQ%)|;M zx)cpf&B4LJ*tG%E?TF3YG2YFkB`rh4OO#vPi@g5wXuaAnG5u^QhibFfB#ABrXeF2y zS)&dBCX04l$k%RK!}4`KgDx-c&I(L&XLT{7oK5I^+UEPR)~;`(y8;>DY*wN8v1VEI zj$m3s_Xztcm&cjMKN{a?2E_4f&LDyUNkdLoM|Ob(`c>eucqSWAM^_gcV`#mMbJG0SAog&Y+k`S zqp6{RLr92egcknIzYYFSugJH$sw$%I$RQ%uwX?M~DxzF%#8Fi$z6u6&adDA7J?8s) zDl%@Rxjh}+4S|_Mlbn%AwbLDHjOm40JBEv0>T-?cEpZDnHEJTp_mGGoP9Qm<(yf8k zA#ePsgdek)t9+vGZ5msD z{$znn46uq|s{0V(%4CNl7_wH|D}(-{s;a82f{%?2=rq|XKs(Tkfu{~puf@~#HH`}k zuC1)`a~fegm_NUe_zGlE2mNP?1+C_pJE0b>&JGcm6%{_W2jeouNixN-Q^}8| z=wUVf4d%A{F5c$mpVCG-Gs1X{uAdu05mx^G9U7I899BrLadOmUVoY^~Wwizb6rHxU z;-eC-t5!V@NO}L$sBUi;g%h5E)tlS>?b4wozno|KiF)o;^?AmojhnMcMa@m`{OH^H zfqz@u_C1q7O9%f;C*b(^`fQ+*oosPZ;3PzW6tf6nbzW+>fDFuZvI`3fU}`2Pl-W<| z=_C}PqWOAErL)KR`4suAPyJlDJMysGTwGiborml6w`?!yD4rqAn_aM)M!Zm2I?0WCjC^N?egRbEUadiogKTSSThJTJZy`At%?t{vs2Cj`UAFV^@>)Xl zK32xXlQw_$6UrQqYG^<^>Up4b`HQ#iyXhXU0(NL*(`yuQN1A%=9%GXdn#W! z(H)lGNLFlrx4$sc#$UN`H|hhsJH;;)xp4+5{J@Kb3;wKpXCyaF{GB<#Q|Yb+k}rg$ z9JClTD%GdoMi3cyA3;8zEEvj=b4@fk+dzGhx&p65VgYl!i@XW^je% z|8(@{@?$ovW1?=FkU3LsCdW9_a%x$tr_UwYFSEaPY>#LHg81`O3RN!|65Lyn_T@u@ zRXaD-@h`7v+^T#$#qUZ_1g8JOT@hg+R|Edy{HZfP9c4t~ol*m&O(Rdi$5Zo*l3U$E2r z(QX4AT7xc7et}D=>zu*YSJSl3qS7-TkB;l?`^5Lv2+=(@Li(4zICWK24KJ~*98i!gAlm&6%9trm-QFMJ3i#|SN(n<3uN?tA*Y){UA$mClXT>+-93=dH`rvLw^d3mN~R!10|(XoQ6c=FMQSWwV6|@*J5hVeb3wKjN(Xp`nk8DW?j4N09`u6E%2XnHa)(kVL!O-mo)r~ z4;usgI_8QFZKlh5g9`VgJlkkWaXy`f{Mr{Dx}`hc$dJR}?=J=irf-~LKRx)RAgCMZ z7Vddm^~bS3`vxHL2j8D<*R8pztt6YY>OK~ChUkSx>^Xe%-;`bOVZLM=c-!vg+emzg z8?4h6Yd+)vRQ6ss*m#lxnB*I zzpYlH^UiOy1xJ3@Sn~DjH;tm?5qn#%aPBmUd8Jotjp0Pf;(Skt*p$Dts9L~0f~LB# zZqLj7v-fUP)45RXyM}YdSJ-;)=Z6#@$9$1oS>)^W=8!PV|4Xdq>aB}qBDra-_R!KB zfhttT^};>7_eC|3Qp+`4@WFrcb86J%JSHmVy+>&bt9Ginwnm27->IbuIlZ$Ysx{Hl z3H+MlDv%dK=a2^yu3_|YX~(HSnH{R+67-)Lc~*CX533rYs}eVSLf>`CHM2y1#d=gT zslg(r2U1qf+uTDEN;R1wQ+R65!}>Xv%-T$%0*FQRkjpv$y4ZsjZ{sukr+j$z6Pb_h zLszUoEzD|Vv+vvj?HqlkDgEY;;0vdKX*2)NJsz{!!w*el(sq(-no2wC&q+ z?`{69D=t~Hk-F_K|DbOxJ!xv6nA66>gsX=OlmbBmATO#?zIW>#G`XqyRJZY~OD>2U zQxbA_15!&dY^K6N#jcalL_->|z0fRzzMjGFNceMiqUD$374Pa!-Vzfb{CeZz?J84# zF#nSZDc_$;)wp;`<4xz2@EW=Prp^1tvkQD({E!GU?YMqOv7em!{QdpC9T&ZTsp#mV zg17-mm`hstNQ0J1R2>5(l!CNV<;Q<1X+FxSH2gHa+!0#p?@MsA zQ1PP8+0spFtz;Noy$qGnMN1T`Ts|Y>QWmt|Dgs3;p3H7LU%;y6YJAyvkQ;nPkLUwN zCz~X5x|p|V`?54{ zozd4nJQ%G(RI)!N!^5N2<|zM<1qi1SaxD6}=xu|T|7Em56weeD9sTM{JbLc`|Guuq z8Il&y|HK6aXPdIIfq-w=viTdVKm_+c5B&cPr|I&3!MDNnqX|{mS~8A6!f+T; zdPCitDQaHcnD(XxHFu6iBs0R8xM%rEEP()&2#<1E3|w?Zdhox%+Gk|Ik0lQ}J=Ktj z19&|*^M3j$gWq2!KM@o~8`(8~MW4IH$jIpJ5@s@XJZ_VZTaFUIP*6|o%l}OR54j0> z5J{TL4eGf3zyFr?R+{Pl`q=V+(IK|_^yxR4VSz}Bcm6kO*p7IKu)cDW2jX3DYNIaK zWEdS5mQeRJSX#u z9@^mjMwf;;*2F%7I(b3#cd7~e?VfaN?tSOr>WlXBxcU zAqF0z?t^_nE=tn|D4u^{>)?ROfu13XTkr|(GLnZTIzQ@$2cUDz&)D>=B~3Fo{UE&j z00LI-#5nx1x=9}C^JL>%{NErSl8c9f0f8Gnr}8M`=!u;~FIg;&H4*>-1F->;7Lou> zMv*8$ECf}2n?}+@3Hc8UFSr~O|NC9DSQ+3{Q1ekzQu0N{TilN}jDO*g>Yf1sVQz6@ zDq*0_WDqqKhVTlX3^5?0K}f$O$BS6) zbhr)cr(D*@l(bXC2H*$qkC7(;D3F*LVF$ULETWyA8jX#Z5NH~;)?j#Oy;EyO7$ktF zYf!}2P&Ql`>a1Wc--#`Z2IK}<-?S;w%F2VNvB{wU!!JoRFjW|1A+hpiy>aiPd#6J4 z{&_PgqI6?fN*j12kRB%-7ZCOuE74MNLKutib0O0spx< zXbn0HHuHE3n?xHILi| z1A)xp17xgnUTPlRgBj}n zUlIjv`!W#@?VPQa$SvZM^t5qPzc|Q$I3|b{mIqLx0i2g>|Fe{gb^f8*+}`f_!RuJo zy~&Ujg&W{wEb}_(Q-gy;D}#$2>C)-=;keTl_;^&hmW0(|Oe68s1hNI@dtyUXV#VP_ z)Un`~l7I=oJ6S^dt#0}KVRfK6h&M-eFhISm1o+v3!MzRKs@xJzZ$T|Gq#OB)@AdoF zb?i18pm6zD02sd1&U~c~6LM!K9T)c;PuMw1vJ?}?Ed&T2V-tz7xU6d>e+3dGxYmJ|Ul>F^H*361G06 zECrC21gDt>=1$`F5LOuJHM;znx*f3>Qp(|lVIzM`G(n;8L3Ao00b0$ShU|tduGoNs8Jta61~2*2 zxNgUGo$#hQRuE7jp55~%+9Xb_ywtgz!Yz5pQ>n5qXbd1_?p4(0x^uXc?f-38ASuo{ z8XD1mts)gDs~V+pZgGyl-$27m&xhLCZdl?Ep@YDIw=<3Ib+xU|W{>?hPbTvS9qAYJ zSm8p2i!`SY0AnH;Kw(3TB*#pg_!X5@XSxYc?1GI0wGQ5Gl?JmoU{+avJ@V`$+i_?{U3$w&~+0O)ko2^61TqwqbS{A{wPNghU zLbXk8xzHDLDX}~SAC_61$Sn-FB8PtVClpVv-8((+)s>{sd0k*zMsyMiT(!@N8Xb^2 zX#FCOR1R7I1smRQW9Nk;C(-5qx3yG_2B8LJM7qc`j?CENwRF*7^~c|`E5SImmUp&z z5jpL?Pb&%89X&Fbaf8WdAkZo?(R^et7#P@G`|;_cBF*w|OP6zIY1}2=K}HxXDo!zv zp{>O;f4HvDQ6e4*uy?IEX6&Jk`vJ#~9)!M2V%6Wiq6CFXC+$J=8-Z>mT77qay=BX0 z&R$-z^nMY5n~wemlK%Mg6Da=lOQ_@O?^dAwTuJ`(9o^&AC{@ihP1E!B^E$x+sFxh_ zl{r*c&Qjj6OdF>fq#VtyQ19IaI@ryaJNVrHEte(34KTIkRHu&*AeNygJcxq7E17|} z*oJ~L7`Hm-C%?4&KTm!EQ}FPLOUE-!3YHsJuthKw^+`J5V_B4GPxGrJTb;81CL!3c zu(wA1|0+b4Jf4X)LX$J4i;6R4S=^^Eu6raAiRLe<2Zv$;d2-&IHIyUH7W1!v0|4K9 z$u+gaoWP)upVK}n5}%J3w4s4H=(?@Vz=2(P&_hZjfa2{~@$hc~ULA|L!34f*`A$jX z)pcMD3O!;5U_+Wh@pjZdvAkPNFRy-h7-pEc$zvcOQDmI?nH%~=7*HK*qvY$y*FixK zd4bgpJ@u-42a*hp918HXlYYwt7z?MjtDCy5Imr+{J^yuP4nu0S5YQrym#~~Vh~O;Q zB@#E&&o;-4b$cs(<0wUL?cp*;{C+!OXfO47)8l#Mdd=_pw-OhN>$=cW&)Utuo^xik z)_1ST2o9H?k4zqt_V!j%cltV>HcX70Jp<5}{l-@iyJQ^_8CmNrDPO4&S58kAQ6H}c z(M^0dZ`^v<+Zt4}z4zeUkejHro6&Jj->aHzF%uSIlxxL8?Z4ZJIl5$YC_^H7tagY0 z(K!)UyYua)O};q~Y0QfMz(!#-U3S*05;*{F*E?~Oyuk8`Zrv0wR3YK`8C`w;k0Bf$ zl9KI-OIPPiv9VOuTur5lahhR<2{k9(u3MPLm z$@1gw_;UP3f%_45J`=rA%he_$gmJx7m@o%CJC_@9ryQa6^k+FLD(MUkqkk{xXxCN5 zfRVoXas^?1(^1ehdxHTCQwkfzhQ2qI?y^xXpNT-6RXqKzcz#d|_(q81R_Bf4QcZlH z20AX6p8rv;J!a(;dyKVSo!;AAA1^(h-qtu}i*P74R>i$6b3fY8C-Z+usgh@)OhR1f zNaQx_@9xB`ikv_BoS>%dQSN!u^GCNQM#RpELF-f&Q|0{OJ$D)1Jf{Vn8Qu2!`DPtzSL0V%iHpQbd+9h_axF zdOHEy7@ePWzU@UUu1wT8A!v;X$+|i-zMLjH2N9*c=BOR3lYI7|&R3ic^=1IlSsVp^ z+LgV!%Lz-1Hx%bm(B!8)(l3u?SL%MS9ed52=n*qU>gm7X6+#tX6&E#m_17cn?*Z^UX+rg5O3V;!y{2Y7-H>_CVmjSH+yg zN^*v{-$Fe9p{jTsdOnrIx~3EUbnQne4<`hmF!YM;VH7tRg^qOG6x@?huD<*dXfQ#wuhx-FE3IFFz4^G3Ii}DI_ z@NYtnx8`7Z$t1hW+w6{;vXiGNBahY3M~gchw;Lv?Y6)?A^0a1?b+64Lm^ixgw7vF} z9_4$rPV=SXy3HgrZTa)G|`YKivdRhicF12v-8aUG#^Tu6gXzb|Zo-+UWnW z4est&?I=&({i@HxCA-Bp#e7zq?=i!-{RjDOO&eK3<^cmNas@ ziWxg*H53W`?PhVV{Z+=&3Ccx-GDEFBUH#7>MbYEhbT*wWju@cvpuXMLX)%Gs;Mgz9| zJZ`kJ@;Zw^Syxg*!d)_P|C1;(gna&vllZ-*-AUZv!GLCc0!a}uPzVY3wpleB=EaLr zxTJpFgAn3!*HA0wzl(o}GXj4(>^|tfdeM`nQ&9xsX>ph9>ClN_#Sd<|f&JrB;z{kf zW;g3TnMsKnNo0cu4RAa1>3AsVIAiM=LayAeKc;XQ4gMN0`Fwfpf6}p3XCtxe`|;yh z_T!VppNw)p|DEFs@rglS+eQ^3`*-`j9haFW4=Wwl2Edm^Gi*tUJ6z9)YT7Aeb2@x2 z<~`L6h-V#teBqAs@`}UNHUhwL^X}@n_~Ujju0)gXt!tSkwhVe$p3SL4suAzM7<|l6 z!Tz)8@sdT_C8^4PmYdwLu1@G^XpD@F{jXO%)Uw6TN1epU@G!rIY@Q?T`Nrx{HVGP9 zO4Qkd1ZqmTT0J+(I__6&nFyod6(-p&&wqm=a#5OxkVN+QE()Tb-s1<9qQu?u@_fy` ztkRfD7H^Y6^Wc{2zud%neM%vR`emI^OX81n-3JC@YPMlZ>CvA?xaM8Ia}alo)__DU zix{-S)>z8(30uO5s%R~%2wf6|?VN9W&TWjJPS2(;U8|C{u72eaC@GrlO~a3XzdluT1+m{TllajO@j}qWzEC=V;R=+wWaTQj%nl zUQBef*zIo@bu>zWtkH0{C?=UVck6F6h9t?=@&=d-d4))?jzopU#evA+spGx2$(xD( ze*H4TDI&@uprh=Y;Z!Epj(>wclwTC}B176;PbTReTb^Tt%gRbm7TILFt8B6#F3#DLg>Eea z<@xxgL!>uQ0BoPRo3|!$G+x$FsyxsTC%V`#LWbnq9VJ`6)@z3hw)$d;ifAL>DHS+ghjH5q1aq zxdo@+3MAPp+r4^jUZM*fA9^o6?^roBDpQ{~0X`s-Yk#-UXk(j#1RqSDhk?W3l+PDE zZWV1e34OOCR`2hV)*i+Q5SM14;&R{+$W;+{;Jtd$7Pa+aWP543O`W$2!_yYsNlvdm5OKp1V0&+>(=cI42m( zG|)3He?MY4y+4x{9L&%G6k4;7eGniQ9d_SKaST9)Z8&?{*(ttrAex+?AC#l>(=F46 zo}QQJ{FuD?RlXum9RP5-zW5|e-sJyy_b3;Q!c*usr>3U5g0OaXcP*;s#jcmn&qlaV zJCP|j3T8&BfV5Gvnf&}F+KoQ<)YJNxGDZ9t(mz0!ZQm@yGJ;2BFuz_(rq?n5wKW!Q z>G)Vt+uVUC2$2U2LSttYMWTb-2IBxQ*@tO@@ecRR!gIHjpP!!&5GM^MZ6{{fdiavb zR;rZT3fNfTAo-LzO}sCo@K-#k0Y5)@tofX@qc>pBvTy1l$(0JEyTT%iERkTh zEM2Ik&&U+={2R8JJC#xT59_3@=EIivp`)EETgeLVzYF=8xB4ytVJ!LVBMeMX4%I0N z{1tN@9ez<}ENDOoqRI5N42m`cXF|*6~ec0{@n7o^p*i$dz(LB8OF^2=HHcg^ZCyO%!p|$d z?1rI%H|vI|sy1s=n2ij)NxZO;SoR+mseu5gUeI%<#LJ2ki~uMJlF+nUo}pYuvUWOf zfuQcgBI|N1ICnN&pd^LE`?Tq#yO?_b?twcEZcs#1A|uO?$c8>Z`-jQiBs6pS3dgUoKA0^0G2~5Z}i#B_$sy8Wm;sr-xE2;S-k{}KGMZ;9di zBQ$>}^_<#y>Wb1LC8a?V!AFzjv${IJq4r4drlQJGt(tj?sxUOW4Jkt`Z3-+Sep%b* z-RGl3HdbD?klW|6K(a<7bV`nCK5KfBRQX}a60XHEQP_K^^b7mz2Mc0iOeSakSK9Bj zli$BssPvyaw#7c1sD1Z0{)OK*y1rY+5ahF2eJJ(ZR^AuVghWJM3K(*`({ zmBAo*!I*gDNSLCO!)P)ITQZTz2ZTfCXyPQtw@At~+C3NUAR{b#%2zAjKN3kzgsDd1 z_{=goPa*?9AI#P@G+5c#q?gn;G@L9ixXzn>p}15xHhy}#Kbiae8_i-8)YHB#K$}6 z)=yVl^knl>hAU)C#%qy)Nv$;xIkiF7lhA0m@8_P`rjfP9sVekDD+onu5!FNA0^5{{ zplHg!w|u#_@gPLw=86p0SaC&3{X+A@a7i?^kU|izbbRKZzALlh^95Nm`5%NFhUup%NW)-zuHI|-v~=^$K%OpLLT0Wt~(nKR!XW+(b8*z8Sj!9La-+j<)F8p36xshiAt zLi3eURMQ1HAYk~9$(KsWkjWyAAHTX}WMuq1o>5M|*PNVJ_m9E4+S+$#@pE%!CI)&M zW~^5FEdNbw~w2h^}@};36Q;nHn2|V zRh)~dv(^9uK$jTrz4Z=0CwsH~Q)#n&Aq$*=e=MHyC|2wrWKTy+3@5DkyR;_NTxsg_ z!(mxnI{y~QTI=0l6%hq7gcW4%vle#=1%!b(Ffoc^)oZ~py{8Y;Y7QUlo^}}tIv)R| zpS0EyLkRIi7DUJlKb;mAW{%+{!vMfrBr&WCKv+Es=*A1|-2_5o!RQR>BLP|r9AS7l zz=i+nPMZ?PtzW@q&48HsA@e`5-U8wH@{)nQ-C8l3g6ENLr0q8k-67{DURFH0X~Hg> z*vuI>Q(w8juY+1j9WdW9*yh@z`NCE=YJUn(Xjap?oFx*7OBO!%O4}(+kpPLI00`Pw zZhNR;q&hm5wsIQ(-ciGvqn#bjm!6}eqs+|A1ZFi(R#r@i^e-@`QpDGvB|^fRI^aoW-j>l8Z)EY@AFo zRLe?H>%Nek%=#MJ~`wcRf~Rh?}gBm^78vw3QXr zO6wA*f=qGatH2`P^Z8^(WI{q|ML0wF(UcxY>_2zu< zJNoIwK3x7xim;upgbvX9EH8dOz>E@pP5kR$NOkQ*W!x9U!QHcT*8}jSt7&OTOktO5 z)1t80l~~>+!LLGew?(ywjJKEsoa`+B?jJeXMN^WA)kL2f4@lzu+ zgu@k;l&}bB4d1=9G@p9)kK1K4I3nV#E2yM3CR)_p&i)|!tXeaui0xkCN8Xe1dXO52 zvP|*Klr#PIPG;#Ot;`cph(la!o9oO^dMnGI&aTs%11prt% znC*A@mm>KUeE-Xbk7w5UL~;!dD;F(mE0ZLXLd!RgJ+d(ZVIA^=p=0FO0r$1UcQy0! zru@9T-=@9$UxdTRY0WLshr%2&l3*h1n5I;BXf8b;97jo9J!r_{b4J8j0p{xNgKr~% zlk$L^UI=B5EblH42>@?9Q9B0)4wr=hN4|>#Af^rN7;rpANSv+UOOnnPC6!$}4~;wX zWI>+@B6)p`(|(h`Cn)7sjQRZ|@t#vtVuJ+ETg5HO1h*DMbGjZ6X`#HnU-kM?w_0iT zmLX2wDoaa~@bN%>A=7FtYJSHw{=1}tW0r{|k+zT!=gL1b<~VJ6bNI{GwcZB=@Cn%% zVP(T(rQgw9mkTG;Y7w^V=|mMSQBXaUspCxzmIgmJ=E72q~w z8F~VTgBy%7(hOw|GECjz?RVG5SZWKF{)w;%?$6f}Z@vv;w$pYJy2OeYscc*pB^+co zVN)_k%LmHmVo-ai#p){ejx5AQf~AiWQ_+*&euy;ZM6RAtbDQ_P-!mLPdMgFO4X?8W zp}jNaNUPxhE)X9eq-T1IZHxVWTYjLaF(DoTzIoCrq713Ce=XX-GH|SpMAwbY9!}K1 zw*}|DCNnDaCZ4_lz$DqtU2o7Ba)Ds%VB#>OEgArm2@89zC-zf&1|-eigE2x$ATkH6 zj&w*@#*zrMu2*2wSkEs?s56w9lfp*QkgQxz&W?_biVj4jYw8*gfqNt2(!GH!@-UXe zIMS1y*J!as9}kLEs0-160m_>}MoCyquZF>aoeT`+s?HO*6GVUTLNAr^4k*yWgnfHe zzl#0<*qxkUAThuRL`IHumLAZ`CW9|L%G5Ivpu^wa>4h_sF!``DbIEq$H>ubXe&Xm> zew&FGO6Jr#c46bku7|f{nPVg=J}%?h;@!-kk$v&7x)y%ktBGVZ>BM*IEiI3=HXAI+ zVAW$Z?11pTIGAb!2rWXaG;cXt_TY`XEnGz&$~Bg1s->aubufb60m|Q7WP%KSRlq@uxaj^wm@Mq*FqmS6D&c zeK47Wm{uIih!}atq{e09j^B13-MF+LeD)@dd0+G8=UtI;m=If|ou*y||@*Sl1zTn;RJYnv|yxl4fip1! zzh_ipKD~QW{Kct!c6t9TamW~Rki-oIPr1i#$RI7>S0A%NTI%;Mu)t&=m!8LfG%(i=nZSH{OdpL2~~!Jd9e^r5lKR z_lr>Wp;gq(&9|XkixPhI^2$dH#~U2RiLKkQ<_?@db~&%cLg^XuQ-^qikM?Ybx)MyV z^kfmCi8x2PrUyG?NwO3hEDe66)_&yWoS}@-XnoP zd@Iu9KG1Nxv~EEUYxX=YJ)XA;s*K8*p$1=7tSDynI~hPQZ+j*7eg0sK>lj&-!gDc zO~P%levwR@>wf%-@+c|b@{=O9x}Wrq&iEpt%=Nb(1#748CPqD4_*Of1=?mS50L0vuoy0UYAg;we^|nOVj)ir!BgOAENV=0{ zb7txddDvbYE$Wr+^wRROlqOlMneuURW5xMG^|+Uj4?6djK4k#XrVU3Y4UuPw@Y404 zWR;7g)7SnWdFo)A9{(wPPVNN9y$~ZN7D$ZzI=@;L9AEurCHu$+UvsfuA?8B3R_+a= ziFs(QCbern?$yF{>*)Y**ky6}uVX+~BV8G8gvGOBVnKUF(R?w23X%H!G`uAN%iBHT?5ee_|D_fSBI2qzM zeyQJv;jZIeQV-l>;mA5_J6`U;jk~- zV=`0HLla-$G<8p|4u(^+_@G%bP02fid)hEaIcDXjOb09YGR>=1-j5I6JBOJ?(mlY2 zp6cFe6>99KsLOLL+l;+^%!`a~o1dEXUt9BAO`ap|j!p?wP#}Xt(LfAR^xuV@*_dZ% z=Qxd9%w_ymqTy&9*=Us+?yi1|8y1Dh1VGS|l(mcEApiRE=Mm$zR_z1^(Eh@3)Qb0L zkG0zjzI26Mm0FVJ!u7p~E!K(Lhs%ZB`^#f@E=M1N*YuasHO3x+FED~H6oRVA>=Tzk zD@vVye>MZMMFNA={)M)EE%5(*unv>AXsFnHAX^Ds-X3!gDqNbFyqcK8aB|X{8>8gF z^-KPm%*7PetCw8C#Hb7sMDE1H?EnU_QuZ%qE5MKxM;ox%3-fUsm-vT!olNfYF2h6r z@b7J$Q7RbeU_jVlg4KO>O1u^SMIqHK-$wDpn*t}({5Jh)OOE(l;hXuWZ4MCh<;l_Z zm!r#)+O4y2AC7w{_A9UNT2+(;p@ma6OnBa)j$i=06xwf)(|acXNCN6-r|vL zC`d+~AIcK(;fN6w#kt=C4u{}C0;d#5jUQJ71x7RiUY$0-^Bo=tbZ}p;XLo6aC7_u) zT150*giMXHwqEZ~a>!Yi)p6h{LgN@16jTvFrX&j`JLco8`;~_mKCUa*-O8QBLtfqv zslS~j#oP!Yj|z^K1P(Sg346EwbDxg09c8ALmlMPKa53@zex>1j>FE^g|LcChIG3u; zE#=O;2TT<+0O+*2ZKmKBKmHA#SZ>TrOiIZncwrD=R*X7*xC-f5c>MlFD~d&bZgvLp zbE=wmq%^XvdVx;B!;;#5e3rF9F7iqh4y!fgBJ$| zX2j;jPKZqjBb1apy1IqrfO&;)E<@)hXKXNoh=PN={_!zL-m$1CuweqAz+$PX{ta$!gRhWg!y!th?GGNi5GsFQ`-ZYmw??gi` zQZt3YC=Y#}o$u2~Mx0s>#;x85t>n6646eZ8Vl4!6g;6vS2<-*E=&AWGH_ z&Ql)X7ysFBz1!e;m6L#OePg91!vUbjE_b8 zy<&;U$sKauW-SZbv6uygA1&EsrHlG(-F*h|ew-iF8WS$`Tz5vJC&r(hOd*)FY!sqX*h}&*_K8~v^TKjr`+UQqfJ%0NuJ|W-xd%37o`QdR8j0dFM`7y5} zmQYI%*`hmznGsL3NPa3WJ7G#!dHN5za`nwoZ5-nAGHAl{y9}|}7jTqVcsK(9FBgQI z{Gj@orPWYrf!no-@8nz0R0@&3c&!NSoJIlYyV^46oMXbG)%U|A7*2Dg5T1hi69Beo zElS^}ULOMBH$@A^>(S+1NJ9!@qT$+k#2HOdA2+ckJObaoU$D3*1kl@)h7^fZS!s{zmE;Kdqsfu8Lke<{1bp#ThxNi9-$li^4hf5P9dA6A8BrGt4Tq{GV z%SFkU2CGKF90(~59$ajFY-{On&kxIW_BtkaGi+|Sm;n91gz-2`agNOi$`O#*5a>nP zdp5-RNVb5WKype__~89Wc3Ow?hDyW3=HQQhG*}HDmEd9U9`nQ2NsMv^zd%XO;F9Uy zr@^kK4Pyp-3Gun|*E!?7QEeCREXl6DxB|hn@Dz02L+<6YjBp~>NM4Ol^lF$>pFsgvH$0NC5Up(krCP4<;~p_Y*@P z`(iYwm)8b5fp{KK_*q0m+9IX2>1d96@KPGp@#7l_5_`k0y;5 zS}o}dhIfODqRyFvbD+ACz_6V6x|&ZlCTlPLj{F+3P=%beIJG^UJ)7J3dyY-B`zuTXav?a~@9{s~N2f z=n*cPLxp0`sK#h+)<$=iB@iFIgguwP!5#56q4z)EiO+gIdtT0 zUz2eAt#v6QEVqi`MnogE1n4uj!4m~!c!maEvN^73p=H`>O2 zE6vdKrwG1XX)w-~a2m^@f9roXZ9<%)&4csf-Jflf&zDiRBDV;ye_0*Rj~fPvD=H=l zBySk-)E0|*6DhAP3xj+zHU@$w!@oH!1xN~hjqYD`sDFG??vNN6{7-Z>HLMRXL?Yj3+gMC+srnzMkfWC!NWsv{;J=CCrgks zji3>m@>GIB4CYJq(F3`^4$m=syYPsH`Ccza57gD82LRA|bE|@B8i+B_D2mv>?5kuP zp_1KjpsSvbbuIqaOI>f}jQo!9E871Q~JHB_-YCk_lPcMRzKh?@)N-t2UVBV1X2uUM*6xfEE!boa$@|;NZ?_yr}%qPl*-3 zB43Hm5LUVsuA8D6@j$_Dc^49Jz!slM@bIjAx_m^MEvOagE zXnfPdB%m`Eu${=oIPbWasW2dYiHg5r6Yz0csif}uJB171!-MG?ybLog7jh|v4S;jV*j-*vfWj=N%>uxV3owVyU z4B>!?V>uXpVp(vo^45gN$EP&}!dM>80(U>P9^`)I;$kWJ#KWT@Ip~z>^F(Is=b&I= zaS)y9AadJ;gI;Ajw~4{kdUX~U8XBXnmygb7^Go>i@V3Q!S$P21a`k+?{c1nnzT*BE zvFc@ix|#Aubm_|r3jYlmvY0oAl_YE_qgj`|e^jWXN~^Z|5vL<5FIYI`YEgc{;9!3(vQs;)CcdK#lOP0C zI(W2Q_Q$`hXsymKctskXNKAstC^@^JD7@$G4xX4?bzN&iwK@t#rD3+}jsNm~e!P~< zX>EOoam%U^BV^&rF5@w>WBT!ITR{Cmr0>_3(7ve1``0BB>0g^iJe z!}}dn3z(F;%#YwOg|W(2{f3<;5jA!ngcrXxSxO@H=ieHI*)HzJ{^8&~CVpxnPsZNz z`g*dg?8n&yrL^?4x2fMJEe5h(X31W^E+Gd`s-Wd=Ol|Eqi1b&pE?MvqW3L=dH_{}}p8frj-C+4)hE1<&2V=+cC)RY)|$ zuO~|40)tPAAJ}nyEUG5{c+rTh#o72Tx4b-B;`ug4@wTGrs-;Am^Zr_bZl(3!?RVb>dTV;Ly`lf?Y;!HA#CayKF@#L^XnH(|!SFS=w$MPkA zMLIvW-6YGBetNRQYGaezmlrpW{@E;=@)^{MZ)+3l$i`<@XPdouuw-QNe+;Nh<*9!p zByu%-_bo2-^L?hrQhjhuHyXp~-xZJ5dgrH_zCM{BFX(6*E`PosS@YYP;WBo;?K8Q@ z%u%DCbzHqTUQ)_tr>yk7J?oL9X>WV_A!KY+>ppWoXYBEqecbLLd5R233KA?|HyDc2gfq#0}p06gR)t>k=l0+=$2a$;N&>6Oqr^n2t1|h$lh!OxRG^Wh> zN)=}6VAxX4NJ#g_QeMezy2XjZ?<7+Q;Wl8z7(=Nxys5h#_vOUrPcd^%b(AQE4Be~EOdP%WSCGlm4sp-t2Qp*fgG1bQmzg^8Y%5p4+dj)Q4W>yG4C@8 zr?%6C>?iN_^|(M->GSy&Rl~{^pTW`>I`KE6jJ4jE`)g}n0|omkI);86MabC2R{znn zhLkZxx$7z}*9%Hs%isULQwchFKTLL%>0hmKNZiXfnuU{&14#)-Oq zR+19EeJh2GzKY)8kHSEYh)^7ktfL1@>Kbbpzx&qUJ`(|FCmp;{qI;=p*yhr6b$3Uk zCZ@tV?I6PA+<8X8mpgiPN{^oWtj`$EXK?|Her70#VMlh`(^!Xy~+kn+N3{}^|M5N+pwrsx@8 zs72G}rP4_V;<|D>c~+>UFZ-uN*j2swy~1S0io!`LBqZ}%z<86=!{g&NapX#FwI$6> zW-_A%b#(%D94ZcP%-G%y85*x@s-N|EZ}9VaA)+Laq(LwyFnh;TsuU`(W!PMrnH_Ub zHJ*rFiNaxli-Ru8U9gUy&g+X|p8o!bP6K7YCYii1hpieOPcqSQ6s1Opp8dDKe|b8w z>1ez(Si3u3*fjway>aJ%TyL54uiSjx-@kk-N7GdV^t#+9=(wvZ;cU8amHy!P^LZ!+ z@o(PZxu}Tx)2`SxJ_{#w78f-u1kXN8%OYwte130m1%3;)HeF=$$f@3)stA3lcCF2w zy-Q0++^jHXKg>0e`LQNuj@V+4{82GbeREW5;(sFdB&mMb+kJcftclsEjx(?Y3-rs4T#fuoWvELhbz+k;=IzxhpCy$}RenFD5w zNUv6B;1ZXx87a6#J@CoxW?ak2N{36FZ+B#Q?+;nAC99;1GcHUE2n4H%pY|F!akg!< zrI@%b%%Q?Z(JI5Me4wKF;+oISG?9Fnm0O}q?s!!Gy4)Er!7y>msZzf#$yFrK;HOY+od@3Ful-BR zDlnmdpl@QBT<8lLRe0J0*bz-7XUU`f<5Kld)I=Z>P`vz*8>z0Vv7WAD+~B{n-yz}q zd=tKvDgs$AokOUEKOPzAK=aa{e`NRwU>jJK^PR+Kz5Mjtn6*o>{rrXT8c() zJ=fJ|5pgnb93@O+_r3V~o;_2Lpf&vx7 zg#h01e*5h1zi|_;CFtZ;`u)5A;2y97BPKYjEtv#*f8{>Rk zM*L3lD$k-2kS22&Qm{Pp6a}(Wlhbh$T+^)3=rw~o~HqKtk@&oRW@3#@0dLQ z87hOUd=exfsbv0ZzUZxm)|7c2uEKrrOItwI2VdB|kbu&Ssk)A$#_Dcs1 zi1S6NFLXSlaS##F!oLP{{<~B9Q|>O@kuQ1epNf$?GixfB$ZULL4*U zj`jTmr-$rp-{*~vWuId&vj6xF_Vx-hTpund>V@Scn#l#%Mr1V$`|~4i)L_O``feeqhgKs%a+cgfd+_U8_qfjLv_%P^cyffq}*QeG&4&_?!>2@{?(m zi7{b-+R=Iz3HL7oA$suGM8er$-f~Lkat00f6#vzFN>8+S(4Xv&%w2mW4uJI*Ww?pdI4p zrf>aMLmekanKCN-kjztf9U>%+%O8%j;`SORph~i1kC9#j99u)anDR zih7^)ww3?pTX;f;^JJfz*wx@-65aOpAH+_CedifEE~HY}ie-_b9b`J-&~TvqwNjdmk1tYD;KWu6GW{AeSHkJ% zCK15^%>;`{2c+|^OQYhmgkXfZB=8k1;K5WE$O~iivSf;sgkw)qS1PAq6Eeho&6-l# znIGYT{=HIW2`}~+(l7k-b@Kz60T8H<#0cQ7VgapWSMNot7p#%}wY6{9!p6C4JBCbB zN~*K=z_zrA5B4KveuUg&6v&0B)XS=F%wq3M$3GV<&raBxI5{Mkz|FGOsa9{S~eZj3w0g#R5SY zpjn|nS3#uJEnHU;`MI`+4FzroO2J09bZ|p?`S^N7^$jMA4N0LqQ9=sXc5vcP26hG1 zqDR9{MnWeG3;(MC_Nx%2DM={eYa@@M5MTVzSu*?dxl0-skcsf&Zg~5b!H+ zZhEOFJK-Ssv%5Rc+)e2#Q!WCrPHTlxWR?8t|5yO91|R)vkinraRMz4&(ljhJoLfkU ztkd3G4#dJT__Yt!P;$BN&B|M4Vi{!#R+bSQP|&2U4g<96f#T!-W-v6bt?a#gw_a)s z&;KLqETf|Ozi>Y@3^4Q%(h>vGNQdMALn+-zH%bWz2nY;4G)RL;mvl=bf~2G%sRGiC zg!DbX|6TXhU29&QHFILlH}`(_^U>v}i~(bQ9f~~~hrz6G@aZM3rux$@?CjI9;r*{j zw_VZ99Y+N*RO8u;MixHnL>8Qn%c1aO-=$jHr>Bf&Ol69s5*A#`&>Y zJY-Ge?%dRHQpz|?&(iEHG3N5geD4C9{qij(q~H4ZgIxv?8Y8Uo+PNDJ)a43@m>i`u zp&kB)7iL1X|6BMEdw3-@RVQA98O7>t;h8*l_{CT6PfmekA^yfXe9P6rJ>=eZ%*;7cf&c2fqP>zyMBf~;q&z*1`<7iT+aopJ6A$jTx zCNnU#&(b4s%Gr`$ElSRrJOm3h&`MFw{3!>kT#ic|7QA53X?w98$yojT`18jHo*Oc^ zSMLg_-ta~#F*ewtw3#UT~ANt z>#?Pfyks}rgPpzqd^$LI2p#PAX^-Kg;IXkW9Q66QcfwU09aIj5%4P^E#<U6BXHLgGNqNdu z*PfDsPs&{2vRDosrVm!BHXt23w<8?mc6Hf9Si1M95RSrVK%}87pH;0sN`FCLytn|f zo@LmO!I23pJ#RL`%Ag~HCmvO5COxM{C5|?DNPY@hp}^HWhr8S1-t@;^H*}C#7*7a{ zm418a@|WxlGt=FTVN0avUwLEWnQXDIg`^MgnaD6os>0uYG&dedz1)Kq^N7t%EH(+h zoYk*>{99eKS>!k|!xS%gj06NB{+FrC7|c;?d;H8rGAroqv?jZnv>ZhU$9KFQV+3R{ z*1GV`x^D@jH!23RCIZF!3|4%^SZzAL^j#|}XTRyzt>)fwbIbtbE^h$KB8O2j;}=?b z2+s?EVpqqSyt>1D8F(a#Pg_(}X?hu&bKIA-mLR%c z0qH~c3dmFF^_c1#U*c%no3D_Pm%fzK!AGG6z{32b#!R9iwN+xybNr5jD`8A*Ki*>i ztK`TAPfw4(rmEcS8OqCaq&;$OgQJ8b4`n=#doN8yT{t*6CJDnZ5W3kaC)Wk{J$K#I z@f}YBW*uOH!MuL`x|aX!RIW}kkD!1?8=k4YQUeQlec;@+sp+E+MDj)j2BfZ9A}un` z=BOTbnO1_LFKF8q@#UZxxMCrvx{{^6yyYzM)yGdW{0PF>B0>2T<}b>RpV#L3tzFe& zE5a!A$!f)_EoQ!V1ndY5aE}Z)_XbbR&pbx7KJjh${!^qJM06Z5bkF#yCu>u?$YcDp zPWesCV$D(dY0AdhaAd?_DRs)52WEpXHAmeV+LfYs2YKY)9p4jX=JC|etXqD2lh)hJ zdlyIAvAfpHqzdSnPa$$R0>wXw1{or?8%lG0HCzK%k~L+T_m*3b1Eh`N&5(yNjrY4r z-POjeZ;@738XDEI6b!QK8ELb#R2$N`-V|Pte#(dbJ5d9QM{{c$Ta=a>@>u@gzA)@e z)lxkXX2s2-f3ezkM~V{0X!O}&sE0>wkyY6HZwnf0XoRe87eo%bFBl{r_?M9% zmoi>&<{q>amankDGX>9#$N~e;E32y?$EEQ?oW;KD|2kW=ke0om0U4m|y*9@(muKc9 z-ZSOYvexZ&w*08Re05a2@~F7@cR{W;qiajM96cSVY49{zL)-RS;7&QJXhc)Ra&a== z!C^dlQEDwk2&T_iu0aYK{5GP{=d#$$%p}BKXh^#!;vMo8_VfkrN*o3F>0a>l;m_PgU2Ub-Jla}&7lA^)(zRzK$#Y^_3&EEvSX{KCz_VvSF83uZ3k zwF~%rGcRUZQ$pSN&UjoZbL(YG56)!W*!iZtEV(ADhKvgvCu5=}HL9q*PJl9IW0Oie zJm`h;dLmu3OX+iKCdcAxEMV~*7RzGK zr~x9~7+%P}ESvtJ>)9btZ+B$&F38F9oMfD{if}X8`%^~|qvJaeEHpBqW6#O(W_`T- ze*2rO_-&;pQ2M!gak=x-34jr90q?7#lJ+XjEs%hMxhLGB>olk3SJ!=)KM)8|d;d1J zdEe07T=?+Pfkbj|a2m)86v5|y>#PJf96>3fnPRi~Uravxdw-}2>HB}>bK^fXnOUpO>0U;VJP{`J6!j(Zf=&0c*aKm zC|Ch~x7$Me{=eMA#TUlqcTD4J0_D~x*8xfKIi9(J9ag)ufB8Ofa7#KlEpG(f+{)E| zZzR{0)8WCZ*)^v|ZAH78o~<2P_$=}TdK_R|&`bN7`Y(6nV^NIA}CtV(z3^&9KOF< z+Dol4kU9IeX1p>Q!*}^{{Lmr!5C?*he6-a*RucT30$Jov&ZaIIcva9(aM-##)-W!c z-XC~3KmRPPL-;IXZ9x4!PrJV~h>MF0V3f6a?V?beQ#gkpP_SXZ(OvibF^ooy-j>xW zsbK0OdC52^@e$v+#nDtprqDR8m=#%p7f^fCwm$Ide%j3E?j-&f_7~rFbC<2ed_ESL zlOrriZ_#waIC?4TBFDRK**mMrkWEeL+seb+EqlG;FJK$E-KEi&PlX9j?!w3|%*F42 z0^zOs%@3v6faGAa$t|w`B=%R3%)JN=!und- z44T1FdpX`j3%yed*?-5k7q?cE1&RPsO?J4iUZi zql9wI@}UMB25tl(Q@`!vL`=uk@H4UQ+qQA4SayWngLOlT`%d=(dM6fc&`9nIL4>+| zqzVz8`&@$SjE_XV(;=5Q)*UR(wzTNF>KF3DvXuXs)GXQvI^EK8%RjIen zJQ@k&3F^ruEjdV)4`Mp4+I6MqGfd+Nd~y7i>6|mmZ^`|K*~P ztgRfM>w6v!yyTmCW6#G|!|Uj+ahT*Q84yhTwoli9H|Rd}0k>=StxsvL|7R~2C4?mh z#n|8=us&oKUlDwz%QuQHdK)|;@?*JY!MyR!aY#P_G&TI?Ny=Jv_wjWapMn#PyuEhM z?rfz&n%jwht`R6oO^H{UxR~E8NDBXRw^S=rzfI`p-xK)?sXFBx54Kd5tU98-C9kG7 z?3|i^cykirKL9oGLRHcXA8CfE`W5+X*Q)lgK0zdodgsP+Z`g(tg$OV~5}9B|MG-hL zC20vfDIn6iT5C%W{w-Yw229)X!qn-?j=st6OG%cbrH-4GFG}&$Po_2rJ{|M0PGF{H zW}2+NTz%b-!|0iti#i@@<;wp4rl}AR;ORM?$gB`bL-V1#)qO3(l1WR%vDV52QLwSG zu}^dcAV8k9wt6G#UQCx*@*5fJuRomlJ~uZp!CzZ1$j&~1g*IvN=H;Xnb*4tcxvKVw z=w^S+%oq>TO4%Ou1Y^m5tZ{q!l2*djkwZv`G{J4YUXc@MB$ls+=rddWroS5Th+6;Q z&^2Nn&B=MV`@_v~!Q8^gi2u!C;?n!2ogH<+Q|^1&7w7{fAdX=3o~dJ8|yrOuCI^%=h@fnsb(`16J0~Y#hDpx9UUEg5e|0tUz{SK%l#_ZH)l;h0GFwn z@JH*}qWVFNO^=0G_+M{fz{Tr`UX4!rJ$%8J2d(+}U9IKi^)I!=={0_C0(swPlAp_; z>YgJbh-pOqPBXMP$kCtpJV+IL0lLsd_tM3=8mlfBf0)t?kG1{%5D5*X3_)wK>k}6;iIAnx;*<;u)+O>e|GZ|v~n)MLQ! zz&a>oAE*LC7|ZjK`h!^(3HVtknic2$*_CF{N#WuBRtdR=v~G;YD#^C#Ewk|I@f$BM zFDZs!ZW9+jC!P=d=WkN$gK7YQcxdw2(g^8Hwc5&yyPBKw>pUH(<^<~IS33EtOf>#R z=|r8B<4IuUh>Zia*1zy8n2l7mVPIC?oj z>(vEeEhuTdq=AMsGr5f(8hgCks*A8(P;U$6dA#qG+t2)g%KGC~tdSTQud(o(+*3qK z$p^cop8^MM;#`^Yt%;=OB_Es8t^eK)FC`S}EwMIk3kmM%E}5LTei~Qy?A9N3juz;< ze85kT*wCk%99o_AnppSgiz+!%)}-QgDt+tc+-)|75&p{gUus~WJ`j31%&K#9UxA3V z{MBKgpPC3(cC^z)^w@DgClAgBf%b3(PENp2?7ttf{a4bv-bv;WhF!NcP9AK~iRymq z-O7RtUA$M@ajt~^jjA(LquHz$?g{9c{FyYWhGc>wSG8)M3=ELhwT_QTTc6I^#7mQ+ zz_s8MHG)+IwepOmfjel*skb=~<>Hr^7TmS5OL7!Q4}|r0rJx|69G7sr?_WHsk(Z%v ze)c*0h`}}uII$?vkk6xvAy~AOJ{E&XgcJ!@BInXcl}6ZARP2Ay4fK+sNYyoxHyB(4 zH8quomqc1|+P@e%_bh(9C|UTHBStE>bG*9G<=q*R^+dH~dCxA|uf8k&x_&pIx^~x| zN(?dQ@-W!U)4j9LuRcq_kX17_dM;}jn)VmfDXwsBdncflZV93oy%l$Sl(DDh%SB%< z336gg@5Ipl|e)t^HMW zf$tM#g@}U3S0|4~eg++;K6*ALafn#Q56~28OC@q_W9naH@oazUv-#vKE6)7-$^6~i z7vhVToM!49vwf|u=T~RDbXl}DomXv^N@?B!<&6|F!Y3R*e_{sK%7tE^@!ep9=I7@D z*@Qol#ai(yku@}GdN-yWNpg|IzCW%0@83x4>v~sq0z7;SbBiHitR;EIp7GEN>6pi3obGme9e$!gLe)}dlJ#S_qTSlwuYrpAn&ahlty>G1!gQ+f(*Tp|1WN%*2PLL))* zjboLhghZZfPU@{LNfZh}7G-N^w{ON*M(!i1&F|g2u=+0h-1kO?F3xq}3FyhF*-*Y9 z)4}6E@9r?%Dr{`f8e;u3%aWVdm*lscnRl$=y?5Di+h2SqVpTB1g0~S{Sri^{#y)K> zG?Wm5PDAH8g0FZoh%=zYVGC!5AW&c4k0Oy_W-DHR9u9;*Jc@ko{kcFBv$JWbsZAtF z)2w?EB>dEWkb4Tc`G~o-rbKf-`~zGY$#PB$nSRyb@!^WDdwclxsH}p=xhe7QcPk8r zy~u~zY!*16o70ymU7eKfRh&>LWr6}9Z#+rx%KG`j;^KAW=Ov5c-cca20MuQ}7l3V;5<)KlWe*JEiBQieQ zTIUov!L1kbl@)!aIsRhv#j4={t)SUHra94r*Hu;VCaIjZ@Kgjbw3K_hxw?w}ewTrB z36#PB#Y5tMzDFgUtE;Qu76VVPOGy8^RA5@7L=%Am|yNNGmdX)B49J2R&PjU%x7K|8nzVw(738 zDf#cV{fY)Y(>!+yb&SNY9nI)&TvUh|op=2jp75Ue@k1Grbo%jsPv`emX_;cAUhU{( zXJ^3fY*iG>+F}W7K|LHx0>w;R^YOCR99PH_81|Zhf*)FScEc$a>8Uj#37N=n^AAb_ zAMind%@)91N%&LeuW|K+yZuv+bKh7jq)N>*M`vAeg|X9ioD27bvREkj;^ z=J}|}PkTF~?8j`C2}_cSpn&E(nXQ4$DvFw&H+<4TyQvh}bX?rrBcr1?MYAPTPPV|q zl_<>4W`k@f%ekX_m9 zkA;4X|H&xM3!kQ^DiGFu5F#{+jEq{XULc}DU3~BF{G89y;aiyjEW$aN#iB1@tPT2F zfq!cCpP@;tNO=@O+@vf@YgF>sKwjk{`BB1 zwvytOZPtBp4X)IMIvZt)oX2k6oiX_2^NMr^OIT%uYuEtV3J5kS7Od>->=^Ob51K{u zc14EZ%M=j4eiVR_xyh$H0tX$QM^;Fp2WrQwH8|~lLpwC$fldd&voh6vhmXc0n#3OC zk$#&<9o(kXRNvB1erOq_f8VU}H#yglAN)-yc;s0UaBO!ddw4zGV89x2C}*cbO!yTc zz)*E^5%=-?b2_t^=6j(6Cf9*ku~ap0ubs{5rYl><_Va@-9I*m1JgLkKtxGnwVxkFO z1S2{F(r(#ZI>+S>vM(?5G{jE(MYnKWKJlcQRpoy6m5B^+KOd+w_?|3HSa{OM(V+WF zjQg|T**k?Z^a`k?OH4WF^13}Z@#pk{Cf~fdpXK>_lF#AJ!{~dT``iNQ4|YK@1^XnS zOPU+Dd0*xnz8pL>q#mwz68rM=nc?@KO@6{Mg;DhHym1l&BP_m*ywFGzttEZ+#zVX8XEl{NHm5|rtJS7`u`7%e6Y)%El(-c z`|{yx<+OBkMl<}RpUQ%me_lP+JgCv`a5W>qw9|G!8*hjFxwlBF^q}JK?sLDMsozTV z-Ex!Q>Hg2@owv|^k{DaYScmQaJ=rqfpEK_dBg86A=MQY|<~xbG*3V)~it*og#V8_z zJ@N%P9ag#1Bq(>~alae1$x0Q5W?z7-uE7GbfzSuD!7bDDSNs3A&`)zy$NW>feD0Sx z(}F5+S)TcRlk(TGz2JIE^3C|qodgnz-O1YE&h%{2pE7b_U8GkoJ^SY{H> zh-*IJ)-78CJilbAOn$ML_*vdV0oD#2QNmK&d@cTad%;X+Q4>^2C+c2&;B2CvL%|AS`Pkf(KL z?#CgdKv+4G%`9K5Y}{!J-0z=I?=Sn}+kOn6f`u$&o>%*|U5EeXolKwvaV+n`!EuwO ztciPQJf(9FyX=XskawatC4W%?mTg+yyx zx{D!befgJSv7VRL8HSVv5)Aq`2?1_|h}1$xE0zyOd$_wGMbNb^O4W9B8!*oH+6#gYw3z z(ek{G_b`UGkg&zc5xHpbX#=f$V{1>9m3rCy^f#rzYgHB{1oV)1N;m{;!vg zL~5%oxU-Ty1a4EDo}nQ3g~|aRf)+5BVkJ9v(gG$iDhrt+>zA?_Ui{SbA}>?Q#~CZJ zp-|ZEja(Q)Asz2MuylX0{0ghU$LCQL=l|0JoB}@sT7yw+tVh@K=co~JJPfq%O=F3D zQTa43&)q2%8?Gd7yS4t`H#|UV_y_=*JwnT$ zO0_@P!F4E(1ByZwbS%$;zH#x1%-fW{{yR~7I=#DAO!B2#c1hXQ#C@y(m%q04x%j_{ zxUW-9%V zUdJN;3&(*3M+1g0Vz?w+?Ki^OQVfg;Xb4DE^+6H{AA@#~j>>~Yqpzzk<>1imr1^*d z4#`91u)ep&H6)~N!^Urp>hqC_O2j^my{f!UU8s4U>c@kpFf zJ|FP`e@}PD39gOQLV|siaomP2^~2c2-K>93cW&&{?szpKGKy5G=b|->T1=+=7#w_ExBUN38DS? zNWIS_JunPXDUKh6(LD{wbE-jqTD)?l9r0JDYT~A4k1aK=s|-TA8xnDV7H0H&5V5c= z2%aE+NM1fAb-gzQznvtEELlX`SCV??yY-{@;hYlU<6fAM0_gnWhh4LW1MQD5vihpD z`USa_qWRgRW1~7lg&t33z91*6bQz(}RFHj0Osx#EW%L%O9-dCVW`=QM?vXC`aCl67V?LfN1S+THlWK>tOe1mM=rcpL;? z2X~(-XkcaVr1CnR4>k7~>#bfC3Q3>RZ0d2japvUXJU=;;wkFR&FDD?wpy^U@dJw$b z>1NK_ikNpvnq^j>F^i_kFGgBx{FMZG7ubFJFIS#5I+PI0z$wCBfJiIEzU)QgDtJBid7_^b3Q|5 z$R*n6y4zRQlO^^o?;Y|gj)L%2jatM+etHc6l-Rba&a?I(x5)X=R!?L|yDm3&TW)MPF_Z+RP0}H zd&ayA79frQg-s$po3-H*EyZ;Fo!#|W_Kn<<}s&rn;(B;AB5s-PURaE2{O_7|{_smQYDRAG`{+1)4 znPQX=haOhv*Z_x-3G&CF>%Cun^xYk<+IR6Jk@QZ9hQp&caQO4Hg5Nj~-8Nu-lY2L__{>f>s;2{L(5B?<{O{NAZZoK>z z)0k$oO+9Bd6}G|)WhM%|WMl?R6>q0@IdS5zkVTU$G!P(9*F? z|C8-fIw79qgR2GEdz-apULkw>z-1D*bNZk#3QPFL+m8(m6@_srtXv-vNzf!-^DDW> z*)}=Kik~f6(}x;c8W-{{z0_TpbJyPz4>rrodB2ZOqbyCVsiY^3?N7w?sj{~N0i>|n zkAAjH)LLoZAP`e+ZS72z#CmNM*2_{Y4%;LSI4pS9Y@c^XjV-M#c3`#Hr2(&lg+u-o z{=8{(XD4s=Hyrtu;g2sRsZmHU0z$zwX`Uq*l54a#q6|_3scrRsqY#ZRc}FQs$5T+i zsNCq?l4EC;;L-eVb$2C%lKkJGt>CS0oH*zr-DOdc&m_n}4Fng)F!7k4&F+-M6%-j^ z(5}WzA%?!%PnjQwD7ZByk{ziSqq=Wk=0YG_ppiW@e(wl)ul6<@k>+`R3M!Xxq!V2c9*U z%vPC;SWOlE<-7UA7}IQ&%KXjV_yu;sagCYhx0}-3K$ppaw=%kh zl2--KWPEy~*t>3LD}n-lYsSSbLM?my6lAY2N7yta=|Q??`?8(eSIud&^Pb~f|E9)6 zdV0ghrB63yaYFN*sxPnpj!R$fj1WxCtFhAF&keC#{M&xEmdXNxW&CYDb&4^6yEDX2 z+u|~``PZ9Y-2Nu4VeHApzl5BUJv2s1sG=C6&8YYGlA{Mdikf1@x?gqDRt{vx>GJdp zIajoww}>f{dT&b1yABdEWvC5Q(7Q+W$9f$N^}$S@`|ucd>!iOoT2|O?k8e>ho|QXv z^CB6&xTCrj?to8Png8rqubmAguC5dk#V0CS)qvB0z;{nS)BzqXKj)xQ-M* zI1vM>?~z9`Az`Yz`UmrBj`C0ACJSH@X~Nuyyy=Gz?{9ApdHpwu6hGG2OJt7-^ICLW zEvFv3%^ek2bZZz)KwOtP0>A9o&FgIfRU1F5Odb+ruSSJIRGM4d-ng|sh`w8C@##Ka z*cE-3JT5Heyx4qpJH}npb|H50$HUh?XWY))dA`wRwD+vy`)&N!{b>h= zU?%9&{6eGaO#LhV)6!qJe#fhi(yC5cUysDQkaTp5CJzN3?>JFlqa+h<*YEF+uKoXB zI!n<4l$ZFlwEe~AYNM<3AKmH$xqdkLoWY8Ws&6m1#M^ zMTL;!UO&LwH1xQR&zUzBrNEE-SP`Sti})=b4(usV2yJ8I7r%?Ds;U|qrcYgL?W@kt z_ZLGyTdJ#ngXi~uD$o1H`5X(B3Qp>IT~P#j<8>QNA;EUrge!U)#oexu#ZbCMpdF37 zwf&WkA&x`yWG79Z{NpYKiBU_8gGF9#`D^WZmNb6}XAziyhumu!`vC|nDX(4?EC^$M zVzrs`W~HAnK*!2@D;uWx`kpbceXmMv&+>O}*9jl{_!E}YVVUD21;D+%l>P2*u+k<$ z_Rhw^Q5z8rqor$^jmdOQ-jSmuDyBkE^SL@?II}f-T8j$swPpw0FwKe?BEWOi6^IFAgkPYhBXK`|Bb~d`0 zW?ZgqaNqsK;QuFsTr=NN%L#Ybk@)TFQC4rTd2nKy_ z6l$%NZgE_^rkNEAqx$ikFK~V4;+F+ooMGTL_kCK~N2`sXe>{%^*1Xu$UFX_plEP?$ z<%B^52rPUu0{p?vnwx`3Q4jWo2;&-yKi!xKUVl|#yj?7EQRlu0V&FAmbQ(2 zU0AJ}5$$u#kE^7#Ao-`$-Z4VlX#l+^6xz&1mlZO+Qso+sFEX=4NjI z=^H=$lyktn+3i<6CR@~nR@zS()Y#YvpslqFm55>#?b-$}zT`%Luqt(F2F<$~mz`Cb zy={|iv0~^tw&K83i2DRONoQ&I@n9p$#`%piiOHs)bnc2nHg$N}>k z4bEo>>uux`n&d!;1@Rz4i=y1vD`jNQuxvpT{16>Amr)0cL7V|Q0b*BXS?}?$JBJ*i zE(_o1C+$hM->$SeeE5!rhQXrVpzxfR8=qwFL@O2G(}0xY=|x<=5;Fhrdn5-SvGOf{ zCn^x*Xkn=FmASdOwSMOvmvQ_dDuY3>8+Bmp8QXNZ< zw?O5ga3DfJ=#{b|JBv1l;N5Qp&&=wLjgsmMv7+g1=QgLcYdRWu1}}CmPl>qdjl7?Q z4A&iBVM&7I!6%@X!2ZtF*_5hhH^1JV0z}o5+wqq=8u$(W(Jx{5g2=e6S@G1t}`LObdHIaY)0n5r<;3wYY{a@H8KH zwmP7uqJbO<5BCc0k(=lV2@Te_czeljgAXEALL|MBB4%Q15oJhlsv_#P22h<+3^jnL z)+)K{c@pN)Y8@b80U?2~3KQHBFxqD&%NBD348-3{cXGt0arUjtohkX8B zOMUya{7xwHLafAE#S%x#38FTUwnHI2Mt1V9bNHkY1u8Qla>d^%9{sHEK^ly_%wB%+ zJT_ZxxDIYg2U1KNWXV%zz6fA;9x9$NDAQI8x9k2U3Kt?X181*Yc9*|;T8nlrI8+0!Z|rTp=nH<3WL65 zt(%(JUX9H9f0wqNI>76=;ymP<5-nIScBjAh(<9$X`felyT^uBOQ9I{7DisQqC&`hKtApiJQCINH& z=4~H_6!>dD2)~$0<27Md%h1x*CC3kMab1?M>h0UuNXrE3y-gD)(0vz1CPYk zD8Wp|gG|gU0t5&$rovB8ho_a$uu)_PjwLlHcrZKJnz9ZvH0j+$@Bi?rnkr7TfLuym zo+E@6f=FiUVTB=?Xg{C4XTSI6@F z{>}BABQ?(pQV%U;rqC-ebMh<7KYt@Pu{J6D*wWXZWuS&0m;CCd)IDKFJVgSQ%Q#q* z)uz{&g|_*>V@i61&&dI1?fVd=k>(H>VYG-iRIQSNPb?bh(NPeIZ6{dmXm zf)meR2!vH04Jqqi#@WkyH4UTyVB%mg|GB-{3>AR}D;%&qC0_WKA-(9}qzOGFmbPGK zY`-^JfBF>$b`6*3=kZrplg)N;ZV0 z_P95iWLOtS+w<{laZL0!7t~sM@k_bdy6{k=&P5Nx9<5sOEVf&>#IrmudDHZm(2_LY zUb|6frm3@`GwxxspIsSEQ+tc|S;2?}=hyn}pxZbm{E+I2XT4)Wwk`EAiLdXbLwJZ6 zD8J7bgC|*O_@#sug%nuJL~(?Ih;WHL0p3b@9vCTbHe_$1hTzx6{Y!&GZ7jR-n^PC? z7#R{8m-6l9^d!!D%>iPQd>-ieqlUKGHOKeQWN!N^7etBk;I`A+AIy1-M92%gV@8`U z$a^^uD7E}%jP7hD9Q5^0X|;g#{C?+!z=9`X#@7G3o+bW?BFG}xHRx_X-4}}qm%uTI zQ8$6lZzi9v7sf~+o|a!-UD=Ho0#jH#wRRPDqTck_hOMC2(Pv!pd{``l^wo@^#d56` z77+N<2lsrcTQd)|(*|PwWtABA)vw&br>N6z;p zhu~Z^*XOpaPstl&;xBEP3QPyVTigU6BNd8#LP>diW64T4$*O~R& zhbQnP?l-Dzx6W)5Y59;aBwP-U^6E4Q3}Sg9Z2U5I5dBmr;F#MmXmhuMuGzWmeDOZ; z`{V%g;K$Sv(XMW0+KQV|J40C7a}fVK3OqtW{=Aiss8jx6@KS{1k{U%YF0nH>z~GnH*?AVR+vWdf67cwGs(Ir>Qy z1Si(JpzLN_VIPqb#ERO+Y)2F@)E+V=W&h`+;z~=Sc`KAISpou5!~qvo345!SK9 zA1=!}whQeBhWcaK{$C6FV;KGZC0TN44N?_MZgsRe=;`YtFfd$ar<+g5PP3IXV%|K= zoZ)qjMZl8KMBxjtmYF!cq%O%n|0NynbAOq6_tt<@#BMkRJR_U++3}r4_tn3w<|?$k z&)(O^un9;e_0yX_(KWG+&fpg+UZP%J5@BdZy8BM#MH_|T2o37BG*1K;!P5K30@6yL zP^1l)a^KJtgg~^i6|lv9Ew1vE6KTLeo?)rZblgwV5bEq+UNi9Fvk}Vhre}!^h+HQxEiKBIyMcp>qU@1d>3et-_#kZvNA^=CYEmyF`Uj1B3Y2)LO<&e#;9p zQ~ zh%qB@z(}zedocXwifC8Vc@7Q2RnOqJJNXXiaQBwxofm_@lxQmJ0FH!oKdP|9hNep+ z5$>`yqv=@YfMTb9-r2>aO1ld2PNUSs!~|ICnA%hHsYx_cx%;za6Gh6O6)Gtfu6QIE zI5-$OC>sKg&1A{Kk0sjD3?yo=w9lH0CmLO8>#s-L-#0n^kcgkoRXC#pgtu8zz9&p_ zc0+4U&s#Df57s~kW)SjS1IdT;xCR-!DCcKBw?~t*&M%$72&G17Mk1Op5l%|h`p>G> zRTzp&Pol;ng{h0@DyGzjAWPLgxNolvJV(dPpWe40rklwjxO<|}$f&4)x^WK~Im(zI z@-NkXNVC*;6PHo`cr3r)tYV++%Y+?!=-AK;E{iWF_2{h*wte~r3WsF~7-mc*e(Dl= zMHcd{Z^AGYhdGlit+!1jPP51;Lr#U0l++%IK#197%+GZZa)jYyql|dSC@0lmDrF*^ z*4|JR&UJD7VHy-g9u`M03Tl1ziaAsqg5O65ZWM+jQX(fSwOSvm%kx_QTZUnO{zJe5 z2FHAQJ1ego7Jhhet*8c*PYW-kQhjw7K48p2L0E>AMF<;^I2ayQJhV;e@&^^Aki?WxlcMp! zkI13~VJxEF%{9xdEDF{BPw^b(nE3l&vB~0hMsZQra%i!=cF8D&qp1^NaOcFC#g^@D zWvBqWZXa%(Q~8jBNtaqMOlAwhyjM%$#KVPwPN05}V&kFaKBa^32+mS{pP_{7p*?m8 zugzd+rkobf3la*^h0iRR6-Z3*HSBu>DDjyBUWn2&EHJ0Sj~AZ`lT_O0TerSWh2s$s z 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

?SAcZ6G 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 YXT3XYM?y{>hcf|UW^@x?dCepCKakpsD*ylh literal 0 HcmV?d00001 diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index 3f1c0e81..d98134e5 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -132,7 +132,7 @@ easy to use. Here's what it looks like. You may also want to check out the [full ### Subscribe as raw stream The `/raw` endpoint will output one line per message, and **will only include the message body**. It's useful for extremely simple scripts, and doesn't include all the data. Additional fields such as [priority](../publish.md#message-priority), -[tags](../publish.md#tags--emojis--) or [message title](../publish.md#message-title) are not included in this output +[tags](../publish.md#tags-emojis) or [message title](../publish.md#message-title) are not included in this output format. Keepalive messages are sent as empty lines. === "Command line (curl)" @@ -257,6 +257,14 @@ curl -s "ntfy.sh/mytopic/json?since=1645970742" curl -s "ntfy.sh/mytopic/json?since=nFS3knfcQ1xe" ``` +### Fetch latest message +If you only want the most recent message sent to a topic and do not have a message ID or timestamp to use with +`since=`, you can use `since=latest` to grab the most recent message from the cache for a particular topic. + +``` +curl -s "ntfy.sh/mytopic/json?poll=1&since=latest" +``` + ### Fetch scheduled messages Messages that are [scheduled to be delivered](../publish.md#scheduled-delivery) at a later date are not typically returned when subscribing via the API, which makes sense, because after all, the messages have technically not been @@ -305,7 +313,7 @@ Depending on whether the server is configured to support [access control](../con may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can: -* Use [basic auth](../publish.md#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` +* Use [basic auth](../publish.md#authentication), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` * or use the [`auth` query parameter](../publish.md#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw` Please refer to the [publishing documentation](../publish.md#authentication) for additional details. diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 3f9ea2e0..78e160c8 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -190,6 +190,10 @@ Here's an example config file that subscribes to three different topics, executi === "~/.config/ntfy/client.yml (Linux)" ```yaml + default-host: https://ntfy.sh + default-user: phill + default-password: mypass + subscribe: - topic: echo-this command: 'echo "Message received: $message"' @@ -210,9 +214,12 @@ Here's an example config file that subscribes to three different topics, executi fi ``` - === "~/Library/Application Support/ntfy/client.yml (macOS)" ```yaml + default-host: https://ntfy.sh + default-user: phill + default-password: mypass + subscribe: - topic: echo-this command: 'echo "Message received: $message"' @@ -226,6 +233,10 @@ Here's an example config file that subscribes to three different topics, executi === "%AppData%\ntfy\client.yml (Windows)" ```yaml + default-host: https://ntfy.sh + default-user: phill + default-password: mypass + subscribe: - topic: echo-this command: 'echo Message received: %message%' @@ -263,43 +274,31 @@ will be used, otherwise, the subscription settings will override the defaults. require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password. ### Using the systemd service -You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service)) -to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started) -if you install the deb/rpm package. To configure it, simply edit `/etc/ntfy/client.yml` and run `sudo systemctl restart ntfy-client`. +You can use the `ntfy-client` systemd services to subscribe to multiple topics just like in the example above. -!!! info - The `ntfy-client.service` runs as user `ntfy`, meaning that typical Linux permission restrictions apply. See below - for how to fix this. +You have the option of either enabling `ntfy-client` as a **system service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service)) +or **user service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/user/ntfy-client.service)). Neither system service nor user service are enabled or started by default, so you have to do that yourself. -If the service runs on your personal desktop machine, you may want to override the service user/group (`User=` and `Group=`), and -adjust the `DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` environment variables. This will allow you to run commands in your X session -as the primary machine user. - -You can either manually override these systemd service entries with `sudo systemctl edit ntfy-client`, and add this -(assuming your user is `phil`). Don't forget to run `sudo systemctl daemon-reload` and `sudo systemctl restart ntfy-client` -after editing the service file: - -=== "/etc/systemd/system/ntfy-client.service.d/override.conf" - ``` - [Service] - User=phil - Group=phil - Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" - ``` -Or you can run the following script that creates this override config for you: +**System service:** The `ntfy-client` systemd system service runs as the `ntfy` user. When enabled, it is started at system boot. To configure it as a system +service, edit `/etc/ntfy/client.yml` and then enable/start the service (as root), like so: ``` -sudo sh -c 'cat > /etc/systemd/system/ntfy-client.service.d/override.conf' < github.com/emersion/go-smtp v0.17.0 // Pi require github.com/pkg/errors v0.9.1 // indirect require ( - firebase.google.com/go/v4 v4.14.1 - github.com/SherClockHolmes/webpush-go v1.3.0 + firebase.google.com/go/v4 v4.15.2 + github.com/SherClockHolmes/webpush-go v1.4.0 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.22.0 github.com/stripe/stripe-go/v74 v74.30.0 ) require ( - cloud.google.com/go v0.115.1 // indirect - cloud.google.com/go/auth v0.9.5 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect - cloud.google.com/go/compute/metadata v0.5.2 // indirect - cloud.google.com/go/iam v1.2.1 // indirect - cloud.google.com/go/longrunning v0.6.1 // indirect + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go v0.121.2 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect + github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/s2a-go v0.1.8 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/klauspost/compress v1.17.10 // indirect - github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.59.1 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect - go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/grpc v1.67.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1bad8365..d2d2e7ab 100644 --- a/go.sum +++ b/go.sum @@ -1,215 +1,116 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= -cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= -cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw= -cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= -cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= -cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= -cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= -cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= -cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= -cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= -cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= -cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= -cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= -firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= -firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= -github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= +firebase.google.com/go/v4 v4.15.2/go.mod h1:qkD/HtSumrPMTLs0ahQrje5gTw2WKFKrzVFoqy4SbKA= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= -github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= -github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= -github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= +github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= -github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= -github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= -github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w= -github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/olebedev/when v1.1.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= -github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= -github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= -go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= -go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= -go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= -go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -217,14 +118,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -232,62 +140,30 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs= -google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= +google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61 h1:KipVMxePgXPFBzXOvpKbny3RVdVmJOD64R/Ob7GPWEs= -google.golang.org/genproto v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:HiAZQz/G7n0EywFjmncAwsfnmFm2bjm7qPjwl8hyzjM= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= -google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= -google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237/go.mod h1:LhI4bRmX3rqllzQ+BGneexULkEjBf2gsAfkbeCA8IbU= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/log/log.go b/log/log.go index 20ad6151..98d9652f 100644 --- a/log/log.go +++ b/log/log.go @@ -198,7 +198,7 @@ func (w *peekLogWriter) Write(p []byte) (n int, err error) { if len(p) == 0 || p[0] == '{' || CurrentFormat() == TextFormat { return w.w.Write(p) } - m := newEvent().Tag(tagStdLog).Render(InfoLevel, strings.TrimSpace(string(p))) + m := newEvent().Tag(tagStdLog).Render(InfoLevel, "%s", strings.TrimSpace(string(p))) if m == "" { return 0, nil } diff --git a/main.go b/main.go index d4600dc8..4e01a0d6 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ If you want to chat, simply join the Discord server (https://discord.gg/cT7ECsZj the Matrix room (https://matrix.to/#/#ntfy:matrix.org). ntfy %s (%s), runtime %s, built at %s -Copyright (C) 2022 Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2 +Copyright (C) Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2 `, version, commit[:7], runtime.Version(), date) app := cmd.New() diff --git a/scripts/postinst.sh b/scripts/postinst.sh index eae0b8a8..d923e7f8 100755 --- a/scripts/postinst.sh +++ b/scripts/postinst.sh @@ -33,7 +33,7 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then fi fi if systemctl is-active -q ntfy-client.service; then - echo "Restarting ntfy-client.service ..." + echo "Restarting ntfy-client.service (system) ..." if [ -x /usr/bin/deb-systemd-invoke ]; then deb-systemd-invoke try-restart ntfy-client.service >/dev/null || true else diff --git a/server/config.go b/server/config.go index 4fa711e9..1593290c 100644 --- a/server/config.go +++ b/server/config.go @@ -26,8 +26,8 @@ const ( // Defines default Web Push settings const ( - DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour - DefaultWebPushExpiryDuration = 9 * 24 * time.Hour + DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour + DefaultWebPushExpiryDuration = 60 * 24 * time.Hour ) // Defines all global and per-visitor limits @@ -144,7 +144,7 @@ type Config struct { VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics BehindProxy bool - ProxyClientIPHeader string + ProxyForwardedHeader string StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration @@ -233,8 +233,8 @@ func NewConfig() *Config { VisitorAuthFailureLimitReplenish: DefaultVisitorAuthFailureLimitReplenish, VisitorStatsResetTime: DefaultVisitorStatsResetTime, VisitorSubscriberRateLimiting: false, - BehindProxy: false, - ProxyClientIPHeader: "", + BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address + ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs StripeSecretKey: "", StripeWebhookKey: "", StripePriceCacheDuration: DefaultStripePriceCacheDuration, diff --git a/server/message_cache.go b/server/message_cache.go index 4f677816..e314ace3 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -99,6 +99,13 @@ const ( WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` + selectMessagesLatestQuery = ` + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + FROM messages + WHERE topic = ? AND published = 1 + ORDER BY time DESC, id DESC + LIMIT 1 + ` selectMessagesDueQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages @@ -416,6 +423,8 @@ func (c *messageCache) addMessages(ms []*message) error { func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) { if since.IsNone() { return make([]*message, 0), nil + } else if since.IsLatest() { + return c.messagesLatest(topic) } else if since.IsID() { return c.messagesSinceID(topic, since, scheduled) } @@ -462,6 +471,14 @@ func (c *messageCache) messagesSinceID(topic string, since sinceMarker, schedule return readMessages(rows) } +func (c *messageCache) messagesLatest(topic string) ([]*message, error) { + rows, err := c.db.Query(selectMessagesLatestQuery, topic) + if err != nil { + return nil, err + } + return readMessages(rows) +} + func (c *messageCache) MessagesDue() ([]*message, error) { rows, err := c.db.Query(selectMessagesDueQuery, time.Now().Unix()) if err != nil { diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 589ecc42..778f28fe 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -66,6 +66,11 @@ func testCacheMessages(t *testing.T, c *messageCache) { require.Equal(t, 1, len(messages)) require.Equal(t, "my other message", messages[0].Message) + // mytopic: latest + messages, _ = c.Messages("mytopic", sinceLatestMessage, false) + require.Equal(t, 1, len(messages)) + require.Equal(t, "my other message", messages[0].Message) + // example: count counts, err = c.MessageCounts() require.Nil(t, err) diff --git a/server/server.go b/server/server.go index a35f571e..22581d86 100644 --- a/server/server.go +++ b/server/server.go @@ -413,7 +413,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor, } else { ev.Info("WebSocket error: %s", err.Error()) } - return // Do not attempt to write to upgraded connection + w.WriteHeader(httpErr.HTTPCode) + return // Do not attempt to write any body to upgraded connection } if isNormalError { ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code) @@ -445,8 +446,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersGet)(w, r, v) - } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { + } else if r.Method == http.MethodPost && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersAdd)(w, r, v) + } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersUpdate)(w, r, v) } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersDelete)(w, r, v) } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath { @@ -1016,7 +1019,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) } } contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") @@ -1025,7 +1028,8 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } template = readBoolParam(r, false, "x-template", "template", "tpl") unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! - if unifiedpush { + contentEncoding := readParam(r, "content-encoding") + if unifiedpush || contentEncoding == "aes128gcm" { firebase = false unifiedpush = true } @@ -1556,8 +1560,8 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled b // parseSince returns a timestamp identifying the time span from which cached messages should be received. // -// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), or -// "all" for all messages. +// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), +// "all" for all messages, or "latest" for the most recent message for a topic func parseSince(r *http.Request, poll bool) (sinceMarker, error) { since := readParam(r, "x-since", "since", "si") @@ -1569,6 +1573,8 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) { return sinceNoMessages, nil } else if since == "all" { return sinceAllMessages, nil + } else if since == "latest" { + return sinceLatestMessage, nil } else if since == "none" { return sinceNoMessages, nil } @@ -1862,6 +1868,12 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Call != "" { r.Header.Set("X-Call", m.Call) } + if m.Cache != "" { + r.Header.Set("X-Cache", m.Cache) + } + if m.Firebase != "" { + r.Header.Set("X-Firebase", m.Firebase) + } return next(w, r, v) } } @@ -1885,14 +1897,14 @@ func (s *Server) transformMatrixJSON(next handleFunc) handleFunc { } func (s *Server) authorizeTopicWrite(next handleFunc) handleFunc { - return s.autorizeTopic(next, user.PermissionWrite) + return s.authorizeTopic(next, user.PermissionWrite) } func (s *Server) authorizeTopicRead(next handleFunc) handleFunc { - return s.autorizeTopic(next, user.PermissionRead) + return s.authorizeTopic(next, user.PermissionRead) } -func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc { +func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { if s.userManager == nil { return next(w, r, v) @@ -1925,7 +1937,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc // that subsequent logging calls still have a visitor context. func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) { // Read "Authorization" header value, and exit out early if it's not set - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyClientIPHeader) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader) vip := s.visitor(ip, nil) if s.userManager == nil { return vip, nil @@ -2000,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us if err != nil { return nil, err } - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyClientIPHeader) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader) go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{ LastAccess: time.Now(), LastOrigin: ip, diff --git a/server/server.yml b/server/server.yml index 7329d37e..bb508cb4 100644 --- a/server/server.yml +++ b/server/server.yml @@ -146,7 +146,7 @@ # Web Push support (background notifications for browsers) # -# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users +# If enabled, allows the ntfy web app to receive push notifications, even when the web app is closed. When enabled, users # can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push # endpoint, which will then forward it to the browser. # @@ -155,15 +155,19 @@ # # - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 # - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 -# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` -# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com` +# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. /var/cache/ntfy/webpush.db +# - web-push-email-address is the admin email address send to the push provider, e.g. sysadmin@example.com # - web-push-startup-queries is an optional list of queries to run on startup` +# - web-push-expiry-warning-duration defines the duration after which unused subscriptions are sent a warning (default is 55d`) +# - web-push-expiry-duration defines the duration after which unused subscriptions will expire (default is 60d) # # web-push-public-key: # web-push-private-key: # web-push-file: # web-push-email-address: # web-push-startup-queries: +# web-push-expiry-warning-duration: "55d" +# web-push-expiry-duration: "60d" # If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header. # diff --git a/server/server_account.go b/server/server_account.go index 3f2368da..acdf25ec 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -37,7 +37,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * return errHTTPConflictUserExists } logvr(v, r).Tag(tagAccount).Field("user_name", newAccount.Username).Info("Creating user %s", newAccount.Username) - if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { + if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser, false); err != nil { if errors.Is(err, user.ErrInvalidArgument) { return errHTTPBadRequestInvalidUsername } @@ -207,7 +207,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ return errHTTPBadRequestIncorrectPasswordConfirmation } logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name) - if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil { + if err := s.userManager.ChangePassword(u.Name, req.NewPassword, false); err != nil { return err } return s.writeJSON(w, newSuccessResponse()) diff --git a/server/server_account_test.go b/server/server_account_test.go index 72ba55c9..91db1bc5 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -87,9 +87,9 @@ func TestAccount_Signup_AsUser(t *testing.T) { defer s.closeDatabases() log.Info("1") - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) log.Info("2") - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) log.Info("3") rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -174,7 +174,7 @@ func TestAccount_ChangeSettings(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) u, _ := s.userManager.User("phil") token, _ := s.userManager.CreateToken(u.ID, "", time.Unix(0, 0), netip.IPv4Unspecified()) @@ -203,7 +203,7 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -254,7 +254,7 @@ func TestAccount_ChangePassword(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": ""}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -296,7 +296,7 @@ func TestAccount_ExtendToken(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -332,7 +332,7 @@ func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! @@ -345,7 +345,7 @@ func TestAccount_DeleteToken(t *testing.T) { s := newTestServer(t, newTestConfigWithAuthFile(t)) defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -455,14 +455,14 @@ func TestAccount_Reservation_AddAdminSuccess(t *testing.T) { Code: "pro", ReservationLimit: 2, })) - require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("noadmin1", "pro")) require.Nil(t, s.userManager.AddReservation("noadmin1", "mytopic", user.PermissionDenyAll)) - require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("noadmin2", "pro")) - require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, false)) // Admin can reserve topic rr := request(t, s, "POST", "/v1/account/reservation", `{"topic":"sometopic","everyone":"deny-all"}`, map[string]string{ @@ -624,7 +624,7 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) { s := newTestServer(t, conf) // Create user with tier - require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser, false)) require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "pro", MessageLimit: 20, diff --git a/server/server_admin.go b/server/server_admin.go index ac295718..eb362956 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -39,11 +39,11 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit } func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err - } else if !user.AllowedUsername(req.Username) || req.Password == "" { - return errHTTPBadRequest.Wrap("username invalid, or password missing") + } else if !user.AllowedUsername(req.Username) || (req.Password == "" && req.Hash == "") { + return errHTTPBadRequest.Wrap("username invalid, or password/password_hash missing") } u, err := s.userManager.User(req.Username) if err != nil && !errors.Is(err, user.ErrUserNotFound) { @@ -60,7 +60,11 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit return err } } - if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { + password, hashed := req.Password, false + if req.Hash != "" { + password, hashed = req.Hash, true + } + if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil { return err } if tier != nil { @@ -71,6 +75,53 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit return s.writeJSON(w, newSuccessResponse()) } +func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } else if !user.AllowedUsername(req.Username) { + return errHTTPBadRequest.Wrap("username invalid") + } else if req.Password == "" && req.Hash == "" && req.Tier == "" { + return errHTTPBadRequest.Wrap("need to provide at least one of \"password\", \"password_hash\" or \"tier\"") + } + u, err := s.userManager.User(req.Username) + if err != nil && !errors.Is(err, user.ErrUserNotFound) { + return err + } else if u != nil { + if u.IsAdmin() { + return errHTTPForbidden + } + if req.Hash != "" { + if err := s.userManager.ChangePassword(req.Username, req.Hash, true); err != nil { + return err + } + } else if req.Password != "" { + if err := s.userManager.ChangePassword(req.Username, req.Password, false); err != nil { + return err + } + } + } else { + password, hashed := req.Password, false + if req.Hash != "" { + password, hashed = req.Hash, true + } + if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil { + return err + } + } + if req.Tier != "" { + if _, err = s.userManager.Tier(req.Tier); errors.Is(err, user.ErrTierNotFound) { + return errHTTPBadRequestTierInvalid + } else if err != nil { + return err + } + if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil { + return err + } + } + return s.writeJSON(w, newSuccessResponse()) +} + func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { diff --git a/server/server_admin_test.go b/server/server_admin_test.go index c2f8f95a..8925702e 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -14,13 +14,13 @@ func TestUser_AddRemove(t *testing.T) { defer s.closeDatabases() // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "tier1", })) // Create user via API - rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, rr.Code) @@ -49,6 +49,226 @@ func TestUser_AddRemove(t *testing.T) { "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, rr.Code) + + // Check user was deleted + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "emma", users[1].Name) + require.Equal(t, user.Everyone, users[2].Name) + + // Reject invalid user change + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) +} + +func TestUser_AddWithPasswordHash(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check that user can login with password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, user.RoleAdmin, users[0].Role) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) +} + +func TestUser_ChangeUserPassword(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with first password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Change password via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure first password fails + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben-two"), + }) + require.Equal(t, 200, rr.Code) +} + +func TestUser_ChangeUserTier(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier2", + })) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Equal(t, "tier1", users[1].Tier.Code) + + // Change user tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "tier": "tier2"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users again + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, "tier2", users[1].Tier.Code) +} + +func TestUser_ChangeUserPasswordAndTier(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier2", + })) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Equal(t, "tier1", users[1].Tier.Code) + + // Change user password and tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben-two", "tier": "tier2"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure first password fails + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben-two"), + }) + require.Equal(t, 200, rr.Code) + + // Check new tier + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, "tier2", users[1].Tier.Code) +} + +func TestUser_ChangeUserPasswordWithHash(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with first password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "not-ben"), + }) + require.Equal(t, 200, rr.Code) + + // Change user password and tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) +} + +func TestUser_DontChangeAdminPassword(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false)) + + // Try to change password via API + rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 403, rr.Code) } func TestUser_AddRemove_Failures(t *testing.T) { @@ -56,23 +276,23 @@ func TestUser_AddRemove_Failures(t *testing.T) { defer s.closeDatabases() // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) // Cannot create user with invalid username - rr := request(t, s, "PUT", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ + rr := request(t, s, "POST", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 400, rr.Code) // Cannot create user if user already exists - rr = request(t, s, "PUT", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ + rr = request(t, s, "POST", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) // Cannot create user with invalid tier - rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ + rr = request(t, s, "POST", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code) @@ -97,8 +317,8 @@ func TestAccess_AllowReset(t *testing.T) { defer s.closeDatabases() // User and admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) // Subscribing not allowed rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ @@ -138,7 +358,7 @@ func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) { defer s.closeDatabases() // User - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) // Grant access fails, because non-admin rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ @@ -154,8 +374,8 @@ func TestAccess_AllowReset_KillConnection(t *testing.T) { defer s.closeDatabases() // User and admin, grant access to "gol*" topics - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard! start, timeTaken := time.Now(), atomic.Int64{} diff --git a/server/server_firebase.go b/server/server_firebase.go index 4a0cb7f9..99f1fb28 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -50,7 +50,7 @@ func (c *firebaseClient) Send(v *visitor, m *message) error { ev.Field("firebase_message", util.MaybeMarshalJSON(fbm)).Trace("Firebase message") } err = c.sender.Send(fbm) - if err == errFirebaseQuotaExceeded { + if errors.Is(err, errFirebaseQuotaExceeded) { logvm(v, m). Tag(tagFirebase). Err(err). @@ -133,56 +133,55 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro "time": fmt.Sprintf("%d", m.Time), "event": m.Event, "topic": m.Topic, - "message": m.Message, + "message": newMessageBody, "poll_id": m.PollID, } apnsConfig = createAPNSAlertConfig(m, data) case messageEvent: - allowForward := true if auther != nil { - allowForward = auther.Authorize(nil, m.Topic, user.PermissionRead) == nil - } - if allowForward { - data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": m.Event, - "topic": m.Topic, - "priority": fmt.Sprintf("%d", m.Priority), - "tags": strings.Join(m.Tags, ","), - "click": m.Click, - "icon": m.Icon, - "title": m.Title, - "message": m.Message, - "content_type": m.ContentType, - "encoding": m.Encoding, - } - if len(m.Actions) > 0 { - actions, err := json.Marshal(m.Actions) - if err != nil { - return nil, err - } - data["actions"] = string(actions) - } - if m.Attachment != nil { - data["attachment_name"] = m.Attachment.Name - data["attachment_type"] = m.Attachment.Type - data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size) - data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) - data["attachment_url"] = m.Attachment.URL - } - apnsConfig = createAPNSAlertConfig(m, data) - } else { - // If anonymous read for a topic is not allowed, we cannot send the message along + // If "anonymous read" for a topic is not allowed, we cannot send the message along // via Firebase. Instead, we send a "poll_request" message, asking the client to poll. - data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": pollRequestEvent, - "topic": m.Topic, + // + // The data map needs to contain all the fields for it to function properly. If not all + // fields are set, the iOS app fails to decode the message. + // + // See https://github.com/binwiederhier/ntfy/pull/1345 + if err := auther.Authorize(nil, m.Topic, user.PermissionRead); err != nil { + m = toPollRequest(m) } - // TODO Handle APNS? } + data = map[string]string{ + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": m.Event, + "topic": m.Topic, + "priority": fmt.Sprintf("%d", m.Priority), + "tags": strings.Join(m.Tags, ","), + "click": m.Click, + "icon": m.Icon, + "title": m.Title, + "message": m.Message, + "content_type": m.ContentType, + "encoding": m.Encoding, + } + if len(m.Actions) > 0 { + actions, err := json.Marshal(m.Actions) + if err != nil { + return nil, err + } + data["actions"] = string(actions) + } + if m.Attachment != nil { + data["attachment_name"] = m.Attachment.Name + data["attachment_type"] = m.Attachment.Type + data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size) + data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) + data["attachment_url"] = m.Attachment.URL + } + if m.PollID != "" { + data["poll_id"] = m.PollID + } + apnsConfig = createAPNSAlertConfig(m, data) } var androidConfig *messaging.AndroidConfig if m.Priority >= 4 { @@ -276,3 +275,17 @@ func maybeTruncateAPNSBodyMessage(s string) string { } return s } + +// toPollRequest converts a message to a poll request message. +// +// This empties all the fields that are not needed for a poll request and just sets the required fields, +// most importantly, the PollID. +func toPollRequest(m *message) *message { + pr := newPollRequestMessage(m.Topic, m.ID) + pr.ID = m.ID + pr.Time = m.Time + pr.Priority = m.Priority // Keep priority + pr.ContentType = m.ContentType + pr.Encoding = m.Encoding + return pr +} diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 9b653a29..2f5b7287 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -223,14 +223,25 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) { require.Equal(t, &messaging.AndroidConfig{ Priority: "high", }, fbm.Android) - require.Equal(t, "", fbm.Data["message"]) - require.Equal(t, "", fbm.Data["priority"]) + require.Equal(t, "New message", fbm.Data["message"]) + require.Equal(t, "5", fbm.Data["priority"]) require.Equal(t, map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": "poll_request", - "topic": "mytopic", + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": "poll_request", + "topic": "mytopic", + "message": "New message", + "title": "", + "tags": "", + "click": "", + "icon": "", + "priority": "5", + "encoding": "", + "content_type": "", + "poll_id": m.ID, }, fbm.Data) + require.Equal(t, "", fbm.APNS.Payload.Aps.Alert.Title) + require.Equal(t, "New message", fbm.APNS.Payload.Aps.Alert.Body) } func TestToFirebaseMessage_PollRequest(t *testing.T) { diff --git a/server/server_payments_test.go b/server/server_payments_test.go index 8da47a65..56d4cc6a 100644 --- a/server/server_payments_test.go +++ b/server/server_payments_test.go @@ -148,7 +148,7 @@ func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testing.T) { Code: "pro", StripeMonthlyPriceID: "price_123", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // Create subscription response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{ @@ -184,7 +184,7 @@ func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) { Code: "pro", StripeMonthlyPriceID: "price_123", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -226,7 +226,7 @@ func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) { Code: "pro", StripeMonthlyPriceID: "price_123", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -280,7 +280,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in MessageExpiryDuration: time.Hour, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) // No tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // No tier u, err := s.userManager.User("phil") require.Nil(t, err) @@ -461,7 +461,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active( AttachmentTotalSizeLimit: 1000000, AttachmentBandwidthLimit: 1000000, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) require.Nil(t, s.userManager.AddReservation("phil", "ztopic", user.PermissionDenyAll)) @@ -570,7 +570,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) { StripeMonthlyPriceID: "price_1234", ReservationLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) @@ -658,7 +658,7 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) { StripeMonthlyPriceID: "price_456", StripeYearlyPriceID: "price_457", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ StripeCustomerID: "acct_123", @@ -690,7 +690,7 @@ func TestPayments_Subscription_Delete_At_Period_End(t *testing.T) { Return(&stripe.Subscription{}, nil) // Create user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ StripeCustomerID: "acct_123", StripeSubscriptionID: "sub_123", @@ -724,7 +724,7 @@ func TestPayments_CreatePortalSession(t *testing.T) { }, nil) // Create user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ StripeCustomerID: "acct_123", StripeSubscriptionID: "sub_123", diff --git a/server/server_test.go b/server/server_test.go index 9aa3ef80..71a87162 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -84,6 +84,22 @@ func TestServer_PublishWithFirebase(t *testing.T) { require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) } +func TestServer_PublishWithoutFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + response := request(t, s, "PUT", "/mytopic", "my first message", map[string]string{ + "firebase": "no", + }) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 0, len(sender.Messages())) +} + func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *testing.T) { // This tests issue #641, which used to panic before the fix @@ -411,7 +427,7 @@ func TestServer_PublishAt_FromUser(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfigWithAuthFile(t)) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), "In": "1h", @@ -594,6 +610,11 @@ func TestServer_PublishAndPollSince(t *testing.T) { require.Equal(t, 1, len(messages)) require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=latest", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=INVALID", "", nil) require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code) } @@ -781,7 +802,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) { c := newTestConfigWithAuthFile(t) s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), @@ -795,7 +816,7 @@ func TestServer_Auth_Success_User(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ @@ -809,7 +830,7 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", user.PermissionReadWrite)) @@ -830,7 +851,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": util.BasicAuth("phil", "INVALID"), @@ -843,7 +864,7 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", user.PermissionReadWrite)) // Not mytopic! response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ @@ -857,7 +878,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) { c.AuthDefault = user.PermissionReadWrite // Open by default s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", user.PermissionDenyAll)) require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead)) @@ -906,7 +927,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) { c.AuthDefault = user.PermissionDenyAll s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, false)) u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "some pass")))) response := request(t, s, "GET", u, "", nil) @@ -954,8 +975,8 @@ func TestServer_StatsResetter(t *testing.T) { MessageLimit: 5, MessageExpiryDuration: -5 * time.Second, // Second, what a hack! })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) - require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("tieruser", "test")) // Send an anonymous message @@ -1099,7 +1120,7 @@ func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) { require.Nil(t, s.userManager.AddTier(&user.Tier{ Code: "test", })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) u, err := s.userManager.User("phil") @@ -1679,6 +1700,35 @@ func TestServer_PublishAsJSON_WithActions(t *testing.T) { require.Equal(t, "target_temp_f=65", m.Actions[1].Body) } +func TestServer_PublishAsJSON_NoCache(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic","message": "this message is not cached","cache":"no"}` + response := request(t, s, "PUT", "/", body, nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "this message is not cached", msg.Message) + require.Equal(t, int64(0), msg.Expires) + + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Empty(t, messages) +} + +func TestServer_PublishAsJSON_WithoutFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + body := `{"topic":"mytopic","message": "my first message","firebase":"no"}` + response := request(t, s, "PUT", "/", body, nil) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 0, len(sender.Messages())) +} + func TestServer_PublishAsJSON_Invalid(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic":"mytopic",INVALID` @@ -1696,7 +1746,7 @@ func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) { MessageLimit: 5, MessageExpiryDuration: -5 * time.Second, // Second, what a hack! })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish to reach message limit @@ -1932,7 +1982,7 @@ func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) { AttachmentExpiryDuration: sevenDays, // 7 days AttachmentBandwidthLimit: 100000, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish and make sure we can retrieve it @@ -1977,7 +2027,7 @@ func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing.T) { AttachmentExpiryDuration: time.Hour, AttachmentBandwidthLimit: 14000, // < 3x5000 bytes -> enough for one upload, one download })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish and make sure we can retrieve it @@ -2015,7 +2065,7 @@ func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) { AttachmentExpiryDuration: 30 * time.Second, AttachmentBandwidthLimit: 1000000, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "test")) // Publish small file as anonymous @@ -2184,7 +2234,7 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) { func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) { c := newTestConfig(t) c.BehindProxy = true - c.ProxyClientIPHeader = "X-Client-IP" + c.ProxyForwardedHeader = "X-Client-IP" s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) r.RemoteAddr = "8.9.10.11" @@ -2250,7 +2300,7 @@ func TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor(t *testing.T) { defer s.closeDatabases() // Create user without tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // Publish a message (anonymous user) rr := request(t, s, "POST", "/mytopic", "hi", nil) diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go index 89a36051..2501916a 100644 --- a/server/server_twilio_test.go +++ b/server/server_twilio_test.go @@ -63,7 +63,7 @@ func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -140,7 +140,7 @@ func TestServer_Twilio_Call_Success(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -185,7 +185,7 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) u, err := s.userManager.User("phil") require.Nil(t, err) @@ -216,7 +216,7 @@ func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) { MessageLimit: 10, CallLimit: 1, })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.ChangeTier("phil", "pro")) // Do the thing diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go index c32c7bf8..f7379511 100644 --- a/server/server_webpush_test.go +++ b/server/server_webpush_test.go @@ -96,7 +96,7 @@ func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) { config.AuthDefault = user.PermissionDenyAll s := newTestServer(t, config) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ @@ -126,7 +126,7 @@ func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) { config := configureAuth(t, newTestConfigWithWebPush(t)) s := newTestServer(t, config) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ @@ -212,7 +212,7 @@ func TestServer_WebPush_Expiry(t *testing.T) { addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") requireSubscriptionCount(t, s, "test-topic", 1) - _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix()) + _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-55*24*time.Hour).Unix()) require.Nil(t, err) s.pruneAndNotifyWebPushSubscriptions() @@ -222,7 +222,7 @@ func TestServer_WebPush_Expiry(t *testing.T) { return received.Load() }) - _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix()) + _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-60*24*time.Hour).Unix()) require.Nil(t, err) s.pruneAndNotifyWebPushSubscriptions() diff --git a/server/types.go b/server/types.go index fb08fb05..30f5c468 100644 --- a/server/types.go +++ b/server/types.go @@ -105,6 +105,8 @@ type publishMessage struct { Filename string `json:"filename"` Email string `json:"email"` Call string `json:"call"` + Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead) + Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead) Delay string `json:"delay"` } @@ -169,8 +171,12 @@ func (t sinceMarker) IsNone() bool { return t == sinceNoMessages } +func (t sinceMarker) IsLatest() bool { + return t == sinceLatestMessage +} + func (t sinceMarker) IsID() bool { - return t.id != "" + return t.id != "" && t.id != "latest" } func (t sinceMarker) Time() time.Time { @@ -182,8 +188,9 @@ func (t sinceMarker) ID() string { } var ( - sinceAllMessages = sinceMarker{time.Unix(0, 0), ""} - sinceNoMessages = sinceMarker{time.Unix(1, 0), ""} + sinceAllMessages = sinceMarker{time.Unix(0, 0), ""} + sinceNoMessages = sinceMarker{time.Unix(1, 0), ""} + sinceLatestMessage = sinceMarker{time.Unix(0, 0), "latest"} ) type queryFilter struct { @@ -248,9 +255,10 @@ type apiStatsResponse struct { MessagesRate float64 `json:"messages_rate"` // Average number of messages per second } -type apiUserAddRequest struct { +type apiUserAddOrUpdateRequest struct { Username string `json:"username"` Password string `json:"password"` + Hash string `json:"hash"` Tier string `json:"tier"` // Do not add 'role' here. We don't want to add admins via the API. } diff --git a/server/util.go b/server/util.go index 0f224de1..c4ee7a79 100644 --- a/server/util.go +++ b/server/util.go @@ -73,61 +73,33 @@ func readQueryParam(r *http.Request, names ...string) string { return "" } -func extractIPAddress(r *http.Request, behindProxy bool, proxyClientIPHeader string) netip.Addr { - logr(r).Debug("Starting IP extraction") - +func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string) netip.Addr { remoteAddr := r.RemoteAddr - logr(r).Debug("RemoteAddr: %s", remoteAddr) - addrPort, err := netip.ParseAddrPort(remoteAddr) ip := addrPort.Addr() if err != nil { - logr(r).Warn("Failed to parse RemoteAddr as AddrPort: %v", err) + // This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified ip, err = netip.ParseAddr(remoteAddr) if err != nil { ip = netip.IPv4Unspecified() - logr(r).Error("Failed to parse RemoteAddr as IP: %v, defaulting to 0.0.0.0", err) + if remoteAddr != "@" && !behindProxy { // RemoteAddr is @ when unix socket is used + logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", remoteAddr) + } } } - - // Log initial IP before further processing - logr(r).Debug("Initial IP after RemoteAddr parsing: %s", ip) - - if proxyClientIPHeader != "" { - logr(r).Debug("Using ProxyClientIPHeader: %s", proxyClientIPHeader) - if customHeaderIP := r.Header.Get(proxyClientIPHeader); customHeaderIP != "" { - logr(r).Debug("Custom header %s value: %s", proxyClientIPHeader, customHeaderIP) - realIP, err := netip.ParseAddr(customHeaderIP) - if err != nil { - logr(r).Error("Invalid IP in %s header: %s, error: %v", proxyClientIPHeader, customHeaderIP, err) - } else { - logr(r).Debug("Successfully parsed IP from custom header: %s", realIP) - ip = realIP - } + if behindProxy && strings.TrimSpace(r.Header.Get(proxyForwardedHeader)) != "" { + // X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, + // only the right-most address can be trusted (as this is the one added by our proxy server). + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. + ips := util.SplitNoEmpty(r.Header.Get(proxyForwardedHeader), ",") + realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr))) + if err != nil { + logr(r).Err(err).Error("invalid IP address %s received in %s header", ip, proxyForwardedHeader) + // Fall back to the regular remote address if X-Forwarded-For is damaged } else { - logr(r).Warn("Custom header %s is empty or missing", proxyClientIPHeader) + ip = realIP } - } else if behindProxy { - logr(r).Debug("No ProxyClientIPHeader set, checking X-Forwarded-For") - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - logr(r).Debug("X-Forwarded-For value: %s", xff) - ips := util.SplitNoEmpty(xff, ",") - realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr))) - if err != nil { - logr(r).Error("Invalid IP in X-Forwarded-For header: %s, error: %v", xff, err) - } else { - logr(r).Debug("Successfully parsed IP from X-Forwarded-For: %s", realIP) - ip = realIP - } - } else { - logr(r).Debug("X-Forwarded-For header is empty or missing") - } - } else { - logr(r).Debug("Behind proxy is false, skipping proxy headers") } - - // Final resolved IP - logr(r).Debug("Final resolved IP: %s", ip) return ip } diff --git a/server/webpush_store.go b/server/webpush_store.go index 62a35f7d..db0304be 100644 --- a/server/webpush_store.go +++ b/server/webpush_store.go @@ -79,8 +79,9 @@ const ( deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscription WHERE user_id = ?` deleteWebPushSubscriptionByAgeQuery = `DELETE FROM subscription WHERE updated_at <= ?` // Full table scan! - insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)` - deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?` + insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)` + deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?` + deleteWebPushSubscriptionTopicWithoutSubscription = `DELETE FROM subscription_topic WHERE subscription_id NOT IN (SELECT id FROM subscription)` ) // Schema management queries @@ -271,6 +272,10 @@ func (c *webPushStore) RemoveSubscriptionsByUserID(userID string) error { // RemoveExpiredSubscriptions removes all subscriptions that have not been updated for a given time period func (c *webPushStore) RemoveExpiredSubscriptions(expireAfter time.Duration) error { _, err := c.db.Exec(deleteWebPushSubscriptionByAgeQuery, time.Now().Add(-expireAfter).Unix()) + if err != nil { + return err + } + _, err = c.db.Exec(deleteWebPushSubscriptionTopicWithoutSubscription) return err } diff --git a/user/manager.go b/user/manager.go index 9f54625f..814ee827 100644 --- a/user/manager.go +++ b/user/manager.go @@ -28,7 +28,7 @@ const ( userHardDeleteAfterDuration = 7 * 24 * time.Hour tokenPrefix = "tk_" tokenLength = 32 - tokenMaxCount = 20 // Only keep this many tokens in the table per user + tokenMaxCount = 60 // Only keep this many tokens in the table per user tag = "user_manager" ) @@ -864,13 +864,19 @@ func (a *Manager) resolvePerms(base, perm Permission) error { } // AddUser adds a user with the given username, password and role -func (a *Manager) AddUser(username, password string, role Role) error { +func (a *Manager) AddUser(username, password string, role Role, hashed bool) error { if !AllowedUsername(username) || !AllowedRole(role) { return ErrInvalidArgument } - hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) - if err != nil { - return err + var hash []byte + var err error = nil + if hashed { + hash = []byte(password) + } else { + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + if err != nil { + return err + } } userID := util.RandomStringPrefix(userIDPrefix, userIDLength) syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() @@ -1192,10 +1198,17 @@ func (a *Manager) ReservationOwner(topic string) (string, error) { } // ChangePassword changes a user's password -func (a *Manager) ChangePassword(username, password string) error { - hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) - if err != nil { - return err +func (a *Manager) ChangePassword(username, password string, hashed bool) error { + var hash []byte + var err error + + if hashed { + hash = []byte(password) + } else { + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + if err != nil { + return err + } } if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil { return err diff --git a/user/manager_test.go b/user/manager_test.go index e9a95b0f..89f35e3c 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -14,13 +14,13 @@ import ( "time" ) -const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources +const minBcryptTimingMillis = int64(40) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources func TestManager_FullScenario_Default_DenyAll(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddUser("john", "john", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) + require.Nil(t, a.AddUser("john", "john", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite)) @@ -134,7 +134,7 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) { // and longer ACL rules are prioritized as well. a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "test*", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "*", PermissionRead)) @@ -147,20 +147,20 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) { func TestManager_AddUser_Invalid(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin)) - require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role")) + require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin, false)) + require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role", false)) } func TestManager_AddUser_Timing(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) start := time.Now().UnixMilli() - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) } func TestManager_AddUser_And_Query(t *testing.T) { a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) require.Nil(t, a.ChangeBilling("user", &Billing{ StripeCustomerID: "acct_123", StripeSubscriptionID: "sub_123", @@ -187,7 +187,7 @@ func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) { a := newTestManager(t, PermissionDenyAll) // Create user, add reservations and token - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) require.Nil(t, a.AddReservation("user", "mytopic", PermissionRead)) u, err := a.User("user") @@ -237,7 +237,7 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) { a := newTestManager(t, PermissionDenyAll) // Create user, add reservations and token - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false)) u, err := a.User("user") require.Nil(t, err) @@ -248,8 +248,8 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) { func TestManager_UserManagement(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite)) @@ -339,21 +339,31 @@ func TestManager_UserManagement(t *testing.T) { func TestManager_ChangePassword(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) + require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) + require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true)) _, err := a.Authenticate("phil", "phil") require.Nil(t, err) - require.Nil(t, a.ChangePassword("phil", "newpass")) + _, err = a.Authenticate("jane", "jane") + require.Nil(t, err) + + require.Nil(t, a.ChangePassword("phil", "newpass", false)) _, err = a.Authenticate("phil", "phil") require.Equal(t, ErrUnauthenticated, err) _, err = a.Authenticate("phil", "newpass") require.Nil(t, err) + + require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true)) + _, err = a.Authenticate("jane", "jane") + require.Equal(t, ErrUnauthenticated, err) + _, err = a.Authenticate("jane", "newpass") + require.Nil(t, err) } func TestManager_ChangeRole(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite)) require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead)) @@ -378,8 +388,8 @@ func TestManager_ChangeRole(t *testing.T) { func TestManager_Reservations(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll)) require.Nil(t, a.AddReservation("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead)) @@ -460,7 +470,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) { AttachmentTotalSizeLimit: 524288000, AttachmentExpiryDuration: 24 * time.Hour, })) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) require.Nil(t, a.ChangeTier("ben", "pro")) require.Nil(t, a.AddReservation("ben", "mytopic", PermissionDenyAll)) @@ -507,7 +517,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) { func TestManager_Token_Valid(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) u, err := a.User("ben") require.Nil(t, err) @@ -551,7 +561,7 @@ func TestManager_Token_Valid(t *testing.T) { func TestManager_Token_Invalid(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length require.Nil(t, u) @@ -570,7 +580,7 @@ func TestManager_Token_NotFound(t *testing.T) { func TestManager_Token_Expire(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) u, err := a.User("ben") require.Nil(t, err) @@ -618,7 +628,7 @@ func TestManager_Token_Expire(t *testing.T) { func TestManager_Token_Extend(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // Try to extend token for user without token u, err := a.User("ben") @@ -647,8 +657,8 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { // Tests that tokens are automatically deleted when the maximum number of tokens is reached a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) ben, err := a.User("ben") require.Nil(t, err) @@ -668,10 +678,10 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { require.NotEmpty(t, token.Value) philTokens = append(philTokens, token.Value) - // Create 22 tokens for ben (only 20 allowed!) + // Create 62 tokens for ben (only 60 allowed!) baseTime := time.Now().Add(24 * time.Hour) benTokens := make([]string, 0) - for i := 0; i < 22; i++ { // + for i := 0; i < 62; i++ { // token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) require.Nil(t, err) require.NotEmpty(t, token.Value) @@ -690,7 +700,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { require.Equal(t, ErrUnauthenticated, err) // Ben: The other tokens should still work - for i := 2; i < 22; i++ { + for i := 2; i < 62; i++ { userWithToken, err := a.AuthenticateToken(benTokens[i]) require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i]) require.Equal(t, "ben", userWithToken.Name) @@ -710,7 +720,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { require.Nil(t, err) require.True(t, rows.Next()) require.Nil(t, rows.Scan(&benCount)) - require.Equal(t, 20, benCount) + require.Equal(t, 60, benCount) var philCount int rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID) @@ -723,7 +733,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { func TestManager_EnqueueStats_ResetStats(t *testing.T) { a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond) require.Nil(t, err) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // Baseline: No messages or emails u, err := a.User("ben") @@ -765,7 +775,7 @@ func TestManager_EnqueueStats_ResetStats(t *testing.T) { func TestManager_EnqueueTokenUpdate(t *testing.T) { a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 500*time.Millisecond) require.Nil(t, err) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // Create user and token u, err := a.User("ben") @@ -798,7 +808,7 @@ func TestManager_EnqueueTokenUpdate(t *testing.T) { func TestManager_ChangeSettings(t *testing.T) { a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond) require.Nil(t, err) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) // No settings u, err := a.User("ben") @@ -866,7 +876,7 @@ func TestManager_Tier_Create_Update_List_Delete(t *testing.T) { AttachmentBandwidthLimit: 21474836480, StripeMonthlyPriceID: "price_2", })) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) require.Nil(t, a.ChangeTier("phil", "pro")) ti, err := a.Tier("pro") @@ -981,7 +991,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) { Name: "Pro", ReservationLimit: 4, })) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) require.Nil(t, a.ChangeTier("phil", "pro")) // Add 10 reservations (pro tier allows that) @@ -1007,7 +1017,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) { func TestUser_PhoneNumberAddListRemove(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) phil, err := a.User("phil") require.Nil(t, err) require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890")) @@ -1032,8 +1042,8 @@ func TestUser_PhoneNumberAddListRemove(t *testing.T) { func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) { a := newTestManager(t, PermissionDenyAll) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) - require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser, false)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) phil, err := a.User("phil") require.Nil(t, err) ben, err := a.User("ben") diff --git a/web/package-lock.json b/web/package-lock.json index e2f51f61..3f428c2e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -41,8 +41,8 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.15.0" + "vite": "^6.3.5", + "vite-plugin-pwa": "^1.0.0" } }, "node_modules/@ampproject/remapping": { @@ -50,6 +50,7 @@ "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" @@ -59,42 +60,46 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -113,56 +118,48 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -171,17 +168,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", - "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -192,13 +190,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", - "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -209,10 +208,11 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -225,40 +225,42 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -268,35 +270,38 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", - "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-wrap-function": "^7.25.0", - "@babel/traverse": "^7.25.0" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -306,14 +311,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", - "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -322,104 +328,84 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", - "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -429,13 +415,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", - "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.3" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -445,12 +432,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", - "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -460,12 +448,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", - "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -475,14 +464,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -492,13 +482,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", - "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.0" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -512,6 +503,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" }, @@ -519,76 +511,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", - "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -598,138 +528,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", - "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -743,6 +548,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -755,12 +561,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -770,15 +577,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", - "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-remap-async-to-generator": "^7.25.0", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/traverse": "^7.25.4" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -788,14 +595,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", - "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -805,12 +613,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -820,12 +629,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", - "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -835,13 +645,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", - "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -851,14 +662,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -868,16 +679,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", - "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.4", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { @@ -888,13 +700,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -904,12 +717,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -919,13 +733,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -935,12 +750,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -950,13 +766,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", - "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -966,13 +783,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -982,13 +799,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -998,13 +815,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1014,13 +831,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1030,14 +848,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", - "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.1" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1047,13 +866,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1063,12 +882,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", - "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1078,13 +898,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1094,12 +914,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1109,13 +930,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1125,14 +947,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1142,15 +964,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", - "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1160,13 +983,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1176,13 +1000,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1192,12 +1017,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1207,13 +1033,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1223,13 +1049,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1239,15 +1065,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", + "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1257,13 +1084,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1273,13 +1101,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1289,14 +1117,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1306,12 +1134,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1321,13 +1150,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", - "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1337,15 +1167,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1355,12 +1185,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1370,12 +1201,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1385,12 +1217,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1400,13 +1233,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1415,13 +1248,31 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1431,12 +1282,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1446,13 +1298,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1462,12 +1315,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1477,12 +1331,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1492,12 +1347,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1507,12 +1363,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1522,13 +1379,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1538,13 +1396,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1554,13 +1413,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", - "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1570,93 +1430,80 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", - "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.4", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.25.1", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.25.2", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-modules-systemjs": "^7.25.0", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.25.4", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.8", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.37.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -1671,6 +1518,7 @@ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -1680,46 +1528,40 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1728,28 +1570,29 @@ } }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@emotion/babel-plugin": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", - "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.2.0", + "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", @@ -1761,16 +1604,18 @@ "node_modules/@emotion/babel-plugin/node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" }, "node_modules/@emotion/cache": { - "version": "11.13.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } @@ -1778,17 +1623,20 @@ "node_modules/@emotion/cache/node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1796,19 +1644,21 @@ "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" }, "node_modules/@emotion/react": { - "version": "11.13.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", - "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, @@ -1822,33 +1672,36 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", - "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.1", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", - "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", + "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.0", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0" + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", @@ -1863,398 +1716,480 @@ "node_modules/@emotion/unitless": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2264,6 +2199,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2287,6 +2223,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2302,6 +2239,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2314,6 +2252,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2324,6 +2263,7 @@ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -2338,6 +2278,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -2351,12 +2292,14 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "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==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2370,6 +2313,7 @@ "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" } @@ -2378,6 +2322,7 @@ "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" } @@ -2387,6 +2332,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2395,12 +2341,14 @@ "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==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "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" @@ -2410,6 +2358,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz", "integrity": "sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==", + "license": "BSD-2-Clause", "dependencies": { "unist-util-visit": "^1.4.1" }, @@ -2418,18 +2367,20 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", - "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz", + "integrity": "sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", - "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.17.1.tgz", + "integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -2442,8 +2393,8 @@ }, "peerDependencies": { "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2452,21 +2403,22 @@ } }, "node_modules/@mui/material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", - "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", + "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.7", - "@mui/system": "^5.16.7", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", + "@mui/core-downloads-tracker": "^5.17.1", + "@mui/system": "^5.17.1", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.3.1", + "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "engines": { @@ -2479,9 +2431,9 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -2496,12 +2448,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", - "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.6", + "@mui/utils": "^5.17.1", "prop-types": "^15.8.1" }, "engines": { @@ -2512,8 +2465,8 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2522,12 +2475,13 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", - "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", + "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", + "@emotion/cache": "^11.13.5", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, @@ -2541,7 +2495,7 @@ "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -2553,15 +2507,16 @@ } }, "node_modules/@mui/system": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", - "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", + "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.6", - "@mui/styled-engine": "^5.16.6", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.16.14", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -2576,8 +2531,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -2592,9 +2547,10 @@ } }, "node_modules/@mui/types": { - "version": "7.2.17", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", - "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -2605,16 +2561,17 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", - "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/types": "^7.2.15", + "@mui/types": "~7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "react-is": "^19.0.0" }, "engines": { "node": ">=12.0.0" @@ -2624,8 +2581,8 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2638,6 +2595,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2651,6 +2609,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -2660,6 +2619,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2672,30 +2632,392 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, "node_modules/@remix-run/router": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", - "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "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/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "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/rollup-android-arm-eabi": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", @@ -2708,6 +3030,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -2717,10 +3040,11 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -2730,126 +3054,130 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mdast": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", "dependencies": { "@types/unist": "^2" } }, - "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", - "dev": true, - "dependencies": { - "undici-types": "~6.19.2" - } - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", - "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", + "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "license": "MIT", + "peer": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", - "dependencies": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { "@types/react": "*" } }, "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true, - "dependencies": { - "@types/node": "*" - } + "license": "MIT" }, "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==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", + "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2862,6 +3190,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2871,6 +3200,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2887,44 +3217,53 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -2938,6 +3277,7 @@ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2958,6 +3298,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2974,17 +3315,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2994,15 +3337,16 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3012,15 +3356,16 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3034,6 +3379,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -3046,19 +3392,19 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -3071,19 +3417,32 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 4.0.0" } @@ -3093,6 +3452,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -3104,10 +3464,11 @@ } }, "node_modules/axe-core": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", - "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", "dev": true, + "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -3117,6 +3478,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -3125,6 +3487,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -3136,13 +3499,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { @@ -3150,25 +3514,27 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3178,6 +3544,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3187,34 +3554,24 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", - "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -3230,11 +3587,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001663", - "electron-to-chromium": "^1.5.28", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -3247,31 +3605,51 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3284,14 +3662,15 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001664", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", - "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -3306,33 +3685,31 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3342,6 +3719,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3351,6 +3729,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3360,27 +3739,36 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, "node_modules/comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3390,13 +3778,15 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -3405,26 +3795,30 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.38.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", - "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", "dev": true, + "license": "MIT", "dependencies": { - "browserslist": "^4.23.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -3435,6 +3829,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -3446,19 +3841,30 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "license": "MIT", "dependencies": { "node-fetch": "2.6.7" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3473,6 +3879,7 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3481,6 +3888,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.3.0.tgz", "integrity": "sha512-ZZXXn51SnxRxAZ6fdY7mBDPmA4OZd83q/J9Gdqz3YmE9TUq+9tZl+tdOnCi7PpNygI6PEkehj9rgifv5+W8a5A==", + "license": "Apache-2.0", "engines": { "node": ">=10.0.0" } @@ -3488,23 +3896,26 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3514,29 +3925,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -3548,9 +3961,10 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3563,49 +3977,19 @@ } } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "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==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3615,6 +3999,7 @@ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3632,6 +4017,7 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3648,6 +4034,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.7.tgz", "integrity": "sha512-2a+BXvVhY5op+smDRLxeBAivE7YcYaneXJ1la3HOkUfX9zKkE/AJ8CNgjiXbtXepFyFmJNGSbmjOwqbT749r/w==", + "license": "Apache-2.0", "engines": { "node": ">=6.0" } @@ -3656,6 +4043,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz", "integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==", + "license": "Apache-2.0", "peerDependencies": { "@types/react": ">=16", "dexie": "^3.2 || ^4.0.1-alpha", @@ -3667,6 +4055,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -3678,16 +4067,33 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -3699,21 +4105,24 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.29", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", - "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", - "dev": true + "version": "1.5.157", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz", + "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -3722,62 +4131,69 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", "dependencies": { "stackframe": "^1.3.4" } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.10", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", + "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -3787,13 +4203,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3803,60 +4217,45 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -3865,37 +4264,44 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -3905,40 +4311,44 @@ } }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { @@ -3946,6 +4356,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3954,6 +4365,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -3965,7 +4377,9 @@ "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4021,6 +4435,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", "dev": true, + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", @@ -4042,6 +4457,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, + "license": "MIT", "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", @@ -4061,6 +4477,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4073,6 +4490,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -4084,6 +4502,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -4093,6 +4512,7 @@ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -4110,15 +4530,17 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", - "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4128,7 +4550,7 @@ "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.9.0", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", @@ -4137,13 +4559,14 @@ "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -4151,6 +4574,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -4160,6 +4584,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -4168,12 +4593,13 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", - "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "license": "MIT", "dependencies": { - "aria-query": "~5.1.3", + "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", @@ -4181,14 +4607,13 @@ "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.19", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.0" + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" @@ -4198,28 +4623,29 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.0.tgz", - "integrity": "sha512-IHBePmfWH5lKhJnJ7WB1V+v/GolbB0rjS8XYVCSQCZKaQCAUhMoVoOEn1Ef8Z8Wf0a7l8KTJvuZg5/e4qrZ6nA==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", + "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", + "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "engines": { @@ -4234,6 +4660,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4246,6 +4673,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -4258,6 +4686,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4275,6 +4704,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4291,6 +4721,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4298,60 +4729,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/eslint/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -4362,32 +4745,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -4400,6 +4763,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -4417,6 +4781,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -4429,6 +4794,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4441,21 +4807,24 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true + "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/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -4463,74 +4832,78 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", - "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", - "dev": true + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -4543,6 +4916,7 @@ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } @@ -4552,6 +4926,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4561,6 +4936,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4568,28 +4944,18 @@ "node": ">=10" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4606,6 +4972,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -4616,18 +4983,26 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/fs-extra": { @@ -4635,6 +5010,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -4649,7 +5025,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -4657,6 +5034,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -4669,20 +5047,24 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -4696,6 +5078,7 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4705,21 +5088,28 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4732,17 +5122,33 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4757,6 +5163,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4777,6 +5184,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -4788,6 +5196,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -4797,6 +5206,7 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -4809,12 +5219,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4824,29 +5235,37 @@ "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 + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { @@ -4854,6 +5273,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -4862,10 +5282,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -4874,10 +5298,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4890,6 +5315,7 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -4904,6 +5330,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -4915,6 +5342,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", "integrity": "sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.3", "comma-separated-tokens": "^1.0.0", @@ -4933,6 +5361,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -4940,20 +5369,23 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", "dependencies": { "void-elements": "3.1.0" } }, "node_modules/humanize-duration": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.1.tgz", - "integrity": "sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==" + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.2.tgz", + "integrity": "sha512-jcTwWYeCJf4dN5GJnjBmHd42bNyK94lY49QTkrsAQrMTUoIYLevvDpmQtg5uv8ZrdIRIbzdasmSNZ278HHUPEg==", + "license": "Unlicense" }, "node_modules/i18next": { "version": "21.10.0", @@ -4973,6 +5405,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.2" } @@ -4981,6 +5414,7 @@ "version": "6.1.8", "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.8.tgz", "integrity": "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.19.0" } @@ -4989,6 +5423,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.5.tgz", "integrity": "sha512-tLuHWuLWl6CmS07o+UB6EcQCaUjrZ1yhdseIN7sfq0u7phsMePJ8pqlGhIAdRDPF/q7ooyo5MID5DRFBCH+x5w==", + "license": "MIT", "dependencies": { "cross-fetch": "3.1.5" } @@ -4997,21 +5432,24 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5028,6 +5466,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -5038,6 +5477,7 @@ "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, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5047,22 +5487,25 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "license": "MIT" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5072,6 +5515,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5081,6 +5525,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -5090,30 +5535,16 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5125,15 +5556,21 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5143,25 +5580,30 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5188,6 +5630,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "engines": { "node": ">=4" } @@ -5197,6 +5640,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5205,9 +5649,10 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -5219,11 +5664,14 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -5234,12 +5682,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5252,6 +5702,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5262,29 +5713,38 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5298,6 +5758,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -5309,6 +5770,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5319,6 +5781,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5330,36 +5793,18 @@ "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-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } + "license": "MIT" }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5373,6 +5818,7 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5382,6 +5828,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5390,18 +5837,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5415,6 +5866,7 @@ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5424,6 +5876,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5432,12 +5885,13 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -5451,6 +5905,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5459,12 +5914,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5474,12 +5931,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5489,12 +5949,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -5508,6 +5969,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5516,25 +5978,30 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5547,25 +6014,32 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jake": { @@ -5573,6 +6047,7 @@ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -5586,126 +6061,24 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "license": "BSD-3-Clause" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -5714,50 +6087,57 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -5770,6 +6150,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -5782,6 +6163,7 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5791,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -5806,6 +6189,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -5814,13 +6198,15 @@ "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, + "license": "MIT", "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -5833,6 +6219,7 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5842,6 +6229,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5853,13 +6241,15 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -5874,30 +6264,35 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -5910,6 +6305,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -5919,14 +6315,26 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", "dev": true, + "license": "MIT", "dependencies": { "sourcemap-codec": "^1.4.8" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", + "license": "MIT", "dependencies": { "unist-util-visit": "^2.0.0" }, @@ -5939,6 +6347,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0", @@ -5953,6 +6362,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0" @@ -5966,6 +6376,7 @@ "version": "0.8.5", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-string": "^2.0.0", @@ -5982,6 +6393,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz", "integrity": "sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==", + "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -6001,6 +6413,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0", @@ -6015,6 +6428,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0" @@ -6028,6 +6442,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -6036,22 +6451,8 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" }, "node_modules/micromark": { "version": "2.11.4", @@ -6067,29 +6468,18 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "debug": "^4.0.0", "parse-entities": "^2.0.0" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6102,6 +6492,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6109,12 +6500,13 @@ "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==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -6122,6 +6514,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6133,12 +6526,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -6155,40 +6550,27 @@ } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6201,19 +6583,23 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -6224,14 +6610,16 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -6242,6 +6630,7 @@ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6260,6 +6649,7 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6270,12 +6660,14 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -6291,6 +6683,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -6300,6 +6693,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -6312,11 +6706,30 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6332,6 +6745,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -6346,6 +6760,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -6357,6 +6772,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -6374,6 +6790,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -6392,6 +6809,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6401,6 +6819,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6410,6 +6829,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6417,46 +6837,51 @@ "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==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -6472,9 +6897,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -6486,6 +6912,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -6495,6 +6922,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -6510,6 +6938,7 @@ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true, + "license": "MIT", "engines": { "node": "^14.13.1 || >=16.0.0" }, @@ -6521,6 +6950,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -6530,12 +6960,14 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", "dependencies": { "xtend": "^4.0.0" }, @@ -6549,6 +6981,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6571,44 +7004,45 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.0" } }, "node_modules/react-i18next": { "version": "11.18.6", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.5", "html-parse-stringify": "^3.0.1" @@ -6630,6 +7064,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "license": "MIT", "dependencies": { "throttle-debounce": "^2.1.0" }, @@ -6638,15 +7073,17 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6655,6 +7092,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-remark/-/react-remark-2.1.0.tgz", "integrity": "sha512-7dEPxRGQ23sOdvteuRGaQAs9cEOH/BOeCN4CqsJdk3laUDIDYRCWnM6a3z92PzXHUuxIRLXQNZx7SiO0ijUcbw==", + "license": "MIT", "dependencies": { "rehype-react": "^6.0.0", "remark-parse": "^9.0.0", @@ -6673,11 +7111,12 @@ } }, "node_modules/react-router": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", - "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -6687,12 +7126,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", - "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2", - "react-router": "6.26.2" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -6706,6 +7146,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -6718,18 +7159,20 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -6742,13 +7185,15 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -6756,30 +7201,19 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -6789,15 +7223,16 @@ } }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -6805,31 +7240,44 @@ "node": ">=4" } }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/rehype-react": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz", "integrity": "sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==", + "license": "MIT", "dependencies": { "@mapbox/hast-util-table-cell-style": "^0.2.0", "hast-to-hyperscript": "^9.0.0" @@ -6843,6 +7291,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^0.8.0" }, @@ -6855,6 +7304,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz", "integrity": "sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==", + "license": "MIT", "dependencies": { "mdast-util-to-hast": "^10.2.0" }, @@ -6868,22 +7318,27 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6892,15 +7347,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6912,6 +7369,7 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -6923,18 +7381,42 @@ } }, "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" } }, @@ -6957,19 +7439,22 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -6997,17 +7482,36 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -7017,27 +7521,27 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -7047,6 +7551,7 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7064,6 +7569,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7074,11 +7580,27 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7091,20 +7613,23 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7113,10 +7638,74 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7126,6 +7715,7 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7135,6 +7725,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -7145,6 +7736,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7154,12 +7746,14 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/space-separated-tokens": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7169,6 +7763,7 @@ "version": "2.0.10", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "license": "MIT", "dependencies": { "stackframe": "^1.3.4" } @@ -7176,12 +7771,14 @@ "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" }, "node_modules/stacktrace-gps": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "license": "MIT", "dependencies": { "source-map": "0.5.6", "stackframe": "^1.3.4" @@ -7191,6 +7788,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7199,52 +7797,48 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "license": "MIT", "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, + "license": "MIT", "dependencies": { - "internal-slot": "^1.0.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" }, "engines": { "node": ">= 0.4" } }, - "node_modules/string.prototype.includes": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz", - "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7258,21 +7852,26 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7282,15 +7881,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7300,6 +7904,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -7317,6 +7922,7 @@ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", @@ -7331,6 +7937,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7343,6 +7950,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7352,6 +7960,7 @@ "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -7361,6 +7970,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -7372,19 +7982,22 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "license": "MIT", "dependencies": { "inline-style-parser": "0.1.1" } }, "node_modules/stylis": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", - "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" }, "node_modules/stylis-plugin-rtl": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/stylis-plugin-rtl/-/stylis-plugin-rtl-2.1.1.tgz", "integrity": "sha512-q6xIkri6fBufIO/sV55md2CbgS5c6gg9EhSVATtHHCdOnbN/jcI0u3lYhNVeuI65c4lQPo67g8xmq5jrREvzlg==", + "license": "MIT", "dependencies": { "cssjanus": "^2.0.1" }, @@ -7393,20 +8006,23 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "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==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7419,6 +8035,7 @@ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7428,6 +8045,7 @@ "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", "dev": true, + "license": "MIT", "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", @@ -7442,13 +8060,14 @@ } }, "node_modules/terser": { - "version": "5.34.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", - "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -7463,45 +8082,46 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/throttle-debounce": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, + "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.4.4", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/trough": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7512,6 +8132,7 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -7524,6 +8145,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -7536,6 +8158,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7548,6 +8171,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -7556,30 +8180,32 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -7589,17 +8215,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -7609,17 +8237,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -7629,31 +8258,30 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7663,6 +8291,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, + "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -7676,6 +8305,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7685,6 +8315,7 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7693,6 +8324,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "license": "MIT", "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -7711,6 +8343,7 @@ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^2.0.0" }, @@ -7722,6 +8355,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7731,6 +8365,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7740,6 +8375,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7749,6 +8385,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7758,6 +8395,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.2" }, @@ -7770,6 +8408,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "license": "MIT", "dependencies": { "unist-util-visit-parents": "^2.0.0" } @@ -7778,6 +8417,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "license": "MIT", "dependencies": { "unist-util-is": "^3.0.0" } @@ -7785,13 +8425,15 @@ "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -7801,15 +8443,16 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4", "yarn": "*" } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -7825,9 +8468,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7841,6 +8485,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -7849,6 +8494,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", @@ -7864,6 +8510,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" @@ -7874,40 +8521,51 @@ } }, "node_modules/vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -7917,6 +8575,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -7925,34 +8586,51 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-plugin-pwa": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.15.2.tgz", - "integrity": "sha512-l1srtaad5NMNrAtAuub6ArTYG5Ci9AwofXXQ6IsbpCMYQ/0HUndwI7RB2x95+1UBFm7VGttQtT7woBlVnNhBRw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.0.0.tgz", + "integrity": "sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "^4.3.4", - "fast-glob": "^3.2.12", - "pretty-bytes": "^6.0.0", - "workbox-build": "^6.5.4", - "workbox-window": "^6.5.4" + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, + "engines": { + "node": ">=16.0.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0", - "workbox-build": "^6.5.4", - "workbox-window": "^6.5.4" + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } } }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7961,6 +8639,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7969,12 +8648,14 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -7985,6 +8666,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -7996,39 +8678,45 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", - "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", + "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8042,6 +8730,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -8056,15 +8745,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -8079,42 +8771,47 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/workbox-background-sync": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", - "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", + "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", "dev": true, + "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-broadcast-update": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", - "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz", + "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-build": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", - "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz", + "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", "dev": true, + "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", + "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", @@ -8124,30 +8821,29 @@ "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "6.6.0", - "workbox-broadcast-update": "6.6.0", - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-google-analytics": "6.6.0", - "workbox-navigation-preload": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-range-requests": "6.6.0", - "workbox-recipes": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0", - "workbox-streams": "6.6.0", - "workbox-sw": "6.6.0", - "workbox-window": "6.6.0" + "workbox-background-sync": "7.3.0", + "workbox-broadcast-update": "7.3.0", + "workbox-cacheable-response": "7.3.0", + "workbox-core": "7.3.0", + "workbox-expiration": "7.3.0", + "workbox-google-analytics": "7.3.0", + "workbox-navigation-preload": "7.3.0", + "workbox-precaching": "7.3.0", + "workbox-range-requests": "7.3.0", + "workbox-recipes": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0", + "workbox-streams": "7.3.0", + "workbox-sw": "7.3.0", + "workbox-window": "7.3.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=16.0.0" } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { @@ -8155,6 +8851,7 @@ "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", "dev": true, + "license": "MIT", "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", @@ -8172,6 +8869,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" @@ -8190,31 +8888,12 @@ } } }, - "node_modules/workbox-build/node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" @@ -8228,6 +8907,7 @@ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", @@ -8240,11 +8920,19 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/workbox-build/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, "node_modules/workbox-build/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8256,17 +8944,39 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -8279,6 +8989,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -8289,27 +9000,12 @@ "fsevents": "~2.3.2" } }, - "node_modules/workbox-build/node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, "node_modules/workbox-build/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, @@ -8322,6 +9018,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^2.1.0" } @@ -8330,13 +9027,15 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/workbox-build/node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, + "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", @@ -8344,141 +9043,154 @@ } }, "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "workbox-background-sync@6.6.0", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", + "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", - "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", - "dev": true + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz", + "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==", + "dev": true, + "license": "MIT" }, "node_modules/workbox-expiration": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", - "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz", + "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", "dev": true, + "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", - "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz", + "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-background-sync": "7.3.0", + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" } }, "node_modules/workbox-navigation-preload": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", - "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz", + "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-precaching": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", - "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz", + "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" } }, "node_modules/workbox-range-requests": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", - "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz", + "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-recipes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", - "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz", + "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" + "workbox-cacheable-response": "7.3.0", + "workbox-core": "7.3.0", + "workbox-expiration": "7.3.0", + "workbox-precaching": "7.3.0", + "workbox-routing": "7.3.0", + "workbox-strategies": "7.3.0" } }, "node_modules/workbox-routing": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", - "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz", + "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-strategies": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", - "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz", + "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "node_modules/workbox-streams": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", - "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz", + "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", "dev": true, + "license": "MIT", "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0" + "workbox-core": "7.3.0", + "workbox-routing": "7.3.0" } }, "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", - "dev": true + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz", + "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==", + "dev": true, + "license": "MIT" }, "node_modules/workbox-window": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", - "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz", + "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", "dev": true, + "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "6.6.0" + "workbox-core": "7.3.0" } }, "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 + "dev": true, + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } @@ -8487,14 +9199,22 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } }, "node_modules/yocto-queue": { @@ -8502,6 +9222,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/web/package.json b/web/package.json index bb84ff16..0de56abd 100644 --- a/web/package.json +++ b/web/package.json @@ -44,8 +44,8 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.15.0" + "vite": "^6.3.5", + "vite-plugin-pwa": "^1.0.0" }, "browserslist": { "production": [ diff --git a/web/public/static/langs/ar.json b/web/public/static/langs/ar.json index b0ddadbe..158fa926 100644 --- a/web/public/static/langs/ar.json +++ b/web/public/static/langs/ar.json @@ -359,5 +359,6 @@ "account_basics_phone_numbers_dialog_verify_button_call": "اتصل بي", "account_basics_phone_numbers_dialog_code_label": "رمز التحقّق", "account_upgrade_dialog_tier_price_per_month": "شهر", - "prefs_appearance_theme_title": "الحُلّة" + "prefs_appearance_theme_title": "الحُلّة", + "subscribe_dialog_subscribe_use_another_background_info": "لن يتم استلام الاشعارات من الخوادم الخارجية عندما يكون تطبيق الويب مغلقاً" } diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json index 15c8cc95..59b85e5b 100644 --- a/web/public/static/langs/bg.json +++ b/web/public/static/langs/bg.json @@ -212,7 +212,7 @@ "nav_upgrade_banner_label": "Надграждане до ntfy Pro", "signup_form_confirm_password": "Парола отново", "signup_disabled": "Регистрациите са затворени", - "signup_error_creation_limit_reached": "Достигнатео е ограничението за създаване на профили", + "signup_error_creation_limit_reached": "Достигнато е ограничението за създаване на профили", "display_name_dialog_title": "Промяна на показваното име", "action_bar_reservation_edit": "Промяна на резервацията", "action_bar_sign_up": "Регистриране", diff --git a/web/public/static/langs/bn.json b/web/public/static/langs/bn.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/bn.json @@ -0,0 +1 @@ +{} diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index a548d0b4..92dec374 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -36,7 +36,7 @@ "message_bar_type_message": "Gib hier eine Nachricht ein", "message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung", "alert_not_supported_title": "Benachrichtigungen werden nicht unterstützt", - "alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt.", + "alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt", "action_bar_settings": "Einstellungen", "action_bar_clear_notifications": "Alle Benachrichtigungen löschen", "alert_notification_permission_required_button": "Jetzt erlauben", @@ -383,5 +383,25 @@ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag", "action_bar_mute_notifications": "Benachrichtigungen stummschalten", "action_bar_unmute_notifications": "Benachrichtigungen laut schalten", - "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert" + "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert", + "alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser", + "notifications_actions_failed_notification": "Aktion nicht erfolgreich", + "alert_notification_ios_install_required_title": "iOS Installation erforderlich", + "alert_notification_ios_install_required_description": "Klicke auf das Teilen-Symbol und “Zum Home-Bildschirm” um auf iOS Benachrichtigungen zu aktivieren", + "subscribe_dialog_subscribe_use_another_background_info": "Benachrichtigungen von anderen Servern werden nicht empfangen, wenn die Web App nicht geöffnet ist", + "publish_dialog_checkbox_markdown": "Als Markdown formatieren", + "prefs_notifications_web_push_title": "Hintergrund-Benachrichtigungen", + "prefs_notifications_web_push_disabled_description": "Benachrichtigungen werden empfangen wenn die Web App läuft (über WebSocket)", + "prefs_notifications_web_push_enabled": "Aktiviert für {{server}}", + "prefs_notifications_web_push_disabled": "Deaktiviert", + "prefs_appearance_theme_title": "Thema", + "prefs_appearance_theme_system": "System (Standard)", + "prefs_appearance_theme_dark": "Nachtmodus", + "prefs_appearance_theme_light": "Tagmodus", + "error_boundary_button_reload_ntfy": "ntfy neu laden", + "web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert", + "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", + "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom server empfangen", + "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest.", + "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht läuft (über Web Push)" } diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index 65bfa6bc..7d2a5a85 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -22,5 +22,82 @@ "common_add": "Lisa", "signup_form_button_submit": "Liitu", "signup_form_toggle_password_visibility": "Vaheta salasõna nähtavust", - "action_bar_account": "Kasutajakonto" + "action_bar_account": "Kasutajakonto", + "action_bar_sign_in": "Logi sisse", + "nav_button_documentation": "Dokumentatsioon", + "action_bar_profile_title": "Profiil", + "action_bar_profile_settings": "Seadistused", + "action_bar_sign_up": "Liitu", + "message_bar_type_message": "Sisesta oma sõnum siia", + "message_bar_error_publishing": "Viga teavituse avaldamisel", + "message_bar_show_dialog": "Näita avaldamisvaadet", + "message_bar_publish": "Avalda sõnum", + "nav_topics_title": "Tellitud teemad", + "nav_button_all_notifications": "Kõik teavitused", + "nav_button_account": "Kasutajakonto", + "nav_button_settings": "Seadistused", + "nav_button_publish_message": "Avalda teavitus", + "nav_button_subscribe": "Telli teema", + "nav_button_muted": "Teavitused on summutatud", + "nav_button_connecting": "loome ühendust", + "nav_upgrade_banner_label": "Uuenda ntfy Pro teenuseks", + "action_bar_profile_logout": "Logi välja", + "notifications_list_item": "Teavitus", + "account_tokens_table_expires_header": "Aegub", + "notifications_attachment_file_document": "muu dokument", + "notifications_list": "Teavituste loend", + "notifications_delete": "Kustuta", + "notifications_copied_to_clipboard": "Kopeeritud lõikelauale", + "alert_notification_permission_denied_description": "Palun luba nad veebibrauseris uuesti", + "account_tokens_table_last_access_header": "Viimase kasutamise aeg", + "account_tokens_table_token_header": "Tunnusluba", + "account_tokens_table_last_origin_tooltip": "IP-aadressilt {{ip}}, klõpsi täpsema teabe nägemiseks", + "action_bar_reservation_add": "Reserveeri teema", + "action_bar_reservation_edit": "Muuda reserveeringut", + "action_bar_reservation_delete": "Eemalda reserveering", + "action_bar_reservation_limit_reached": "Ülempiir on käes", + "action_bar_send_test_notification": "Saata testteavitus", + "action_bar_clear_notifications": "Kustuta kõik teavitused", + "action_bar_mute_notifications": "Summuta teavitused", + "nav_upgrade_banner_description": "Reserveeri teemasid, rohkem sõnumeid ja e-kirju ning suuremad manused", + "action_bar_unmute_notifications": "Lõpeta teavituste summutamine", + "action_bar_unsubscribe": "Lõpeta tellimus", + "action_bar_toggle_mute": "Lülita teavituste summutamine sisse/välja", + "action_bar_toggle_action_menu": "Ava/sulge tegevuste menüü", + "notifications_mark_read": "Märgi loetuks", + "notifications_tags": "Sildid", + "notifications_priority_x": "{{priority}}. prioriteet", + "notifications_new_indicator": "Uus teavitus", + "notifications_attachment_image": "Pilt manusena", + "notifications_attachment_copy_url_title": "Kopeeri manuse võrguaadress lõikelauale", + "notifications_attachment_copy_url_button": "Kopeeri võrguaadress", + "notifications_attachment_open_title": "Ava {{url}} aadress", + "notifications_attachment_open_button": "Ava manus", + "notifications_attachment_link_expires": "link aegub {{date}}", + "notifications_attachment_link_expired": "allalaadimise link on aegunud", + "notifications_attachment_file_image": "pildifail", + "notifications_attachment_file_video": "videofail", + "notifications_attachment_file_audio": "helifail", + "notifications_attachment_file_app": "Androidi rakenduse fail", + "notifications_click_copy_url_title": "Kopeeri lingi võrguaadress lõikelauale", + "notifications_click_copy_url_button": "Kopeeri link", + "notifications_click_open_button": "Ava link", + "notifications_actions_open_url_title": "Ava {{url}} aadress", + "notifications_actions_not_supported": "Toiming pole veebirakenduses toetatud", + "alert_notification_permission_required_title": "Teavitused pole kasutusel", + "alert_notification_permission_required_description": "Anna oma brauserile õigused näidata töölauateavitusi", + "alert_notification_permission_required_button": "Luba nüüd", + "alert_notification_permission_denied_title": "Teavitused on blokeeritud", + "alert_notification_ios_install_required_title": "Vajalik on iOS-i paigaldamine", + "alert_not_supported_title": "Teavitused pole toetatud", + "alert_not_supported_description": "Teavitused pole sinu veebibrauseris toetatud", + "account_tokens_table_label_header": "Silt", + "account_tokens_table_never_expires": "Ei aegu iialgi", + "account_tokens_table_current_session": "Praegune brauserisessioon", + "account_tokens_table_copied_to_clipboard": "Ligipääsu tunnusluba on kopeeritud", + "account_tokens_table_cannot_delete_or_edit": "Praeguse sessiooni tunnusluba ei saa muuta ega kustutada", + "account_tokens_table_create_token_button": "Loo ligipääsuks vajalik tunnusluba", + "account_tokens_dialog_title_create": "Loo ligipääsuks vajalik tunnusluba", + "account_tokens_dialog_title_edit": "Muuda ligipääsuks vajalikku tunnusluba", + "account_tokens_dialog_title_delete": "Kustuta ligipääsuks vajalik tunnusluba" } diff --git a/web/public/static/langs/fa.json b/web/public/static/langs/fa.json index 9ef390d2..4d46c422 100644 --- a/web/public/static/langs/fa.json +++ b/web/public/static/langs/fa.json @@ -32,5 +32,27 @@ "action_bar_reservation_edit": "تغییر رزرو", "action_bar_reservation_delete": "حذف رزرو", "action_bar_mute_notifications": "ساکت کردن اعلان ها", - "action_bar_clear_notifications": "پاک کردن تمام اعلان ها" + "action_bar_clear_notifications": "پاک کردن تمام اعلان ها", + "action_bar_toggle_action_menu": "گشودن يا بستن فهرست کنش", + "action_bar_profile_title": "نمايه", + "action_bar_profile_settings": "تنظیمات", + "action_bar_profile_logout": "خروج", + "action_bar_sign_in": "ورود", + "action_bar_sign_up": "ثبت نام", + "message_bar_type_message": "یک پیام بنویسید", + "message_bar_error_publishing": "خطا در انتظار اعلان", + "message_bar_publish": "انتشار پیام", + "nav_button_all_notifications": "همه اعلان‌ها", + "nav_button_account": "حساب کاربری", + "nav_button_settings": "تنظیمات", + "nav_button_documentation": "مستندات", + "nav_button_publish_message": "انتشار اعلان", + "nav_button_muted": "اعلان بی‌صدا شد", + "nav_button_connecting": "در حال اتصال", + "nav_upgrade_banner_label": "ارتقا با ntfy پیشرفته", + "alert_notification_permission_required_title": "اعلان‌ها غیرفعال هستند", + "alert_notification_permission_required_description": "به مرورگر خود اجازه دهید تا اعلان‌های دسکتاپ را نمایش دهد", + "alert_notification_permission_denied_title": "اعلان‌ها مسدود هستند", + "alert_notification_ios_install_required_title": "لازم به نصب نسخه iOS است", + "alert_notification_ios_install_required_description": "برای فعال کردن اعلان‌ها در iOS، روی نماد اشتراک‌گذاری و افزودن به صفحه اصلی کلیک کنید" } diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json index f5954f8d..4ddf2920 100644 --- a/web/public/static/langs/fi.json +++ b/web/public/static/langs/fi.json @@ -170,7 +170,7 @@ "account_basics_tier_description": "Tilisi taso", "account_basics_phone_numbers_description": "Puheluilmoituksia varten", "prefs_reservations_dialog_title_add": "Varaa topikki", - "account_basics_tier_free": "Vapaa", + "account_basics_tier_free": "Maksuton", "account_upgrade_dialog_cancel_warning": "Tämä peruuttaa tilauksesi ja alentaa tilisi {{date}}. Tuona päivänä topikit sekä palvelimen välimuistissa olevat viestit poistetaan.", "notifications_click_copy_url_button": "Kopioi linkki", "account_basics_tier_admin": "Admin", @@ -266,7 +266,7 @@ "alert_not_supported_title": "Ilmoituksia ei tueta", "account_tokens_dialog_button_cancel": "Peruuta", "subscribe_dialog_error_user_anonymous": "Anonyymi", - "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Tallenna {{save}}.", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Säästä {{save}}.", "prefs_notifications_min_priority_high_and_higher": "Korkea prioriteetti ja korkeammat", "account_usage_basis_ip_description": "Tämän tilin käyttötilastot ja rajoitukset perustuvat IP-osoitteeseesi, joten ne voidaan jakaa muiden käyttäjien kanssa. Yllä esitetyt rajat ovat likimääräisiä perustuen olemassa oleviin rajoituksiin.", "publish_dialog_priority_high": "Korkea prioriteetti", @@ -400,5 +400,11 @@ "error_boundary_button_reload_ntfy": "Lataa ntfy uudelleen", "web_push_subscription_expiring_title": "Ilmoitukset keskeytetään", "web_push_subscription_expiring_body": "Avaa ntfy jatkaaksesi ilmoitusten vastaanottamista", - "web_push_unknown_notification_title": "Tuntematon ilmoitus vastaanotettu palvelimelta" + "web_push_unknown_notification_title": "Tuntematon ilmoitus vastaanotettu palvelimelta", + "alert_notification_ios_install_required_description": "Napauta Jaa-kuvaketta ja Lisää aloitusnäyttöön ottaaksesi ilmoitukset käyttöön iOS:ssä", + "prefs_notifications_web_push_disabled_description": "Ilmoituksia vastaanotetaan, kun verkkosovellus on käynnissä (WebSocket:in kautta)", + "web_push_unknown_notification_body": "Voit joutua päivittämään ntfy:n avaamalla verkkosovelluksen", + "notifications_actions_failed_notification": "Epäonnistunut toiminto", + "subscribe_dialog_subscribe_use_another_background_info": "Ilmoituksia muilta palvelimilta ei vastaanoteta, mikäli verkkosovellus ei ole avoinna", + "prefs_notifications_web_push_enabled_description": "Ilmoituksia vastaanotetaan siitä huolimatta, että verkkosovellus ei ole käynnissä (Web Push:n kautta)" } diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json index a7666a19..2fa8c32d 100644 --- a/web/public/static/langs/gl.json +++ b/web/public/static/langs/gl.json @@ -62,7 +62,7 @@ "notifications_none_for_topic_title": "Aínda non recibiches ningunha notificación para este tema.", "reserve_dialog_checkbox_label": "Reservar tema e configurar acceso", "notifications_loading": "Cargando notificacións…", - "publish_dialog_base_url_placeholder": "URL de servizo, ex. https://exemplo.com", + "publish_dialog_base_url_placeholder": "URL do servizo, ex. https://exemplo.com", "publish_dialog_topic_label": "Nome do tema", "publish_dialog_topic_placeholder": "Nome do tema, ex. alertas_equipo", "publish_dialog_topic_reset": "Restablecer tema", @@ -172,7 +172,7 @@ "account_tokens_table_token_header": "Token", "prefs_notifications_delete_after_never": "Nunca", "prefs_users_description": "Engadir/eliminar usuarias dos temas protexidos. Ten en conta que as credenciais gárdanse na almacenaxe local do navegador.", - "subscribe_dialog_subscribe_description": "Os temas poderían non estar proxetidos con contrasinal, así que elixe un nome complicado de adiviñar. Unha vez subscrita, podes PUT/POST notificacións.", + "subscribe_dialog_subscribe_description": "Os temas poden non estar protexidos con contrasinal, asi que escolle un nome que non sexa fácil de pesquisar. Unha vez suscrito, podes notificar con PUT/POST.", "account_upgrade_dialog_interval_yearly_discount_save_up_to": "aforro ata un {{discount}}%", "account_tokens_dialog_label": "Etiqueta, ex. notificación de Radarr", "account_tokens_table_expires_header": "Caducidade", @@ -315,17 +315,17 @@ "account_basics_password_dialog_current_password_incorrect": "Contrasinal incorrecto", "account_basics_phone_numbers_dialog_number_label": "Número de teléfono", "account_basics_password_dialog_button_submit": "Modificar contrasinal", - "account_basics_username_title": "Usuario", + "account_basics_username_title": "Identificador", "account_basics_phone_numbers_dialog_check_verification_button": "Código de confirmación", "account_usage_messages_title": "Mesaxes publicados", "account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS", "account_basics_tier_change_button": "Cambiar", "account_basics_phone_numbers_dialog_description": "Para usar a característica de chamadas de teléfono, vostede debe engadir e verificar ao menos un número de teléfono. A verificación pode ser realizada vía SMS ou a través de chamada.", - "account_delete_title": "Borrar conta", + "account_delete_title": "Eliminar a conta", "account_delete_dialog_label": "Contrasinal", "account_basics_tier_admin_suffix_with_tier": "(con tier {{tier}})", - "subscribe_dialog_login_username_label": "Nome de usuario, ex. phil", - "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} non autorizado", + "subscribe_dialog_login_username_label": "Identificador, ex. xoana", + "subscribe_dialog_error_user_not_authorized": "Identificador {{username}} non autorizado", "account_basics_title": "Conta", "account_basics_phone_numbers_no_phone_numbers_yet": "Aínda non hay números de teléfono", "subscribe_dialog_subscribe_button_generate_topic_name": "Xerar nome", @@ -333,9 +333,9 @@ "subscribe_dialog_subscribe_button_subscribe": "Subscribirse", "account_basics_phone_numbers_dialog_title": "Engadir número de teléfono", "account_basics_username_admin_tooltip": "É vostede Admin", - "account_delete_dialog_description": "Isto borrará permanentemente a túa conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu nome de usuario non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirme co seu contrasinal na caixa inferior.", + "account_delete_dialog_description": "Isto borrará permanentemente a conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu identificador non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirma co contrasinal na caixa inferior.", "account_usage_reservations_none": "Non hai temas reservados para esta conta", - "subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. phil_alertas", + "subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. alertas_xoana", "account_usage_title": "Uso", "account_basics_tier_upgrade_button": "Mexorar a Pro", "subscribe_dialog_error_topic_already_reserved": "Tema xa reservado", @@ -351,11 +351,11 @@ "account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado no portapapeis", "account_basics_tier_title": "Tipo de conta", "account_usage_cannot_create_portal_session": "Non foi posible abrir o portal de pagos", - "account_delete_description": "Borrar permanentemente a túa conta", + "account_delete_description": "Eliminar a conta de xeito definitivo", "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444", "account_basics_phone_numbers_dialog_code_placeholder": "ex. 123456", "account_basics_tier_manage_billing_button": "Xestionar pagos", - "account_basics_username_description": "Ei, ese eres ti ❤", + "account_basics_username_description": "Ei, es ti ❤", "account_basics_password_dialog_confirm_password_label": "Confirmar contrasinal", "account_basics_tier_interval_yearly": "anual", "account_delete_dialog_button_submit": "Borrar permanentemente a conta", @@ -364,7 +364,7 @@ "account_basics_password_dialog_new_password_label": "Novo contrasinal", "account_usage_of_limit": "de {{limit}}", "subscribe_dialog_error_user_anonymous": "anónimo", - "account_usage_basis_ip_description": "Estadísticas de uso e límites para esta conta están basados na sua IP, polo que poden estar compartidos con outros usuarios. Os limites mostrados son aproximados, basados nos ratios de limite existentes.", + "account_usage_basis_ip_description": "As estatísticas de uso e límites para esta conta están basados na IP, polo que poden estar compartidas con outras usuarias. Os limites mostrados son aproximados, baseados nos límites das taxas existentes.", "account_basics_password_dialog_title": "Modificar contrasinal", "account_usage_limits_reset_daily": "Límite de uso é reiniciado diariamente a medianoite (UTC(", "account_usage_unlimited": "Sen límites", @@ -380,7 +380,7 @@ "account_basics_phone_numbers_dialog_verify_button_call": "Chámame", "account_usage_emails_title": "Emails enviados", "account_basics_phone_numbers_dialog_channel_sms": "SMS", - "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse.", + "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, escribe as credenciais para subscribirte.", "action_bar_mute_notifications": "Acalar notificacións", "action_bar_unmute_notifications": "Reactivar notificacións", "alert_notification_permission_required_title": "Notificacións desactivadas", diff --git a/web/public/static/langs/hu.json b/web/public/static/langs/hu.json index e4e0b85b..45fcd50d 100644 --- a/web/public/static/langs/hu.json +++ b/web/public/static/langs/hu.json @@ -1,7 +1,7 @@ { "action_bar_send_test_notification": "Teszt értesítés küldése", "action_bar_clear_notifications": "Összes értesítés törlése", - "alert_not_supported_description": "A böngésző nem támogatja az értesítések fogadását.", + "alert_not_supported_description": "A böngésződ nem támogatja az értesítések fogadását", "action_bar_settings": "Beállítások", "action_bar_unsubscribe": "Leiratkozás", "message_bar_type_message": "Írd ide az üzenetet", @@ -9,19 +9,19 @@ "nav_button_all_notifications": "Összes értesítés", "nav_topics_title": "Feliratkozott témák", "alert_notification_permission_required_title": "Az értesítések le vannak tiltva", - "alert_notification_permission_required_description": "Engedélyezd a böngészőnek, hogy asztali értesítéseket jeleníttessen meg.", + "alert_notification_permission_required_description": "Engedélyezd a böngésződnek, hogy asztali értesítéseket jelenítsen meg", "nav_button_settings": "Beállítások", "nav_button_documentation": "Dokumentáció", "nav_button_publish_message": "Értesítés küldése", "alert_notification_permission_required_button": "Engedélyezés", - "alert_not_supported_title": "Nem támogatott funkció", - "notifications_copied_to_clipboard": "Másolva a vágólapra", + "alert_not_supported_title": "Az értesítések nincsenek támogatva", + "notifications_copied_to_clipboard": "Vágólapra másolva", "notifications_tags": "Címkék", "notifications_attachment_copy_url_title": "Másolja vágólapra a csatolmány URL-ét", "notifications_attachment_copy_url_button": "URL másolása", "notifications_attachment_open_title": "Menjen a(z) {{url}} címre", "notifications_attachment_open_button": "Csatolmány megnyitása", - "notifications_attachment_link_expired": "A letöltési hivatkozás lejárt", + "notifications_attachment_link_expired": "A letöltési link lejárt", "notifications_attachment_link_expires": "A hivatkozás {{date}}-kor jár le", "nav_button_subscribe": "Feliratkozás témára", "notifications_click_copy_url_title": "Másolja vágólapra a hivatkozás URL-ét", @@ -187,5 +187,33 @@ "prefs_users_edit_button": "Felhasználó szerkesztése", "prefs_users_delete_button": "Felhasználó törlése", "error_boundary_unsupported_indexeddb_title": "Privát böngészés nem támogatott", - "subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL" + "subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL", + "signup_form_username": "Felhasználónév", + "signup_form_password": "Jelszó", + "signup_form_button_submit": "Regisztráció", + "login_form_button_submit": "Bejelentkezés", + "login_link_signup": "Regisztráció", + "login_disabled": "Bejelentkezés kikapcsolva", + "action_bar_change_display_name": "Megjelenített név módosítása", + "action_bar_profile_logout": "Kijelentkezés", + "action_bar_sign_in": "Bejelentkezés", + "action_bar_sign_up": "Regisztráció", + "action_bar_profile_title": "Profil", + "nav_button_account": "Fiók", + "common_copy_to_clipboard": "Másolás vágólapra", + "action_bar_reservation_limit_reached": "Limit elérve", + "login_title": "Jelentkezz be a ntfy felhasználódba", + "signup_title": "Hozz létre egy ntfy felhasználói fiókot", + "signup_form_confirm_password": "Jelszó megerősítése", + "signup_already_have_account": "Már van felhasználód? Jelentkezz be!", + "action_bar_account": "Fiók", + "action_bar_profile_settings": "Beállítások", + "signup_error_username_taken": "A felhasználónév {{username}} már foglalt", + "signup_error_creation_limit_reached": "Felhasználói regisztráció limit elérve", + "action_bar_mute_notifications": "Értesítések némítása", + "action_bar_unmute_notifications": "Értesítések némításának feloldása", + "alert_notification_permission_denied_title": "Az értesítések blokkolva vannak", + "alert_notification_permission_denied_description": "Kérjük kapcsold őket vissza a böngésződben", + "alert_notification_ios_install_required_title": "iOS telepítés szükséges", + "alert_not_supported_context_description": "Az értesítések kizárólag HTTPS-en keresztül támogatottak. Ez a Notifications API korlátozása." } diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json index a562436a..0095138b 100644 --- a/web/public/static/langs/id.json +++ b/web/public/static/langs/id.json @@ -24,7 +24,7 @@ "nav_button_subscribe": "Berlangganan ke topik", "alert_notification_permission_required_title": "Notifikasi dinonaktifkan", "alert_notification_permission_required_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.", - "alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda.", + "alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda", "notifications_attachment_open_title": "Pergi ke {{url}}", "notifications_attachment_open_button": "Buka lampiran", "notifications_attachment_link_expires": "tautan kadaluwarsa {{date}}", diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index 7ddd60fc..1ba1eba8 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -316,5 +316,92 @@ "action_bar_unmute_notifications": "Riattiva audio notifiche", "alert_notification_ios_install_required_title": "E' richiesta l'installazione di iOS", "alert_notification_ios_install_required_description": "Fare clic sull'icona Condividi e Aggiungi alla schermata home per abilitare le notifiche su iOS", - "publish_dialog_checkbox_markdown": "Formatta come markdown" + "publish_dialog_checkbox_markdown": "Formatta come markdown", + "account_upgrade_dialog_interval_yearly": "Annualmente", + "account_tokens_table_token_header": "Token", + "account_tokens_table_label_header": "Etichetta", + "account_tokens_table_cannot_delete_or_edit": "Impossibile modificare o eliminare il token della sessione corrente", + "account_tokens_dialog_label": "Etichetta, ad esempio Notifiche Radarr", + "account_tokens_dialog_title_delete": "Elimina token di accesso", + "account_tokens_dialog_title_edit": "Modifica token di accesso", + "account_tokens_dialog_button_create": "Crea token", + "account_tokens_dialog_button_update": "Aggiorna token", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mails giornaliere", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messaggi giornalieri", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} spazio di archiviazione totale", + "notifications_actions_failed_notification": "Azione non riuscita", + "account_usage_attachment_storage_description": "{{filesize}} per file, eliminato dopo {{expiry}}", + "account_upgrade_dialog_title": "Cambia livello account", + "account_upgrade_dialog_interval_monthly": "Mensilmente", + "account_upgrade_dialog_cancel_warning": "Questa azione annullerà il tuo abbonamento e declasserà il tuo account il {{date}}. In quella data, le prenotazioni degli argomenti e i messaggi memorizzati nella cache del server verranno eliminati.", + "account_upgrade_dialog_reservations_warning_other": "Il livello selezionato consente meno argomenti riservati rispetto al livello attuale. Prima di cambiare il livello, elimina almeno {{count}} prenotazioni. Puoi rimuovere le prenotazioni nelle Impostazioni.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} argomenti riservati", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-mail giornaliere", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} telefonate giornaliere", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} telefonate giornaliere", + "account_upgrade_dialog_tier_features_no_calls": "Nessuna telefonata", + "account_tokens_description": "Utilizza i token di accesso quando pubblichi e ti iscrivi tramite l'API ntfy, così non dovrai inviare le credenziali del tuo account. Consulta la documentazione per saperne di più.", + "account_tokens_table_copied_to_clipboard": "Token di accesso copiato", + "account_tokens_table_create_token_button": "Crea token di accesso", + "account_tokens_table_last_origin_tooltip": "Dall'indirizzo IP {{ip}}, clicca per cercare", + "account_tokens_dialog_title_create": "Crea token di accesso", + "account_tokens_dialog_button_cancel": "Annulla", + "web_push_unknown_notification_body": "Potrebbe essere necessario aggiornare ntfy aprendo l'app web", + "account_upgrade_dialog_proration_info": "Prorata: quando si esegue l'upgrade tra piani a pagamento, la differenza di prezzo verrà addebitata immediatamente. Quando si esegue il downgrade a un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.", + "account_tokens_table_last_access_header": "Ultimo accesso", + "account_tokens_table_expires_header": "Scade", + "account_tokens_table_never_expires": "Non scade mai", + "account_tokens_table_current_session": "Sessione corrente del browser", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "Risparmia fino al {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save": "conserva {{discount}}%", + "prefs_users_description_no_sync": "Gli utenti e le password non vengono sincronizzati con il tuo account.", + "prefs_reservations_title": "Argomenti riservati", + "prefs_reservations_table_access_header": "Accesso", + "reservation_delete_dialog_action_delete_title": "Elimina i messaggi e gli allegati memorizzati nella cache", + "reservation_delete_dialog_submit_button": "Elimina prenotazione", + "account_tokens_dialog_expires_label": "Il token di accesso scade tra", + "account_tokens_dialog_expires_unchanged": "Lascia la data di scadenza invariata", + "account_tokens_delete_dialog_submit_button": "Elimina definitivamente il token", + "prefs_reservations_description": "Qui puoi riservare i nomi degli argomenti per uso personale. Riservare un argomento ti dà la proprietà dell'argomento e ti consente di definire i permessi di accesso per altri utenti sull'argomento.", + "prefs_reservations_add_button": "Aggiungi argomento riservato", + "prefs_reservations_edit_button": "Modifica accesso argomento", + "prefs_reservations_delete_button": "Reimposta accesso argomento", + "prefs_reservations_table_everyone_read_only": "Posso pubblicare e iscrivermi, tutti possono iscriversi", + "prefs_reservations_table_not_subscribed": "Non iscritto", + "prefs_reservations_table_everyone_write_only": "Posso pubblicare ed iscrivermi, tutti possono pubblicare", + "prefs_reservations_table_everyone_read_write": "Tutti possono pubblicare e iscriversi", + "prefs_reservations_dialog_title_delete": "Elimina prenotazione argomento", + "prefs_reservations_dialog_description": "Prenotando un argomento ne diventi proprietario e puoi definire le autorizzazioni di accesso per altri utenti.", + "reservation_delete_dialog_action_keep_description": "I messaggi e gli allegati memorizzati nella cache del server diventeranno visibili al pubblico per le persone a conoscenza del nome dell'argomento.", + "reservation_delete_dialog_action_delete_description": "I messaggi e gli allegati memorizzati nella cache verranno eliminati definitivamente. Questa azione non può essere annullata.", + "prefs_reservations_limit_reached": "Hai raggiunto il limite di argomenti riservati.", + "prefs_reservations_table_click_to_subscribe": "Clicca per iscriverti", + "prefs_reservations_dialog_title_add": "Prenota argomento", + "prefs_reservations_dialog_title_edit": "Modifica argomento riservato", + "account_tokens_dialog_expires_x_days": "Il token scade tra {{days}} giorni", + "account_tokens_dialog_expires_never": "Il token non scade mai", + "account_tokens_delete_dialog_title": "Elimina token di accesso", + "account_tokens_delete_dialog_description": "Prima di eliminare un token di accesso, assicurati che nessuna applicazione o script lo stia utilizzando attivamente. Questa azione non può essere annullata.", + "prefs_notifications_web_push_title": "Notifiche in background", + "prefs_notifications_web_push_enabled_description": "Le notifiche vengono ricevute anche quando l'app Web non è in esecuzione (tramite Web Push)", + "prefs_notifications_web_push_disabled_description": "Le notifiche vengono ricevute quando l'app Web è in esecuzione (tramite WebSocket)", + "prefs_notifications_web_push_enabled": "Abilitato per {{server}}", + "prefs_notifications_web_push_disabled": "Disabilitato", + "prefs_users_table_cannot_delete_or_edit": "Impossibile eliminare o modificare l'utente registrato", + "prefs_appearance_theme_title": "Tema", + "prefs_appearance_theme_system": "Sistema (predefinito)", + "prefs_appearance_theme_dark": "Modalità scura", + "prefs_appearance_theme_light": "Modalità chiara", + "prefs_reservations_table_topic_header": "Argomento", + "prefs_reservations_dialog_access_label": "Accesso", + "reservation_delete_dialog_description": "La rimozione di una prenotazione comporta la rinuncia alla proprietà dell'argomento e consente ad altri di riservarlo. Puoi mantenere o eliminare i messaggi e gli allegati esistenti.", + "prefs_reservations_table_everyone_deny_all": "Solo io posso pubblicare e iscrivermi", + "prefs_reservations_dialog_topic_label": "Argomento", + "reservation_delete_dialog_action_keep_title": "Mantieni i messaggi e gli allegati memorizzati nella cache", + "web_push_subscription_expiring_title": "Le notifiche verranno sospese", + "web_push_subscription_expiring_body": "Apri ntfy per continuare a ricevere notifiche", + "web_push_unknown_notification_title": "Notifica sconosciuta ricevuta dal server", + "account_tokens_dialog_expires_x_hours": "Il token scade tra {{hours}} ore", + "prefs_reservations_table": "Tabella argomenti riservati" } diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json index 84afc30b..3d9643e0 100644 --- a/web/public/static/langs/ja.json +++ b/web/public/static/langs/ja.json @@ -7,7 +7,7 @@ "action_bar_clear_notifications": "全ての通知を消去", "action_bar_unsubscribe": "購読解除", "nav_button_documentation": "ドキュメント", - "alert_not_supported_description": "通知機能はこのブラウザではサポートされていません。", + "alert_not_supported_description": "通知機能はこのブラウザではサポートされていません", "notifications_copied_to_clipboard": "クリップボードにコピーしました", "notifications_example": "例", "publish_dialog_title_topic": "{{topic}}に送信", @@ -28,7 +28,7 @@ "message_bar_type_message": "メッセージを入力してください", "nav_topics_title": "購読しているトピック", "nav_button_subscribe": "トピックを購読", - "alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください。", + "alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください", "alert_notification_permission_required_button": "許可する", "notifications_attachment_link_expires": "リンクは {{date}} に失効します", "notifications_click_copy_url_button": "リンクをコピー", @@ -191,7 +191,7 @@ "signup_form_username": "ユーザー名", "signup_form_password": "パスワード", "signup_form_confirm_password": "パスワードを確認", - "signup_already_have_account": "アカウントをお持ちならサインイン", + "signup_already_have_account": "アカウントをお持ちならサインイン!", "signup_disabled": "サインアップは無効化されています", "signup_error_creation_limit_reached": "アカウント作成制限に達しました", "login_title": "あなたのntfyアカウントにサインイン", @@ -380,5 +380,28 @@ "account_upgrade_dialog_tier_features_calls_other": "電話 1日 {{calls}} 回", "publish_dialog_chip_call_no_verified_numbers_tooltip": "認証済み電話番号がありません", "account_basics_phone_numbers_dialog_channel_sms": "SMS", - "account_basics_phone_numbers_dialog_channel_call": "電話する" + "account_basics_phone_numbers_dialog_channel_call": "電話する", + "error_boundary_button_reload_ntfy": "ntfyをリロード", + "prefs_appearance_theme_light": "ライトモード", + "web_push_subscription_expiring_title": "通知は一時停止されます", + "web_push_subscription_expiring_body": "ntfyを開いて通知の受信を継続させてください", + "alert_notification_ios_install_required_description": "Shareアイコンをクリック・ホーム画面に追加してiOSでの通知を有効化して下さい", + "action_bar_mute_notifications": "通知をミュート", + "action_bar_unmute_notifications": "通知ミュートを解除", + "alert_notification_permission_denied_title": "通知はブロックされています", + "alert_notification_permission_denied_description": "ブラウザで通知を再度有効化してください", + "notifications_actions_failed_notification": "アクション失敗", + "alert_notification_ios_install_required_title": "iOS用インストールが必要です", + "publish_dialog_checkbox_markdown": "Markdownとして表示", + "subscribe_dialog_subscribe_use_another_background_info": "ウェブアプリが開かれていない場合は他のサーバーからの通知は受信されません", + "prefs_notifications_web_push_title": "バックグラウンド通知", + "prefs_notifications_web_push_enabled_description": "ウェブアプリが開かれていなくても通知を受信します (Web Push経由)", + "prefs_notifications_web_push_disabled_description": "ウェブアプリが開かれていなくても通知を受信します (WebSocket経由)", + "prefs_notifications_web_push_enabled": "{{server}}で有効", + "prefs_notifications_web_push_disabled": "無効", + "prefs_appearance_theme_title": "テーマ", + "prefs_appearance_theme_system": "システム (既定)", + "prefs_appearance_theme_dark": "ダークモード", + "web_push_unknown_notification_title": "不明な通知を受信しました", + "web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります" } diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json index ca259523..2bcf6391 100644 --- a/web/public/static/langs/nb_NO.json +++ b/web/public/static/langs/nb_NO.json @@ -3,7 +3,7 @@ "action_bar_settings": "Innstillinger", "action_bar_send_test_notification": "Send testmerknad", "action_bar_clear_notifications": "Tøm alle merknader", - "action_bar_unsubscribe": "Opphev abonnement", + "action_bar_unsubscribe": "Meld av", "message_bar_type_message": "Skriv en melding her", "nav_button_all_notifications": "Alle merknader", "nav_button_settings": "Innstillinger", @@ -133,8 +133,8 @@ "publish_dialog_chip_delay_label": "Forsink leveringen", "publish_dialog_details_examples_description": "For eksempler og en detaljert beskrivelse av alle sendefunksjoner, se dokumentasjonen.", "publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com", - "alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.", - "alert_not_supported_description": "Varsler støttes ikke i nettleseren din.", + "alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler", + "alert_not_supported_description": "Varsler støttes ikke i nettleseren din", "notifications_attachment_file_app": "Android-app-fil", "notifications_no_subscriptions_description": "Klikk på \"{{linktext}}\"-koblingen for å opprette eller abonnere på et emne. Etter det kan du sende meldinger via PUT eller POST, og du vil motta varsler her.", "notifications_actions_http_request_title": "Send HTTP {{metode}} til {{url}}", @@ -195,5 +195,213 @@ "signup_form_username": "Brukernavn", "signup_form_password": "Passord", "signup_form_button_submit": "Meld deg på", - "signup_form_confirm_password": "Bekreft passord" + "signup_form_confirm_password": "Bekreft passord", + "signup_disabled": "Registrering er deaktivert", + "common_copy_to_clipboard": "Kopier til utklippstavle", + "signup_form_toggle_password_visibility": "Slå av/på passordvisning", + "signup_already_have_account": "Har du allerede en konto? Logg inn!", + "signup_error_username_taken": "Brukernavnet {{username}} er allerede opptatt", + "signup_error_creation_limit_reached": "Grense for nye kontoer nådd", + "login_title": "Logg inn på ntfy-kontoen din", + "login_form_button_submit": "Logg inn", + "login_link_signup": "Registrer deg", + "login_disabled": "Innlogging deaktivert", + "action_bar_change_display_name": "Endre visningsnavn", + "account_basics_tier_interval_yearly": "årlig", + "account_basics_tier_change_button": "Endre", + "account_usage_reservations_title": "Reserverte emner", + "account_usage_cannot_create_portal_session": "Kunne ikke åpne betalingsportalen", + "account_delete_dialog_label": "Passord", + "account_tokens_table_copied_to_clipboard": "Tilgangstoken kopiert", + "account_tokens_table_last_origin_tooltip": "Fra IP-adresse {{ip}}, klikk for å gjøre oppslag", + "account_tokens_dialog_title_create": "Opprett tilgangstoken", + "account_tokens_delete_dialog_title": "Slett tilgangstoken", + "prefs_users_table_cannot_delete_or_edit": "Kan ikke slette eller redigere innlogget bruker", + "prefs_reservations_table_everyone_deny_all": "Bare jeg kan publisere og abonnere", + "prefs_reservations_dialog_access_label": "Tilgang", + "reservation_delete_dialog_action_keep_title": "Behold mellomlagrede meldinger og vedlegg", + "action_bar_reservation_add": "Reserver emne", + "action_bar_reservation_edit": "Endre reservasjon", + "action_bar_reservation_delete": "Fjern reservasjon", + "action_bar_reservation_limit_reached": "Grense nådd", + "account_basics_phone_numbers_dialog_description": "For å bruke ringevarslingsfunksjonen må du legge til og verifisere minst ett telefonnummer. Verifisering kan gjøres vis SMS eller oppringing.", + "account_basics_tier_interval_monthly": "månedlig", + "account_basics_tier_upgrade_button": "Oppgrader til Pro", + "account_usage_emails_title": "E-poster sendt", + "account_delete_description": "Slett kontoen din permanent", + "account_usage_calls_title": "Telefonsamtaler", + "account_upgrade_dialog_interval_monthly": "Månedlig", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverte emner", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige meldinger", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} daglige e-poster", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} daglige telefonsamtaler", + "account_upgrade_dialog_tier_selected_label": "Valgt", + "account_upgrade_dialog_tier_current_label": "Nåværende", + "account_upgrade_dialog_button_cancel": "Avbryt", + "account_upgrade_dialog_billing_contact_email": "For faktureringsspørsmål, vennligst kontakt oss direkte.", + "account_tokens_table_token_header": "Token", + "account_tokens_table_label_header": "Etikett", + "account_tokens_table_cannot_delete_or_edit": "Kan ikke redigere eller slette nåværende økt-token", + "account_tokens_table_create_token_button": "Opprett tilgangstoken", + "account_tokens_dialog_expires_unchanged": "La utløpsdato være uendret", + "account_tokens_dialog_expires_x_hours": "Token utløper om {{hours}} timer", + "account_tokens_delete_dialog_description": "Før du sletter et tilgangstoken, sørg for at ingen applikasjoner eller script bruker det. Denne handlingen kan ikke angres.", + "account_tokens_delete_dialog_submit_button": "Slett token permanent", + "prefs_users_description_no_sync": "Brukere og passord synkroniseres ikke til kontoen din.", + "prefs_reservations_dialog_title_delete": "Slett emnereservasjon", + "prefs_reservations_dialog_topic_label": "Emne", + "display_name_dialog_title": "Endre visningsnavn", + "reserve_dialog_checkbox_label": "Rserver emne og sett opp tilgang", + "publish_dialog_chip_call_label": "Telefonsamtale", + "account_basics_tier_free": "Gratis", + "account_basics_tier_basic": "Grunnleggende", + "account_basics_tier_canceled_subscription": "Abonnementet ditt ble avsluttet og blir degradert til en gratiskonto den {{date}}.", + "account_delete_dialog_description": "Dette vil slette kontoen din permanent, inkludert alle data som er lagret på serveren. Etter sletting vil brukernavnet ditt være utilgjengelig i 7 dager. Hvis du virkelig vil fortsette, vennligst bekreft ved å skrive passordet ditt i boksen under.", + "account_upgrade_dialog_proration_info": "Pro-rate: Når du oppgraderer mellom betalte kontotyper, vil prisforskjellen bli fakturert umiddelbart. Når du nedgraderer til en billigere kontotype, vil det allerede innbetalte beløpet brukes til å betale for fremtidige regningsperioder.", + "account_upgrade_dialog_reservations_warning_other": "Det valgte nivået tillater færre reserverte emner enn ditt nåvære nivå. Før du endrer nivå, vennligst slett minst {{count}} reservasjoner. Du kan slette reservasjoner i Innstillingene.", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} daglig melding", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pr. år. Fakturert månedlig.", + "account_upgrade_dialog_button_redirect_signup": "Registrer deg nå", + "account_upgrade_dialog_button_pay_now": "Betal nå og abonner", + "account_upgrade_dialog_button_cancel_subscription": "Avslutt abonnement", + "account_tokens_description": "Bruk tilgangstokener når du publiserer og abonnerer via ntfy-APIet, slik at du ikke trenger å sende innloggingsinformasjon for kontoen din. Se dokumentasjonen for å lære mer.", + "account_tokens_table_current_session": "Nåværende nettleserøkt", + "prefs_appearance_theme_system": "System (standard)", + "prefs_notifications_web_push_disabled_description": "Varslinger mottas når web-appen kjører (via WebSocket)", + "prefs_appearance_theme_title": "Tema", + "prefs_appearance_theme_dark": "Mørk modus", + "prefs_appearance_theme_light": "Lys modus", + "prefs_reservations_title": "Reserverte emner", + "prefs_reservations_table_click_to_subscribe": "Klikk for å abonnere", + "prefs_reservations_table_everyone_read_write": "Alle kan publisere og abonnere", + "prefs_reservations_table_not_subscribed": "Ikke abonnent", + "prefs_reservations_table_everyone_write_only": "Jeg kan publisere og abonnere, alle andre kan publisere", + "prefs_reservations_dialog_title_add": "Reserver emne", + "prefs_reservations_dialog_title_edit": "Rediger reservert emne", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reservert emne", + "reservation_delete_dialog_action_delete_title": "Slett mellomlagrede meldinger og vedlegg", + "nav_upgrade_banner_label": "Oppgrader til ntfy Pro", + "nav_upgrade_banner_description": "Reserver emner, flere meldinger & e-poster, og større vedlegg", + "account_delete_dialog_button_submit": "Slett konto permanent", + "account_basics_username_description": "Hei, det er deg ❤", + "account_basics_username_admin_tooltip": "Du er administrator", + "account_basics_password_title": "Passord", + "account_basics_password_description": "Endre passordet ditt", + "account_usage_title": "Forbruk", + "account_delete_dialog_button_cancel": "Avbryt", + "account_tokens_dialog_title_delete": "Slett tilgangstoken", + "account_tokens_dialog_label": "Etikett, f.eks. Radarr-varslinger", + "prefs_reservations_table": "Tabell over reserverte emner", + "prefs_reservations_edit_button": "Rediger tilgang til emne", + "prefs_reservations_delete_button": "Nullstill tilgang til emne", + "prefs_reservations_table_topic_header": "Emne", + "account_basics_title": "Konto", + "account_basics_phone_numbers_dialog_code_label": "Verifiseringskode", + "alert_notification_permission_denied_title": "Varslinger blokkert", + "alert_notification_permission_denied_description": "Vennligst reaktiver dem i nettleseren din", + "alert_notification_ios_install_required_title": "iOS-installasjon kreves", + "alert_notification_ios_install_required_description": "Klikk på Del-ikonet og Legg til hjemmeskjerm for å aktivere varslinger på iOS", + "action_bar_mute_notifications": "Demp varslinger", + "action_bar_unmute_notifications": "Avdemp varslinger", + "action_bar_profile_title": "Profil", + "action_bar_profile_logout": "Logg ut", + "action_bar_sign_in": "Logg inn", + "action_bar_sign_up": "Registrer deg", + "alert_not_supported_context_description": "Varslinger er kun støttet over HTTPS. Dette er en begrensning i Varslings-APIet.", + "notifications_actions_failed_notification": "Handling feilet", + "display_name_dialog_description": "Angi et alternativt navn for et emne som vises i abonneringslisten. Dette hjelper til med å enklere identifisere emner med kompliserte navn.", + "display_name_dialog_placeholder": "Visningnavn", + "publish_dialog_call_label": "Telefonsamtale", + "publish_dialog_call_item": "Ring telefonnummer {{number}}", + "publish_dialog_call_reset": "Fjern telefonsamtale", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Ingen verfiserte telefonnumre", + "publish_dialog_checkbox_markdown": "Formatter som Markdown", + "subscribe_dialog_subscribe_use_another_background_info": "Varslinger fra andre servere vil ikke bli tatt imot når webappen ikke er åpen", + "subscribe_dialog_subscribe_button_generate_topic_name": "Generer navn", + "subscribe_dialog_error_topic_already_reserved": "Emne allerede reservert", + "account_basics_username_title": "Brukernavn", + "account_basics_password_dialog_title": "Endre passord", + "account_basics_password_dialog_current_password_label": "Nåværende passord", + "account_basics_password_dialog_new_password_label": "Nytt passord", + "account_basics_password_dialog_confirm_password_label": "Bekreft passord", + "account_basics_password_dialog_button_submit": "Endre passord", + "account_basics_password_dialog_current_password_incorrect": "Passordet er feil", + "account_basics_phone_numbers_title": "Telefonnumre", + "account_basics_phone_numbers_description": "For telefonvarsling", + "account_basics_phone_numbers_no_phone_numbers_yet": "Ingen telefonnumre enda", + "account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopiert til utklippstavle", + "account_basics_phone_numbers_dialog_title": "Legg til telefonnummer", + "account_basics_phone_numbers_dialog_number_label": "Telefonnummer", + "account_basics_phone_numbers_dialog_number_placeholder": "f.eks. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Ring meg", + "account_basics_phone_numbers_dialog_code_placeholder": "f.eks. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Bekreft kode", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Ring", + "account_usage_of_limit": "av {{limit}}", + "account_usage_unlimited": "Ubegrenset", + "account_usage_limits_reset_daily": "Forbruksgrenser nullstilles hver dag ved midnatt (UTC)", + "account_basics_tier_title": "Kontotype", + "account_basics_tier_description": "Din kontos styrke", + "account_basics_tier_admin": "Administrator", + "account_basics_tier_admin_suffix_with_tier": "(med {{tier}} nivå)", + "account_basics_tier_admin_suffix_no_tier": "(ingen nivå)", + "account_basics_tier_paid_until": "Abonnement betalt til {{date}}, og vil bli fornyet automatisk", + "account_basics_tier_payment_overdue": "Betalingen din har forfalt. Vennligst oppdater betalingsmetoden din, hvis ikke blir kontoen din snart degradert.", + "account_basics_tier_manage_billing_button": "Behandle betalinger", + "account_usage_messages_title": "Publiserte meldinger", + "account_usage_calls_none": "Ingen telefonsamtaler kan foretas med denne kontoen", + "account_usage_reservations_none": "Ingen reserverte emner for denne kontoen", + "account_usage_attachment_storage_title": "Vedleggslagring", + "account_usage_basis_ip_description": "Forbruksstatistikk og -grenser for denne kontoen er basert på IP-adressen din, så det kan være de er delt med andre brukere. Forbruksgrenser vist over er omtrentlige, basert på eksisterende begrensninger.", + "account_delete_title": "Slett konto", + "account_delete_dialog_billing_warning": "Sletting av kontoen din avslutter også abonnementet og betalingene dine umiddelbart. Du vil ikke ha tilgang til betalingsportalen lenger.", + "account_upgrade_dialog_title": "Endre kontonivå", + "account_upgrade_dialog_interval_yearly": "Årlig", + "account_upgrade_dialog_interval_yearly_discount_save": "spar {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "spar inntil {{discount}}%", + "account_upgrade_dialog_cancel_warning": "Dette vil avslutte abonnementet ditt, og nedgradere kontoen din den {{date}}. På den datoen vil alle emnereservasjoner såvel som meldinger lagret på serveren bli slettet.", + "account_upgrade_dialog_reservations_warning_one": "Det valgte nivået tillater færre reserverte emner enn ditt nåvære nivå. Før du endrer nivå, vennligst slett minst én reservasjon. Du kan slette reservasjoner i Innstillingene.", + "account_upgrade_dialog_tier_features_no_reservations": "Ingen reserverte emner", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} daglig e-post", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} daglige telefonsamtaler", + "account_upgrade_dialog_tier_features_no_calls": "Ingen telefonsamtaler", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pr. fil", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total lagringsplass", + "account_upgrade_dialog_tier_price_per_month": "måned", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} fakturert årlig. Spar {{save}}.", + "account_upgrade_dialog_billing_contact_website": "For faktureringsspørsmål, vennligst se vår nettside.", + "account_upgrade_dialog_button_update_subscription": "Oppdater abonnement", + "account_tokens_title": "Tilgangstokener", + "account_tokens_table_last_access_header": "Sist aksessert", + "account_tokens_table_expires_header": "Utløper", + "account_tokens_table_never_expires": "Utløper aldri", + "account_tokens_dialog_title_edit": "Rediger tilgangstoken", + "account_tokens_dialog_button_create": "Opprett token", + "account_tokens_dialog_button_update": "Oppdater token", + "account_tokens_dialog_button_cancel": "Avbryt", + "account_tokens_dialog_expires_label": "Tilgangstoken utløper om", + "account_tokens_dialog_expires_x_days": "Token utløper om {{days}} dager", + "account_tokens_dialog_expires_never": "Token utløper aldri", + "prefs_notifications_web_push_title": "Bakgrunnsvarslinger", + "prefs_notifications_web_push_enabled_description": "Varslinger mottas send om web-appen ikke kjører (via Web Push)", + "prefs_notifications_web_push_enabled": "Aktivert for {{server}}", + "prefs_notifications_web_push_disabled": "Deaktivert", + "prefs_reservations_description": "Du kan reservere emnenavn for personlig bruk her. Reservasjon av et emne gir deg eierskap over emnet og lar deg definere tilgangsrettigheter for andre brukere av dette emnet.", + "prefs_reservations_limit_reached": "Du har nådd grensen for antall reserverte emner du kan ha.", + "prefs_reservations_add_button": "Legg til reservert emne", + "prefs_reservations_table_access_header": "Tilgang", + "prefs_reservations_table_everyone_read_only": "Jeg kan publisere og abonnere, alle andre kan abonnere", + "prefs_reservations_dialog_description": "Reservering av et emne gir deg eierskap over emnet, og lar deg definere tilgangsrettigheter for andre brukere av emnet.", + "reservation_delete_dialog_description": "Ved å fjerne en reservasjon gir du fra deg eierskapet over emnet, og gir dermed andre muligheten til å reservere det. Du kan beholde eller slette eksisterende meldinger og vedlegg.", + "reservation_delete_dialog_action_keep_description": "Meldinger og vedlegg som er mellomlagret på serveren vil bli synlige for alle som kjenner til emnenavnet.", + "reservation_delete_dialog_action_delete_description": "Mellomlagrede meldinger og vedlegg vil bli permanent slettet. Denne handlingen kan ikke angres.", + "reservation_delete_dialog_submit_button": "Slett reservasjon", + "error_boundary_button_reload_ntfy": "Last inn ntfy på nytt", + "web_push_subscription_expiring_title": "Varslinger vil bli satt på pause", + "web_push_subscription_expiring_body": "Åpne ntfy for å fortsette å motta varslinger", + "web_push_unknown_notification_title": "Ukjent varsel mottatt fra server", + "web_push_unknown_notification_body": "Du må muligens oppdatere ntfy ved å åpne web-appen", + "account_usage_attachment_storage_description": "{{filesize}} pr. fil, slettet etter {{expiry}}" } diff --git a/web/public/static/langs/pl.json b/web/public/static/langs/pl.json index 20cbfe29..3f90cac4 100644 --- a/web/public/static/langs/pl.json +++ b/web/public/static/langs/pl.json @@ -404,5 +404,10 @@ "prefs_reservations_dialog_title_add": "Zarezerwuj temat", "reservation_delete_dialog_action_keep_title": "Zachowaj wiadomości i załącznik w pamięci cache", "reservation_delete_dialog_action_keep_description": "Wiadomości i załączniki które są zapisane w pamięci cache będą dostępne publicznie dla każdego znającego nazwę powiązanego z nimi tematu.", - "web_push_unknown_notification_title": "Nieznane powiadomienie otrzymane od serwera" + "web_push_unknown_notification_title": "Nieznane powiadomienie otrzymane od serwera", + "action_bar_unmute_notifications": "Włącz ponownie powiadomienia", + "prefs_appearance_theme_title": "Wygląd", + "prefs_reservations_dialog_description": "Zastrzeżenie tematu daje użytkownikowi prawo własności do tego tematu i umożliwia zdefiniowanie uprawnień dostępu do tego tematu dla innych użytkowników.", + "reservation_delete_dialog_description": "Usunięcie rezerwacji powoduje rezygnację z prawa własności do tematu i umożliwia innym jego zarezerwowanie. Istniejące wiadomości i załączniki można zachować lub usunąć.", + "web_push_unknown_notification_body": "Konieczne może być zaktualizowanie ntfy poprzez otwarcie aplikacji internetowej" } diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json index 48159d21..1c988568 100644 --- a/web/public/static/langs/pt.json +++ b/web/public/static/langs/pt.json @@ -16,7 +16,7 @@ "nav_button_muted": "Notificações desativadas", "nav_button_connecting": "A ligar", "alert_notification_permission_required_title": "As notificações estão desativadas", - "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações.", + "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações", "alert_not_supported_title": "Notificações não suportadas", "notifications_list": "Lista de notificações", "alert_not_supported_description": "As notificações não são suportadas pelo seu navegador", @@ -215,14 +215,14 @@ "action_bar_reservation_add": "Reservar tópico", "action_bar_sign_up": "Registar", "nav_button_account": "Conta", - "common_copy_to_clipboard": "Copiar", - "nav_upgrade_banner_label": "Atualizar para ntfy Pro", - "alert_not_supported_context_description": "Notificações são suportadas apenas sobre HTTPS. Essa é uma limitação da API de Notificações.", - "display_name_dialog_title": "Alterar nome mostrado", - "display_name_dialog_description": "Configura um nome alternativo ao tópico que é mostrado na lista de assinaturas. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.", - "display_name_dialog_placeholder": "Nome exibido", + "common_copy_to_clipboard": "Copiar à área de transferência", + "nav_upgrade_banner_label": "Upgrade para ntfy Pro", + "alert_not_supported_context_description": "As notificações são apenas suportadas através de HTTPS. Isto é uma limitação da Notifications API.", + "display_name_dialog_title": "Alterar o nome público", + "display_name_dialog_description": "Configurar um nome alternativo para um tópico que é mostrado na lista de subscrições. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.", + "display_name_dialog_placeholder": "Nome público", "reserve_dialog_checkbox_label": "Reservar tópico e configurar acesso", - "publish_dialog_call_label": "Chamada telefônica", + "publish_dialog_call_label": "Chamada telefónica", "publish_dialog_call_placeholder": "Número de telefone para ligar com a mensagem, ex: +12223334444, ou 'Sim'", "publish_dialog_call_reset": "Remover chamada telefônica", "publish_dialog_chip_call_label": "Chamada telefônica", @@ -231,17 +231,17 @@ "alert_notification_ios_install_required_description": "Clique no ícone Compartilhar e Adicionar à Tela Inicial para ativar as notificações no iOS", "publish_dialog_checkbox_markdown": "Formatar como Markdown", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Números de telefone não verificados", - "subscribe_dialog_error_topic_already_reserved": "Tópico já está reservado", + "subscribe_dialog_error_topic_already_reserved": "Tópico já reservado", "action_bar_mute_notifications": "Silenciar notificações", "alert_notification_permission_denied_title": "Notificações estão bloqueadas", "alert_notification_permission_denied_description": "Por favor reative-as em seu navegador", "alert_notification_ios_install_required_title": "Requer instalação em iOS", "notifications_actions_failed_notification": "Houve uma falha na ação", - "publish_dialog_call_item": "Ligar para o número {{number}}", + "publish_dialog_call_item": "Ligar para o número de telefone {{number}}", "subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas enquanto o aplicativo web não estiver aberto", - "account_basics_username_description": "Olá, é você ❤", - "account_basics_password_dialog_new_password_label": "Nova senha", - "account_basics_password_dialog_current_password_incorrect": "Senha incorreta", + "account_basics_username_description": "Olá, és tu ❤", + "account_basics_password_dialog_new_password_label": "Nova palavra-passe", + "account_basics_password_dialog_current_password_incorrect": "Palavra-passe inválida", "account_basics_phone_numbers_title": "Números de telefone", "account_basics_phone_numbers_dialog_description": "Para utilizar o recurso de notificação por ligação, você precisa adicionar e verificar pelo menos um número de telefone. A verificação poderá ser feita via SMS ou ligação telefônica.", "account_basics_phone_numbers_dialog_title": "Adicionar número de telefone", @@ -258,20 +258,20 @@ "account_usage_reservations_none": "Esta conta não possui tópicos reservados", "account_usage_attachment_storage_title": "Armazenamento de anexos", "account_usage_emails_title": "E-mails enviados", - "account_basics_password_description": "Alterar a senha da sua conta", - "account_basics_password_dialog_title": "Alterar a senha", + "account_basics_password_description": "Mudar a palavra-passe da conta", + "account_basics_password_dialog_title": "Mudar a palavra-passe", "account_basics_phone_numbers_description": "Para notificações por ligação", "account_basics_tier_paid_until": "Assinatura paga até {{date}}, e será renovada automaticamente", - "account_basics_password_dialog_confirm_password_label": "Confirmar senha", - "account_basics_password_dialog_button_submit": "Alterar senha", + "account_basics_password_dialog_confirm_password_label": "Confirmar palavra-passe", + "account_basics_password_dialog_button_submit": "Mudar palavra-passe", "account_basics_title": "Conta", - "account_basics_username_admin_tooltip": "Você é Administrador", - "account_basics_password_title": "Senha", - "account_basics_password_dialog_current_password_label": "Senha atual", + "account_basics_username_admin_tooltip": "És Admin", + "account_basics_password_title": "Palavra-passe", + "account_basics_password_dialog_current_password_label": "Palavra-passe atual", "account_basics_phone_numbers_no_phone_numbers_yet": "Nenhum número de telefone", "account_basics_phone_numbers_copied_to_clipboard": "Telefones copiados para área de transferência", "account_basics_phone_numbers_dialog_channel_sms": "SMS", - "account_usage_title": "Uso", + "account_usage_title": "Utilização", "account_usage_of_limit": "de {{limit}}", "account_usage_unlimited": "Ilimitado", "account_usage_limits_reset_daily": "Limites de uso são resetados diariamente à meia noite (UTC)", @@ -290,5 +290,24 @@ "account_usage_messages_title": "Mensagens publicadas", "account_usage_calls_title": "Ligações realizadas", "account_usage_calls_none": "Esta conta não pode realizar ligações", - "account_usage_reservations_title": "Tópicos reservados" + "account_usage_reservations_title": "Tópicos reservados", + "account_basics_username_title": "Usuário", + "account_delete_dialog_description": "Isto irá eliminar definitivamente a sua conta, incluindo dados que estejam armazenados no servidor. Apos ser eliminado, o nome de utilizador ficará indisponível durante 7 dias. Se deseja mesmo proceder, por favor confirme com a sua palavra-passe na caixa abaixo.", + "account_delete_dialog_button_submit": "Eliminar conta definitivamente", + "account_delete_dialog_billing_warning": "Eliminar a sua conta também cancela a sua subscrição de faturação imediatamente. Não terá acesso ao portal de faturação de futuro.", + "account_upgrade_dialog_title": "Alterar o nível da sua conta", + "account_upgrade_dialog_interval_monthly": "Mensalmente", + "account_upgrade_dialog_interval_yearly": "Anualmente", + "account_upgrade_dialog_interval_yearly_discount_save": "poupe {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "poupe até {{discount}}%", + "account_delete_dialog_label": "Palavra-passe", + "account_usage_cannot_create_portal_session": "Impossível abrir o portal de faturação", + "account_usage_basis_ip_description": "Estatísticas de utilização e limites para esta conta são baseadas no seu endereço IP, pelo que podem ser partilhados com outros utilizadores. Os limites mostrados acima são aproximados com base nos limites existentes.", + "account_usage_attachment_storage_description": "{{filesize}} por ficheiro, eliminado após {{expiry}}", + "account_delete_title": "Eliminar conta", + "account_delete_description": "Eliminar definitivamente a sua conta", + "account_delete_dialog_button_cancel": "Cancelar", + "account_upgrade_dialog_cancel_warning": "Isto irá cancelar a sua assinatura, e fazer downgrade da sua conta em {{date}}. Nessa data, tópicos reservados bem como mensagens guardadas no servidor serão eliminados.", + "account_upgrade_dialog_proration_info": "Proporção: Quando atualizar entre planos pagos, a diferença de preço será debitada imediatamente. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação.", + "prefs_users_description_no_sync": "Utilizadores e palavras-passe não estão sincronizados com a sua conta." } diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json index bfaf68af..ffe4131a 100644 --- a/web/public/static/langs/pt_BR.json +++ b/web/public/static/langs/pt_BR.json @@ -8,10 +8,10 @@ "nav_button_settings": "Configurações", "nav_button_subscribe": "Inscrever no tópico", "alert_notification_permission_required_title": "Notificações estão desativadas", - "alert_notification_permission_required_description": "Conceder ao navegador permissão para mostrar notificações.", + "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações", "alert_notification_permission_required_button": "Conceder agora", "alert_not_supported_title": "Notificações não são suportadas", - "alert_not_supported_description": "Notificações não são suportadas pelo seu navegador.", + "alert_not_supported_description": "Notificações não são suportadas pelo seu navegador", "notifications_copied_to_clipboard": "Copiado para a área de transferência", "notifications_tags": "Etiquetas", "notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência", @@ -189,15 +189,15 @@ "prefs_users_delete_button": "Excluir usuário", "error_boundary_unsupported_indexeddb_title": "Navegação anônima não suportada", "error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.

Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso nesta edição do GitHub, ou falar conosco em Discord ou Matrix.", - "action_bar_reservation_add": "Reserve topic", - "action_bar_reservation_edit": "Change reservation", - "signup_disabled": "Registrar está desativado", - "signup_error_username_taken": "Usuário {{username}} já existe", + "action_bar_reservation_add": "Reservar tópico", + "action_bar_reservation_edit": "Mudar reserva", + "signup_disabled": "O registro está desativado", + "signup_error_username_taken": "O nome de usuário {{username}} já está em uso", "signup_error_creation_limit_reached": "Limite de criação de contas atingido", "action_bar_reservation_delete": "Remover reserva", "action_bar_account": "Conta", - "action_bar_change_display_name": "Change display name", - "common_copy_to_clipboard": "Copiar para área de transferência", + "action_bar_change_display_name": "Mudar nome de exibição", + "common_copy_to_clipboard": "Copiar para a Área de Transferência", "login_link_signup": "Registrar", "login_title": "Entrar na sua conta ntfy", "login_form_button_submit": "Entrar", @@ -210,13 +210,13 @@ "action_bar_sign_up": "Registrar", "nav_button_account": "Conta", "signup_title": "Criar uma conta ntfy", - "signup_form_username": "Usuário", + "signup_form_username": "Nome de usuário", "signup_form_password": "Senha", "signup_form_confirm_password": "Confirmar senha", - "signup_form_button_submit": "Registrar", + "signup_form_button_submit": "Criar conta", "account_basics_phone_numbers_title": "Telefones", - "signup_form_toggle_password_visibility": "Ativar visibilidade de senha", - "signup_already_have_account": "Já possui uma conta? Entrar!", + "signup_form_toggle_password_visibility": "Alterar visibilidade da senha", + "signup_already_have_account": "Já tem uma conta? Entre!", "nav_upgrade_banner_label": "Atualizar para ntfy Pro", "account_basics_phone_numbers_dialog_description": "Para usar o recurso de notificação de chamada, é necessários adicionar e verificar pelo menos um número de telefone. A verificação pode ser feita por SMS ou chamada telefônica.", "account_basics_phone_numbers_description": "Para notificações de chamada telefônica", @@ -224,7 +224,7 @@ "account_basics_tier_canceled_subscription": "Sua assinatura foi cancelada e será rebaixada para uma conta gratuita em {{date}}.", "account_basics_password_dialog_current_password_incorrect": "Senha incorreta", "account_basics_phone_numbers_dialog_number_label": "Número de telefone", - "account_basics_password_dialog_button_submit": "Alterar senha", + "account_basics_password_dialog_button_submit": "Mudar senha", "reserve_dialog_checkbox_label": "Guardar tópico e configurar acesso", "account_basics_username_title": "Nome de usuário", "account_basics_phone_numbers_dialog_check_verification_button": "Confirmar código", @@ -250,11 +250,11 @@ "account_basics_tier_free": "Grátis", "account_basics_tier_admin": "Administrador", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nenhum número de telefone verificado", - "account_basics_password_description": "Alterar a senha da sua conta", + "account_basics_password_description": "Mudar a senha da sua conta", "publish_dialog_call_label": "Chamada telefônica", "account_usage_calls_title": "Chamadas de telefone feitas", "account_basics_tier_basic": "Básico", - "alert_not_supported_context_description": "Notificações são suportadas apenas através de HTTPS. Esta é uma limitação da API de Notificações.", + "alert_not_supported_context_description": "Notificações são suportadas somente por HTTPS. Essa é uma limitação da Notifications API.", "account_basics_phone_numbers_copied_to_clipboard": "Número de telefone copiado para a área de transferência", "account_basics_tier_title": "Tipo de conta", "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444", @@ -268,14 +268,14 @@ "account_basics_password_dialog_new_password_label": "Nova senha", "display_name_dialog_placeholder": "Nome de exibição", "account_usage_of_limit": "de {{limit}}", - "account_basics_password_dialog_title": "Alterar senha", + "account_basics_password_dialog_title": "Mudar senha", "account_usage_limits_reset_daily": "Os limites de uso são redefinidos diariamente à meia-noite (UTC)", "account_usage_unlimited": "Ilimitado", "account_basics_password_dialog_current_password_label": "Senha atual", "account_usage_reservations_title": "Tópicos reservados", "account_usage_calls_none": "Nenhum telefonema pode ser feito com esta conta", - "display_name_dialog_title": "Alterar o nome de exibição", - "nav_upgrade_banner_description": "Guarde tópicos, mais mensagens & emails e anexos grandes", + "display_name_dialog_title": "Alterar nome de exibição", + "nav_upgrade_banner_description": "Reserve tópicos, mais mensagens e e-mails, e anexos maiores", "publish_dialog_call_reset": "Remover chamada telefônica", "account_basics_phone_numbers_dialog_code_label": "Código de verificação", "account_basics_tier_paid_until": "Assinatura paga até {{date}}, será renovada automaticamente", @@ -302,15 +302,15 @@ "subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas quando o web app não estiver aberto", "account_usage_basis_ip_description": "As estatísticas e limites de uso desta conta são baseados no seu endereço IP, portanto, podem ser compartilhados com outros usuários. Os limites mostrados acima são aproximados com base nos limites de taxa existentes.", "account_usage_cannot_create_portal_session": "Não foi possível abrir o portal de cobrança", - "account_delete_description": "Deletar conta permanentemente", + "account_delete_description": "Deletar sua conta permanentemente", "account_delete_dialog_button_cancel": "Cancelar", "account_delete_dialog_button_submit": "Deletar conta permanentemente", "account_upgrade_dialog_interval_monthly": "Mensal", "account_upgrade_dialog_interval_yearly_discount_save": "desconto de {{discount}}%", "account_upgrade_dialog_interval_yearly_discount_save_up_to": "desconto de até {{discount}}%", "account_upgrade_dialog_cancel_warning": "Isso cancelará sua assinatura e fará downgrade de sua conta em {{date}}. Nessa data, as reservas de tópicos, bem como as mensagens armazenadas em cache no servidor serão excluídas.", - "account_upgrade_dialog_reservations_warning_one": "O nível selecionada permite menos tópicos reservados do que a camada atual. Antes de alterar seu nível, exclua pelo menos uma reserva. Você pode remover reservas nas Configurações", - "account_upgrade_dialog_reservations_warning_other": "O plano selecionado permite menos tópicos reservados do que o seu plano atual. Antes de mudar seu plano, exclua por favor ao menos {{count}} reservas. Você pode remover reservas em Configurações.", + "account_upgrade_dialog_reservations_warning_one": "O nível selecionado permite menos tópicos reservados do que o nível atual. Antes de alterar seu nível, exclua pelo menos uma reserva. Você pode remover reservas nas Configurações.", + "account_upgrade_dialog_reservations_warning_other": "O nível selecionado permite menos tópicos reservados do que o seu nível atual. Antes de mudar seu nível, por favor exclua ao menos {{count}} reservas. Você pode remover reservas nas Configurações.", "account_upgrade_dialog_tier_features_no_reservations": "Sem tópicos reservados", "account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensagen diária", "account_upgrade_dialog_tier_features_emails_one": "{{emails}} email diário", @@ -321,5 +321,88 @@ "account_upgrade_dialog_tier_current_label": "Atual", "account_upgrade_dialog_tier_price_per_month": "mês", "account_upgrade_dialog_button_cancel": "Cancelar", - "account_upgrade_dialog_tier_selected_label": "Selecionado" + "account_upgrade_dialog_tier_selected_label": "Selecionado", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por arquivo", + "account_tokens_table_last_access_header": "Último acesso", + "account_upgrade_dialog_button_cancel_subscription": "Cancelar assinatura", + "account_tokens_table_never_expires": "Nunca expira", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} cobrado anualmente. Salvar {{save}}.", + "account_upgrade_dialog_tier_features_no_calls": "Nenhuma chamada", + "account_tokens_table_token_header": "Token", + "account_upgrade_dialog_button_update_subscription": "Atualizar assinatura", + "account_tokens_table_current_session": "Sessão atual do navegador", + "account_tokens_table_copied_to_clipboard": "Token de acesso copiado", + "account_tokens_title": "Tokens de Acesso", + "account_upgrade_dialog_button_redirect_signup": "Cadastre-se agora", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas diárias", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} chamadas telefônicas diárias", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} armazenamento total", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} por ano. Cobrado mensalmente.", + "account_upgrade_dialog_button_pay_now": "Pague agora para assinar", + "account_tokens_table_expires_header": "Expira", + "prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta.", + "account_tokens_description": "Use tokens de acesso ao publicar e assinar por meio da API ntfy, para que você não precise enviar as credenciais da sua conta. Consulte a documentação para saber mais.", + "account_tokens_table_cannot_delete_or_edit": "Não é possível editar ou excluir o token da sessão atual", + "account_tokens_dialog_title_edit": "Editar token de acesso", + "account_tokens_dialog_title_delete": "Excluir token de acesso", + "prefs_reservations_table_everyone_read_write": "Todos podem publicar e se inscrever", + "prefs_reservations_table_everyone_read_only": "Posso publicar e me inscrever, todos podem se inscrever", + "prefs_reservations_limit_reached": "Você atingiu seu limite de tópicos reservados.", + "prefs_reservations_delete_button": "Redefinir o acesso ao tópico", + "prefs_reservations_edit_button": "Editar acesso ao tópico", + "prefs_reservations_table_everyone_write_only": "Eu posso publicar e me inscrever, todos podem publicar", + "prefs_reservations_table_not_subscribed": "Não inscrito", + "prefs_reservations_table_click_to_subscribe": "Clique para se inscrever", + "reservation_delete_dialog_action_keep_title": "Manter mensagens e anexos em cache", + "account_tokens_table_label_header": "Rótulo", + "account_tokens_table_last_origin_tooltip": "Do endereço IP {{ip}}, clique para pesquisar", + "account_tokens_dialog_title_create": "Criar token de acesso", + "account_tokens_delete_dialog_title": "Excluir token de acesso", + "account_tokens_dialog_label": "Rótulo, por exemplo, notificações de Radarr", + "account_tokens_dialog_expires_never": "O token nunca expira", + "prefs_reservations_dialog_title_edit": "Editar tópico reservado", + "prefs_notifications_web_push_enabled_description": "As notificações são recebidas mesmo quando o aplicativo Web não está em execução (via Web Push)", + "prefs_notifications_web_push_disabled_description": "As notificações são recebidas quando o aplicativo Web está em execução (via WebSocket)", + "account_upgrade_dialog_billing_contact_website": "Para perguntas sobre faturamento, consulte nosso website.", + "account_tokens_table_create_token_button": "Criar token de acesso", + "account_tokens_dialog_button_cancel": "Cancelar", + "account_tokens_dialog_button_update": "Atualizar token", + "prefs_reservations_table": "Tabela de tópicos reservados", + "prefs_reservations_table_everyone_deny_all": "Somente eu posso publicar e me inscrever", + "account_tokens_delete_dialog_description": "Antes de excluir um token de acesso, certifique-se de que nenhum aplicativo ou script o esteja usando ativamente. Esta ação não pode ser desfeita.", + "account_tokens_delete_dialog_submit_button": "Excluir token permanentemente", + "account_tokens_dialog_expires_x_hours": "O token expira em {{hours}} horas", + "account_tokens_dialog_expires_x_days": "O token expira em {{days}} dias", + "prefs_reservations_description": "Você pode reservar nomes de tópicos para uso pessoal aqui. A reserva de um tópico lhe dá propriedade sobre ele e permite que você defina permissões de acesso para outros usuários sobre o tópico.", + "prefs_reservations_dialog_access_label": "Acesso", + "account_upgrade_dialog_billing_contact_email": "Para questões de cobrança, entre em contato conosco diretamente.", + "account_tokens_dialog_button_create": "Criar token", + "account_tokens_dialog_expires_label": "O token de acesso expira em", + "account_tokens_dialog_expires_unchanged": "Deixar a data de validade inalterada", + "prefs_notifications_web_push_title": "Notificações em segundo plano", + "prefs_notifications_web_push_enabled": "Ativado para {{server}}", + "prefs_notifications_web_push_disabled": "Desativado", + "prefs_appearance_theme_title": "Tema", + "prefs_users_table_cannot_delete_or_edit": "Não é possível excluir ou editar o usuário conectado", + "prefs_appearance_theme_system": "Sistema (padrão)", + "prefs_appearance_theme_dark": "Modo escuro", + "prefs_appearance_theme_light": "Modo claro", + "prefs_reservations_title": "Tópicos reservados", + "prefs_reservations_add_button": "Adicionar tópico reservado", + "prefs_reservations_table_topic_header": "Tópico", + "prefs_reservations_table_access_header": "Acesso", + "prefs_reservations_dialog_title_add": "Reservar tópico", + "prefs_reservations_dialog_title_delete": "Excluir reserva de tópico", + "prefs_reservations_dialog_description": "A reserva de um tópico lhe dá propriedade sobre ele e permite definir permissões de acesso para outros usuários sobre o tópico.", + "prefs_reservations_dialog_topic_label": "Tópico", + "reservation_delete_dialog_description": "A remoção de uma reserva abre mão da propriedade sobre o tópico e permite que outros o reservem. Você pode manter ou excluir as mensagens e os anexos existentes.", + "reservation_delete_dialog_action_keep_description": "As mensagens e os anexos armazenados em cache no servidor ficarão visíveis publicamente para as pessoas que souberem o nome do tópico.", + "reservation_delete_dialog_action_delete_title": "Excluir mensagens e anexos armazenados em cache", + "reservation_delete_dialog_action_delete_description": "As mensagens e os anexos armazenados em cache serão excluídos permanentemente. Essa ação não pode ser desfeita.", + "reservation_delete_dialog_submit_button": "Excluir reserva", + "error_boundary_button_reload_ntfy": "Recarregar ntfy", + "web_push_subscription_expiring_title": "As notificações serão pausadas", + "web_push_subscription_expiring_body": "Abra o ntfy para continuar recebendo notificações", + "web_push_unknown_notification_title": "Notificação desconhecida recebida do servidor", + "web_push_unknown_notification_body": "Talvez seja necessário atualizar o ntfy abrindo o aplicativo da Web" } diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index 67b92e1d..df000eca 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -27,8 +27,8 @@ "alert_notification_permission_required_title": "Notificările sunt dezactivate", "alert_notification_permission_required_button": "Permite acum", "alert_not_supported_title": "Notificările nu sunt acceptate", - "alert_not_supported_description": "Notificările nu sunt acceptate în browser.", - "alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări.", + "alert_not_supported_description": "Notificările nu sunt acceptate în browserul tău", + "alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări", "notifications_list": "Lista de notificări", "notifications_list_item": "Notificare", "notifications_mark_read": "Marchează ca citit", @@ -102,9 +102,9 @@ "publish_dialog_emoji_picker_show": "Alege un emoji", "notifications_loading": "Încărcare notificări…", "publish_dialog_priority_low": "Prioritate joasă", - "signup_form_username": "Nume de utilizator", - "signup_form_button_submit": "Înscrie-te", - "common_copy_to_clipboard": "Copiază în clipboard", + "signup_form_username": "Utilizator", + "signup_form_button_submit": "Înregistrare", + "common_copy_to_clipboard": "Copiază", "signup_form_toggle_password_visibility": "Schimbă vizibilitatea parolei", "signup_title": "Crează un cont ntfy", "signup_already_have_account": "Deja ai un cont? Autentifică-te!", @@ -123,5 +123,110 @@ "message_bar_show_dialog": "Arată dialogul de publicare", "signup_error_username_taken": "Numele de utilizator {{username}} este deja folosit", "login_title": "Autentifică-te în contul ntfy", - "action_bar_reservation_add": "Rezervă topicul" + "action_bar_reservation_add": "Rezervă topicul", + "action_bar_mute_notifications": "Oprește notificările", + "action_bar_unmute_notifications": "Pornește notificările", + "nav_topics_title": "Subiecte abonate", + "publish_dialog_chip_attach_url_label": "Atașează fișier prin URL", + "publish_dialog_call_label": "Apel telefonic", + "publish_dialog_button_cancel_sending": "Anulează trimiterea", + "subscribe_dialog_subscribe_title": "Abonează-te la subiect", + "subscribe_dialog_login_password_label": "Parolă", + "subscribe_dialog_login_button_login": "Autentificare", + "subscribe_dialog_error_user_not_authorized": "Utilizatorul {{username}} nu este autorizat", + "account_basics_title": "Cont", + "account_basics_username_title": "Nume de utilizator", + "account_basics_username_description": "Hei, ești tu ❤", + "subscribe_dialog_error_topic_already_reserved": "Subiectul este deja rezervat", + "publish_dialog_attached_file_title": "Fișier atașat:", + "publish_dialog_attached_file_filename_placeholder": "Nume fișier atașat", + "publish_dialog_attached_file_remove": "Elimină fișierul atașat", + "emoji_picker_search_placeholder": "Caută emoji", + "nav_button_muted": "Notificări dezactivate", + "alert_notification_permission_denied_title": "Notificările sunt blocate", + "alert_notification_ios_install_required_description": "Apasă pe butonul Partajare și Adăugați la ecranul principal pentru a porni notificările pe iOS", + "alert_notification_ios_install_required_title": "Instalare iOS necesară", + "alert_notification_permission_denied_description": "Repornește-le în browserul tău", + "alert_not_supported_context_description": "Notificările sunt acceptate doar prin HTTPS. Aceasta este o limitare a API-ului de notificări.", + "notifications_actions_failed_notification": "Acțiune nereușită", + "publish_dialog_email_placeholder": "Adresă către care se va redirecționa notificarea, ex. phil@example.com", + "publish_dialog_email_reset": "Șterge redirecționare email", + "publish_dialog_call_item": "Apelează numărul de telefon {{number}}", + "publish_dialog_attach_label": "URL atașament", + "publish_dialog_attach_placeholder": "Atașează fișier prin URL, ex. https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "Șterge atașament URL", + "publish_dialog_filename_label": "Nume fișier", + "publish_dialog_filename_placeholder": "Nume fișier atașament", + "publish_dialog_delay_label": "Întârziere", + "publish_dialog_call_reset": "Șterge apel telefonic", + "publish_dialog_delay_placeholder": "Întârzie livrarea, ex. {{unixTimestamp}}, {{relativeTime}}, sau \"{{naturalLanguage}}\" (doar engleză)", + "publish_dialog_delay_reset": "Șterge livrare întârziată", + "publish_dialog_other_features": "Alte funcționalități:", + "publish_dialog_chip_click_label": "Accesează URL-ul", + "publish_dialog_chip_email_label": "Redirecționează către email", + "publish_dialog_chip_call_label": "Apel telefonic", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nu există numere de telefon verificate", + "publish_dialog_chip_attach_file_label": "Atașează fișier local", + "publish_dialog_chip_delay_label": "Întârziere livrare", + "publish_dialog_chip_topic_label": "Schimbă subiectul", + "publish_dialog_details_examples_description": "Pentru exemple și o descriere detaliată a tuturor funcțiilor de trimitere, vă rugăm să consultați documentația.", + "publish_dialog_button_cancel": "Anulează", + "publish_dialog_button_send": "Trimite", + "publish_dialog_checkbox_markdown": "Formatează ca Markdown", + "publish_dialog_checkbox_publish_another": "Publică altul", + "publish_dialog_drop_file_here": "Trage fișierul aici", + "emoji_picker_search_clear": "Șterge căutarea", + "subscribe_dialog_subscribe_description": "Subiectele nu pot fi protejate prin parolă, așa că alege un nume care să nu fie ușor de ghicit. Odată abonat, poți utiliza metodele PUT/POST pentru a trimite notificări.", + "subscribe_dialog_subscribe_topic_placeholder": "Nume subiect, de exemplu, phil_alerts", + "subscribe_dialog_subscribe_use_another_label": "Foloseșste alt server", + "subscribe_dialog_subscribe_use_another_background_info": "Notificările de la alte servere nu vor fi primite atunci când aplicația web nu este deschisă", + "subscribe_dialog_subscribe_base_url_label": "URL serviciu", + "subscribe_dialog_subscribe_button_generate_topic_name": "Generează nume", + "subscribe_dialog_subscribe_button_cancel": "Anulează", + "subscribe_dialog_subscribe_button_subscribe": "Abonează-te", + "subscribe_dialog_login_title": "Autentificare necesară", + "subscribe_dialog_login_description": "Acest subiect este protejat prin parolă. Vă rugăm să introduceți numele de utilizator și parola pentru a vă abona.", + "subscribe_dialog_login_username_label": "Nume de utilizator, de exemplu, phil", + "subscribe_dialog_error_user_anonymous": "anonim", + "account_basics_tier_interval_monthly": "lunar", + "account_basics_password_dialog_title": "Schimbă parola", + "account_basics_password_dialog_current_password_label": "Parola actuală", + "account_basics_phone_numbers_copied_to_clipboard": "Numărul de telefon a fost copiat", + "account_basics_username_admin_tooltip": "Sunteți administrator", + "account_basics_tier_paid_until": "Abonamentul este plătit până la {{date}}, și se va reînnoi automat", + "account_basics_tier_payment_overdue": "Plata dvs. este restantă. Actualizați metoda de plată sau contul dvs. va fi retrogradat în curând.", + "account_basics_tier_interval_yearly": "anual", + "account_basics_tier_upgrade_button": "Upgrade la Pro", + "account_basics_phone_numbers_title": "Numere de telefon", + "account_basics_password_description": "Schimbă parola contului", + "account_basics_password_dialog_confirm_password_label": "Confirmă parola", + "account_basics_password_dialog_button_submit": "Schimbă parola", + "account_basics_password_dialog_current_password_incorrect": "Parola este incorectă", + "account_basics_phone_numbers_dialog_description": "Pentru a folosi funcția de notificare prin apel, trebuie să adăugați și să verificați cel puțin un număr de telefon. Verificare poate fi făcută prin SMS sau apel vocal.", + "account_basics_phone_numbers_description": "Pentru notificări prin apel", + "account_basics_phone_numbers_dialog_verify_button_sms": "Trimite SMS", + "account_basics_phone_numbers_no_phone_numbers_yet": "Încă nu există numere de telefon", + "account_basics_phone_numbers_dialog_title": "Adaugă număr de telefon", + "account_basics_phone_numbers_dialog_number_label": "Număr de telefon", + "account_basics_phone_numbers_dialog_number_placeholder": "e.x. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_call": "Sună-mă", + "account_basics_phone_numbers_dialog_code_label": "Cod de verificare", + "account_basics_phone_numbers_dialog_code_placeholder": "e.x. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Confirmă codul", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Apel", + "account_usage_title": "Utilizare", + "account_usage_unlimited": "Nelimitat", + "account_usage_limits_reset_daily": "Limitele de utilizare sunt resetate zilnic la miezul nopții (UTC)", + "account_basics_tier_title": "Tip de cont", + "account_usage_of_limit": "din {{limit}}", + "account_basics_tier_admin": "Administrator", + "account_basics_tier_admin_suffix_with_tier": "(cu nivelul {{tier}})", + "account_basics_tier_admin_suffix_no_tier": "(niciun nivel)", + "account_basics_tier_basic": "De bază", + "account_basics_tier_change_button": "Schimbă", + "account_basics_password_dialog_new_password_label": "Parola nouă", + "account_basics_password_title": "Parolă", + "account_basics_tier_description": "Nivelul de putere al contului", + "account_basics_tier_free": "Gratuit" } diff --git a/web/public/static/langs/ru.json b/web/public/static/langs/ru.json index a1f26d70..0c2eaae3 100644 --- a/web/public/static/langs/ru.json +++ b/web/public/static/langs/ru.json @@ -67,7 +67,7 @@ "subscribe_dialog_subscribe_title": "Подписаться на тему", "publish_dialog_button_cancel": "Отмена", "subscribe_dialog_subscribe_description": "Темы могут быть не защищены паролем, поэтому укажите сложное имя. После подписки Вы сможете отправлять уведомления используя PUT/POST-запросы.", - "prefs_users_description": "Добавляйте/удаляйте пользователей для защищенных тем. Обратите внимание, что имя пользователя и пароль хранятся в локальном хранилище браузера.", + "prefs_users_description": "Вы можете управлять пользователями для защищённых тем. Учтите, что имя учётные данные хранятся в локальном хранилище браузера.", "error_boundary_description": "Это не должно было случиться. Нам очень жаль.
Если Вы можете уделить минуту своего времени, пожалуйста сообщите об этом на GitHub, или дайте нам знать через Discord или Matrix.", "publish_dialog_email_placeholder": "Адрес для пересылки уведомления. Например, phil@example.com", "publish_dialog_attach_placeholder": "Прикрепите файл по URL. Например, https://f-droid.org/F-Droid.apk", @@ -96,36 +96,36 @@ "subscribe_dialog_subscribe_button_subscribe": "Подписаться", "subscribe_dialog_login_title": "Требуется авторизация", "subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.", - "subscribe_dialog_login_username_label": "Имя пользователя. Например, phil", + "subscribe_dialog_login_username_label": "Имя пользователя. Например, oleg", "subscribe_dialog_login_password_label": "Пароль", "common_back": "Назад", "subscribe_dialog_login_button_login": "Войти", "subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован", "subscribe_dialog_error_user_anonymous": "анонимный пользователь", "prefs_notifications_title": "Уведомления", - "prefs_notifications_sound_title": "Звук уведомления", - "prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении", + "prefs_notifications_sound_title": "Звук уведомлений", + "prefs_notifications_sound_description_none": "При получении уведомлений не звуки не проигрываются", "prefs_notifications_sound_no_sound": "Без звука", "prefs_notifications_min_priority_title": "Минимальный приоритет", - "prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от приоритета", + "prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от их приоритета", "prefs_notifications_min_priority_description_x_or_higher": "Показывать уведомления, если приоритет {{number}} ({{name}}) или выше", "prefs_notifications_min_priority_description_max": "Показывать уведомления, если приоритет равен 5 (максимальный)", "prefs_notifications_min_priority_any": "Любой приоритет", "prefs_notifications_min_priority_low_and_higher": "Низкий приоритет и выше", "prefs_notifications_min_priority_max_only": "Только максимальный приоритет", - "prefs_notifications_delete_after_title": "Удалить уведомления", + "prefs_notifications_delete_after_title": "Удаление уведомлений", "prefs_notifications_delete_after_never": "Никогда", "prefs_notifications_delete_after_three_hours": "Через три часа", - "prefs_notifications_sound_description_some": "Уведомления воспроизводят звук {{sound}}", + "prefs_notifications_sound_description_some": "При уведомлениях проигрывается звук {{sound}}", "prefs_notifications_min_priority_default_and_higher": "Стандартный приоритет и выше", "prefs_notifications_delete_after_one_day": "Через день", "prefs_notifications_delete_after_one_week": "Через неделю", "prefs_notifications_delete_after_one_month": "Через месяц", "prefs_notifications_delete_after_never_description": "Уведомления никогда не удаляются автоматически", - "prefs_notifications_delete_after_three_hours_description": "Уведомления автоматически удаляются через три часа", - "prefs_notifications_delete_after_one_day_description": "Уведомления автоматически удаляются через один день", - "prefs_notifications_delete_after_one_week_description": "Уведомления автоматически удаляются через неделю", - "prefs_notifications_delete_after_one_month_description": "Уведомления автоматически удаляются через месяц", + "prefs_notifications_delete_after_three_hours_description": "Уведомления удаляются автоматически через три часа", + "prefs_notifications_delete_after_one_day_description": "Уведомления удаляются автоматически через один день", + "prefs_notifications_delete_after_one_week_description": "Уведомления удаляются автоматически через неделю", + "prefs_notifications_delete_after_one_month_description": "Уведомления удаляются автоматически через месяц", "prefs_users_title": "Управление пользователями", "prefs_users_add_button": "Добавить пользователя", "prefs_users_table_user_header": "Пользователь", @@ -133,7 +133,7 @@ "prefs_users_dialog_title_add": "Добавить пользователя", "prefs_users_dialog_title_edit": "Редактировать пользователя", "prefs_users_dialog_base_url_label": "URL-адрес сервера. Например, https://ntfy.sh", - "prefs_users_dialog_username_label": "Имя пользователя. Например, phil", + "prefs_users_dialog_username_label": "Имя пользователя. Например, oleg", "prefs_users_dialog_password_label": "Пароль", "common_cancel": "Отмена", "common_add": "Добавить", @@ -157,11 +157,11 @@ "emoji_picker_search_clear": "Сбросить поиск", "account_upgrade_dialog_cancel_warning": "Это действие отменит Вашу подписку и переведет Вашую учетную запись на бесплатное обслуживание {{date}}. При наступлении этой даты, все резервирования и сообщения в кэше будут удалены.", "account_tokens_table_create_token_button": "Создать токен доступа", - "account_tokens_table_last_origin_tooltip": "с IP-адреса {{ip}}, нажмите для подробностей", + "account_tokens_table_last_origin_tooltip": "С IP-адреса {{ip}}, нажмите для подробностей", "account_tokens_dialog_title_edit": "Изменить токен доступа", "account_delete_dialog_button_cancel": "Отмена", "account_delete_dialog_billing_warning": "Удаление учетной записи также отменяет все платные подписки. У Вас не будет доступа к порталу оплаты.", - "account_delete_dialog_description": "Это действие безвозвратно удалит Вашу учетную запись, включая все Ваши данные хранящиеся на сервере. После удаления, Ваше имя пользователя не будет доступно для регистрации в течении 7 дней. Если Вы действительно хотите продолжить, пожалуйста введите Ваш пароль ниже.", + "account_delete_dialog_description": "Это действие безвозвратно удалит вашу учётную запись, включая все данные, хранящиеся на сервере. После удаления имя пользователя вашей учётной записи не будет доступно для регистрации в течение 7 дней. Если вы точно хотите продолжить, пожалуйста, введите свой пароль ниже.", "account_delete_dialog_label": "Пароль", "reservation_delete_dialog_action_keep_description": "Сообщения и вложения которые находятся в кэше сервера станут доступны всем, кто знает имя темы.", "prefs_reservations_table": "Список зарезервированных тем", @@ -173,7 +173,7 @@ "prefs_reservations_table_not_subscribed": "Не подписан", "prefs_reservations_table_everyone_deny_all": "Только я могу публиковать и подписываться", "prefs_reservations_table_everyone_read_write": "Все могут публиковать и подписываться", - "prefs_reservations_table_click_to_subscribe": "Нажмите чтобы подписаться", + "prefs_reservations_table_click_to_subscribe": "Нажмите, чтобы подписаться", "prefs_reservations_dialog_title_add": "Зарезервировать тему", "prefs_reservations_dialog_title_delete": "Удалить резервирование", "prefs_reservations_dialog_title_edit": "Изменение резервированной темы", @@ -202,7 +202,7 @@ "account_tokens_dialog_expires_never": "Токен никогда не истекает", "prefs_notifications_sound_play": "Воспроизводить выбранный звук", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервированных тем", - "account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. сообщений в день", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. писем в день", "account_basics_tier_free": "Бесплатный", "account_tokens_dialog_title_create": "Создать токен доступа", "account_tokens_dialog_title_delete": "Удалить токен доступа", @@ -215,11 +215,11 @@ "account_upgrade_dialog_tier_current_label": "Текущая", "account_upgrade_dialog_button_cancel": "Отмена", "prefs_users_edit_button": "Редактировать пользователя", - "account_basics_tier_upgrade_button": "Подписаться на Pro", + "account_basics_tier_upgrade_button": "Обновить до Pro", "account_basics_tier_paid_until": "Подписка оплачена до {{date}} и будет продляться автоматически", "account_basics_tier_change_button": "Изменить", - "account_delete_dialog_button_submit": "Безвозвратно удалить учетную запись", - "account_upgrade_dialog_title": "Изменить уровень учетной записи", + "account_delete_dialog_button_submit": "Безвозвратно удалить эту учётную запись", + "account_upgrade_dialog_title": "Изменить уровень учётной записи", "account_usage_basis_ip_description": "Статистика и ограничения на использование учитываются по IP-адресу, поэтому они могут совмещаться с другими пользователями. Уровни, указанные выше, примерно соответствуют текущим ограничениям.", "publish_dialog_topic_reset": "Сбросить тему", "account_basics_tier_admin_suffix_no_tier": "(без подписки)", @@ -231,9 +231,9 @@ "signup_form_toggle_password_visibility": "Показать/скрыть пароль", "signup_disabled": "Регистрация недоступна", "signup_error_username_taken": "Имя пользователя {{username}} уже занято", - "signup_title": "Создать учетную запись ntfy", - "signup_already_have_account": "Уже есть учетная запись? Войдите!", - "signup_error_creation_limit_reached": "Лимит на создание учетных записей исчерпан", + "signup_title": "Создать учётную запись ntfy", + "signup_already_have_account": "Уже есть учётная запись? Войдите!", + "signup_error_creation_limit_reached": "Исчерпано ограничение создания учётных записей", "login_form_button_submit": "Вход", "login_link_signup": "Регистрация", "login_disabled": "Вход недоступен", @@ -249,12 +249,12 @@ "message_bar_publish": "Опубликовать сообщение", "nav_button_muted": "Уведомления заглушены", "nav_button_connecting": "установка соединения", - "action_bar_account": "Учетная запись", - "login_title": "Вход в Вашу учетную запись ntfy", + "action_bar_account": "Учётная запись", + "login_title": "Войдите в учётную запись ntfy", "action_bar_reservation_limit_reached": "Лимит исчерпан", "action_bar_toggle_mute": "Заглушить/разрешить уведомления", - "nav_button_account": "Учетная запись", - "nav_upgrade_banner_label": "Купить подписку ntfy Pro", + "nav_button_account": "Учётная запись", + "nav_upgrade_banner_label": "Подписка ntfy Pro", "message_bar_show_dialog": "Открыть диалог публикации", "notifications_list": "Список уведомлений", "notifications_list_item": "Уведомление", @@ -279,12 +279,12 @@ "subscribe_dialog_subscribe_base_url_label": "URL-адрес сервера", "subscribe_dialog_subscribe_button_generate_topic_name": "Сгенерировать случайное имя", "subscribe_dialog_error_topic_already_reserved": "Тема уже зарезервирована", - "account_basics_title": "Учетная запись", + "account_basics_title": "Учётная запись", "account_basics_username_title": "Имя пользователя", - "account_basics_username_admin_tooltip": "Вы Администратор", + "account_basics_username_admin_tooltip": "Вы администратор", "account_basics_password_title": "Пароль", - "account_basics_username_description": "Это Вы! :)", - "account_basics_password_description": "Смена пароля учетной записи", + "account_basics_username_description": "Это вы! :)", + "account_basics_password_description": "Смена пароля учётной записи", "account_basics_password_dialog_title": "Смена пароля", "account_basics_password_dialog_current_password_label": "Текущий пароль", "account_basics_password_dialog_current_password_incorrect": "Введен неверный пароль", @@ -292,11 +292,11 @@ "account_usage_of_limit": "из {{limit}}", "account_usage_unlimited": "Неограниченно", "account_usage_limits_reset_daily": "Ограничения сбрасываются ежедневно в полночь (UTC)", - "account_basics_tier_description": "Уровень Вашей учетной записи", + "account_basics_tier_description": "Уровень вашей учётной записи", "account_basics_tier_admin": "Администратор", - "account_basics_tier_admin_suffix_with_tier": "(с {{tier}} подпиской)", - "account_basics_tier_payment_overdue": "У Вас задолженность по оплате. Пожалуйста проверьте метод оплаты, иначе Вы скоро потеряете преимущества Вашей подписки.", - "account_basics_tier_canceled_subscription": "Ваша подписка была отменена; учетная запись перейдет на бесплатное обслуживание {{date}}.", + "account_basics_tier_admin_suffix_with_tier": "(с подпиской {{tier}})", + "account_basics_tier_payment_overdue": "У вас имеется задолженность по оплате. Пожалуйста, проверьте метод оплаты, иначе скоро вы утратите преимущества подписки.", + "account_basics_tier_canceled_subscription": "Ваша подписка была отменена. Учётная запись перейдет на бесплатное обслуживание {{date}}.", "account_basics_tier_manage_billing_button": "Управление оплатой", "account_usage_messages_title": "Опубликованные сообщения", "account_usage_emails_title": "Отправленные электронные сообщения", @@ -305,8 +305,8 @@ "account_usage_attachment_storage_title": "Хранение вложений", "account_usage_attachment_storage_description": "{{filesize}} за файл, удаляются спустя {{expiry}}", "account_usage_cannot_create_portal_session": "Невозможно открыть портал оплаты", - "account_delete_title": "Удалить учетную запись", - "account_delete_description": "Безвозвратно удалить Вашу учетную запись", + "account_delete_title": "Удаление учётной записи", + "account_delete_description": "Безвозвратное удаление этой учётной записи", "account_upgrade_dialog_button_redirect_signup": "Зарегистрироваться", "account_upgrade_dialog_button_pay_now": "Оплатить и подписаться", "account_upgrade_dialog_button_cancel_subscription": "Отменить подписку", @@ -319,8 +319,8 @@ "account_tokens_table_expires_header": "Истекает", "account_tokens_dialog_label": "Название, например Radarr notifications", "prefs_reservations_title": "Зарезервированные темы", - "prefs_reservations_description": "Здесь Вы можете резервировать темы для личного пользования. Резервирование дает Вам возможность управлять темой и настраивать правила доступа к ней для пользователей.", - "prefs_reservations_limit_reached": "Вы исчерпали Ваш лимит на количество зарезервированных тем.", + "prefs_reservations_description": "Здесь вы можете резервировать темы для личного пользования. Резервирование дает возможность управления темой и настройки правил доступа к ней для других пользователей.", + "prefs_reservations_limit_reached": "Лимит количества зарезервированных тем исчерпан.", "prefs_reservations_add_button": "Добавить тему", "prefs_reservations_edit_button": "Настройка доступа", "prefs_reservations_delete_button": "Сбросить правила доступа", @@ -339,7 +339,7 @@ "account_basics_password_dialog_new_password_label": "Новый пароль", "account_basics_password_dialog_confirm_password_label": "Подтвердите пароль", "account_basics_password_dialog_button_submit": "Сменить пароль", - "account_basics_tier_title": "Тип учетной записи", + "account_basics_tier_title": "Тип учётной записи", "error_boundary_unsupported_indexeddb_description": "Веб-приложение ntfy использует IndexedDB, который не поддерживается Вашим браузером в приватном режиме.

Хотя это и не лучший вариант, использовать веб-приложение ntfy в приватном режиме не имеет особого смысла, так как все данные храняться в локальном хранилище браузера. Вы можете узнать больше в этом отчете на GitHub или связавшись с нами через Discord или Matrix.", "account_basics_tier_interval_monthly": "ежемесячно", "account_basics_tier_interval_yearly": "ежегодно", @@ -356,11 +356,11 @@ "publish_dialog_call_reset": "Удалить вызов", "account_basics_phone_numbers_dialog_description": "Для того что бы использовать возможность уведомлений о вызовах, нужно добавить и проверить хотя бы один номер телефона. Проверить можно используя SMS или звонок.", "account_basics_phone_numbers_dialog_title": "Добавить номер телефона", - "account_basics_phone_numbers_dialog_number_placeholder": "например +1222333444", - "account_basics_phone_numbers_dialog_code_placeholder": "например 123456", + "account_basics_phone_numbers_dialog_number_placeholder": "например, +72223334444", + "account_basics_phone_numbers_dialog_code_placeholder": "например, 123456", "account_basics_phone_numbers_dialog_verify_button_sms": "Отправить SMS", "account_usage_calls_title": "Совершённые вызовы", - "account_usage_calls_none": "Невозможно совершать вызовы с этим аккаунтом", + "account_usage_calls_none": "Невозможно совершать вызовы с этой учётной записью", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Нет проверенных номеров", "account_basics_phone_numbers_copied_to_clipboard": "Номер телефона скопирован в буфер обмена", "account_upgrade_dialog_tier_features_no_calls": "Нет вызовов", @@ -371,8 +371,8 @@ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервированная тема", "account_basics_phone_numbers_no_phone_numbers_yet": "Телефонных номеров пока нет", "publish_dialog_chip_call_label": "Звонок", - "account_upgrade_dialog_tier_features_emails_one": "{{emails}} ежедневное письмо", - "account_upgrade_dialog_tier_features_messages_one": "{{messages}} ежедневное сообщения", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} эл. письмо в день", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} сообщение в день", "account_basics_phone_numbers_description": "Для уведомлений о телефонных звонках", "publish_dialog_call_label": "Звонок", "account_basics_phone_numbers_dialog_channel_call": "Позвонить", @@ -383,8 +383,8 @@ "account_basics_phone_numbers_dialog_channel_sms": "SMS", "action_bar_mute_notifications": "Заглушить уведомления", "action_bar_unmute_notifications": "Разрешить уведомления", - "alert_notification_permission_denied_title": "Уведомления заблокированы", - "alert_notification_permission_denied_description": "Пожалуйста, разрешите их в своём браузере", + "alert_notification_permission_denied_title": "Уведомления не разрешены", + "alert_notification_permission_denied_description": "Пожалуйста, разрешите отправку уведомлений браузере", "alert_notification_ios_install_required_title": "iOS требует установку", "alert_notification_ios_install_required_description": "Нажмите на значок \"Поделиться\" и \"Добавить на главный экран\", чтобы включить уведомления на iOS", "error_boundary_button_reload_ntfy": "Перезагрузить ntfy", @@ -401,7 +401,7 @@ "notifications_actions_failed_notification": "Неудачное действие", "publish_dialog_checkbox_markdown": "Форматировать как Markdown", "subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто", - "prefs_appearance_theme_system": "Системный (по умолчанию)", - "prefs_appearance_theme_dark": "Ночной режим", - "prefs_appearance_theme_light": "Дневной режим" + "prefs_appearance_theme_system": "Как в системе (по умолчанию)", + "prefs_appearance_theme_dark": "Тёмная", + "prefs_appearance_theme_light": "Светлая" } diff --git a/web/public/static/langs/sq.json b/web/public/static/langs/sq.json new file mode 100644 index 00000000..c146c77d --- /dev/null +++ b/web/public/static/langs/sq.json @@ -0,0 +1,63 @@ +{ + "common_back": "Prapa", + "signup_form_username": "Emri i përdoruesit", + "signup_title": "Krijo një llogari \"ntfy\"", + "signup_form_toggle_password_visibility": "Ndrysho dukshmërinë e fjalëkalimit", + "common_save": "Ruaj", + "signup_form_confirm_password": "Konfirmo Fjalëkalimin", + "common_copy_to_clipboard": "Kopjo", + "signup_form_button_submit": "Regjistrohu", + "signup_already_have_account": "Keni tashmë llogari? Identifikohu!", + "signup_disabled": "Regjistrimi është i çaktivizuar", + "signup_error_username_taken": "Emri i përdoruesit {{username}} është marrë tashmë", + "signup_error_creation_limit_reached": "U arrit kufiri i krijimit të llogarisë", + "login_title": "Hyni në llogarinë tuaj ntfy", + "login_form_button_submit": "Identifikohu", + "login_disabled": "Identifikimi është i çaktivizuar", + "action_bar_show_menu": "Shfaq menunë", + "action_bar_settings": "Parametrat", + "action_bar_account": "Llogaria", + "action_bar_change_display_name": "Ndrysho emrin e shfaqur", + "action_bar_reservation_add": "Rezervo temën", + "action_bar_reservation_edit": "Ndrysho rezervimin", + "action_bar_reservation_delete": "Hiq rezervimin", + "action_bar_reservation_limit_reached": "U arrit kufiri", + "action_bar_send_test_notification": "Dërgo njoftim testues", + "action_bar_clear_notifications": "Pastro të gjitha njoftimet", + "action_bar_mute_notifications": "Heshti njoftimet", + "action_bar_unmute_notifications": "Lejo njoftimet", + "action_bar_unsubscribe": "Ç'abonohu", + "action_bar_toggle_mute": "Hesht/lejo njoftimet", + "action_bar_toggle_action_menu": "Hap/mbyll menynë e veprimit", + "action_bar_profile_title": "Profili", + "action_bar_profile_settings": "Parametrat", + "action_bar_profile_logout": "Dil", + "action_bar_sign_in": "Identifikohu", + "action_bar_sign_up": "Regjistrohu", + "message_bar_type_message": "Shkruaj një mesazh këtu", + "common_cancel": "Anullo", + "signup_form_password": "Fjalëkalimi", + "common_add": "Shto", + "login_link_signup": "Regjistrohu", + "action_bar_logo_alt": "logo e ntfy", + "message_bar_error_publishing": "Gabim duke postuar njoftimin", + "message_bar_show_dialog": "Trego dialogun e publikimit", + "message_bar_publish": "Publiko mesazhin", + "nav_topics_title": "Temat e abonuara", + "nav_button_all_notifications": "Të gjitha njoftimet", + "nav_button_account": "Llogaria", + "nav_button_settings": "Cilësimet", + "nav_button_publish_message": "Publiko njoftimin", + "nav_button_subscribe": "Abunohu tek tema", + "nav_button_connecting": "duke u lidhur", + "nav_upgrade_banner_label": "Përmirëso në ntfy Pro", + "nav_upgrade_banner_description": "Rezervoni tema, më shumë mesazhe dhe email-e, si dhe bashkëngjitje më të mëdha", + "nav_button_muted": "Njoftimet janë të fikura", + "alert_notification_permission_required_title": "Njoftimet janë të çaktivizuar", + "alert_notification_permission_required_description": "Jepni leje Browser-it tuaj për të shfaqur njoftimet në desktop", + "alert_notification_permission_denied_title": "Njoftimet janë të bllokuara", + "alert_notification_ios_install_required_title": "Instalimi i iOS-it detyrohet", + "alert_notification_permission_denied_description": "Ju lutemi riaktivizoni ato në Browser-in tuaj", + "nav_button_documentation": "Dokumentacion", + "alert_notification_permission_required_button": "Lejo tani" +} diff --git a/web/public/static/langs/ta.json b/web/public/static/langs/ta.json new file mode 100644 index 00000000..11635c91 --- /dev/null +++ b/web/public/static/langs/ta.json @@ -0,0 +1,407 @@ +{ + "action_bar_account": "கணக்கு", + "action_bar_change_display_name": "காட்சி பெயரை மாற்றவும்", + "action_bar_show_menu": "மெனுவைக் காட்டு", + "action_bar_logo_alt": "ntfy லோகோ", + "action_bar_settings": "அமைப்புகள்", + "action_bar_reservation_add": "இருப்பு தலைப்பு", + "message_bar_publish": "செய்தியை வெளியிடுங்கள்", + "nav_topics_title": "சந்தா தலைப்புகள்", + "nav_button_all_notifications": "அனைத்து அறிவிப்புகளும்", + "nav_button_account": "கணக்கு", + "nav_button_settings": "அமைப்புகள்", + "nav_button_documentation": "ஆவணப்படுத்துதல்", + "nav_button_publish_message": "அறிவிப்பை வெளியிடுங்கள்", + "alert_not_supported_description": "உங்கள் உலாவியில் அறிவிப்புகள் ஆதரிக்கப்படவில்லை", + "alert_not_supported_context_description": "அறிவிப்புகள் HTTP களில் மட்டுமே ஆதரிக்கப்படுகின்றன. இது அறிவிப்புகள் பநிஇ இன் வரம்பு.", + "notifications_list": "அறிவிப்புகள் பட்டியல்", + "notifications_delete": "நீக்கு", + "notifications_copied_to_clipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", + "notifications_list_item": "அறிவிப்பு", + "notifications_mark_read": "படித்தபடி குறி", + "notifications_tags": "குறிச்சொற்கள்", + "notifications_priority_x": "முன்னுரிமை {{priority}}", + "notifications_actions_not_supported": "வலை பயன்பாட்டில் நடவடிக்கை ஆதரிக்கப்படவில்லை", + "notifications_none_for_topic_title": "இந்த தலைப்புக்கு நீங்கள் இதுவரை எந்த அறிவிப்புகளையும் பெறவில்லை.", + "notifications_actions_http_request_title": "Http {{method}} {{url}} க்கு அனுப்பவும்", + "notifications_actions_failed_notification": "தோல்வியுற்ற செயல்", + "notifications_none_for_topic_description": "இந்த தலைப்புக்கு அறிவிப்புகளை அனுப்ப, தலைப்பு முகவரி க்கு வைக்கவும் அல்லது இடுகையிடவும்.", + "notifications_loading": "அறிவிப்புகளை ஏற்றுகிறது…", + "publish_dialog_title_topic": "{{topic}} க்கு வெளியிடுங்கள்", + "publish_dialog_title_no_topic": "அறிவிப்பை வெளியிடுங்கள்", + "publish_dialog_progress_uploading": "பதிவேற்றுதல்…", + "publish_dialog_message_published": "அறிவிப்பு வெளியிடப்பட்டது", + "publish_dialog_attachment_limits_file_and_quota_reached": "{{fileSizeLimit}} கோப்பு வரம்பு மற்றும் ஒதுக்கீடு, {{remainingBytes}} மீதமுள்ளது", + "publish_dialog_attachment_limits_file_reached": "{{fileSizeLimit}} கோப்பு வரம்பை மீறுகிறது", + "publish_dialog_attachment_limits_quota_reached": "ஒதுக்கீட்டை மீறுகிறது, {{remainingBytes}} மீதமுள்ளவை", + "publish_dialog_progress_uploading_detail": "பதிவேற்றுவது {{loaded}}/{{{total}} ({{percent}}%)…", + "publish_dialog_priority_min": "மணித்துளி. முன்னுரிமை", + "publish_dialog_emoji_picker_show": "ஈமோசியைத் தேர்ந்தெடுங்கள்", + "publish_dialog_priority_low": "குறைந்த முன்னுரிமை", + "publish_dialog_priority_default": "இயல்புநிலை முன்னுரிமை", + "publish_dialog_priority_high": "அதிக முன்னுரிமை", + "publish_dialog_priority_max": "அதிகபட்சம். முன்னுரிமை", + "publish_dialog_base_url_label": "பணி முகவரி", + "publish_dialog_base_url_placeholder": "பணி முகவரி, எ.கா. https://example.com", + "publish_dialog_topic_label": "தலைப்பு பெயர்", + "publish_dialog_topic_placeholder": "தலைப்பு பெயர், எ.கா. phil_alerts", + "publish_dialog_topic_reset": "தலைப்பை மீட்டமைக்கவும்", + "publish_dialog_title_label": "தலைப்பு", + "publish_dialog_title_placeholder": "அறிவிப்பு தலைப்பு, எ.கா. வட்டு விண்வெளி எச்சரிக்கை", + "publish_dialog_message_label": "செய்தி", + "publish_dialog_message_placeholder": "இங்கே ஒரு செய்தியைத் தட்டச்சு செய்க", + "publish_dialog_tags_label": "குறிச்சொற்கள்", + "publish_dialog_tags_placeholder": "குறிச்சொற்களின் கமாவால் பிரிக்கப்பட்ட பட்டியல், எ.கா. எச்சரிக்கை, SRV1-Backup", + "publish_dialog_priority_label": "முன்னுரிமை", + "publish_dialog_click_label": "முகவரி ஐக் சொடுக்கு செய்க", + "publish_dialog_click_placeholder": "அறிவிப்பைக் சொடுக்கு செய்யும் போது திறக்கப்படும் முகவரி", + "publish_dialog_click_reset": "சொடுக்கு முகவரி ஐ அகற்று", + "publish_dialog_email_label": "மின்னஞ்சல்", + "publish_dialog_email_placeholder": "அறிவிப்பை அனுப்ப முகவரி, எ.கா. phil@example.com", + "publish_dialog_email_reset": "மின்னஞ்சலை முன்னோக்கி அகற்றவும்", + "publish_dialog_call_label": "தொலைபேசி அழைப்பு", + "publish_dialog_call_item": "தொலைபேசி எண்ணை அழைக்கவும் {{number}}", + "publish_dialog_call_reset": "தொலைபேசி அழைப்பை அகற்று", + "publish_dialog_attach_label": "இணைப்பு முகவரி", + "publish_dialog_attach_placeholder": "முகவரி ஆல் கோப்பை இணைக்கவும், எ.கா. https://f-droid.org/f-droid.apk", + "publish_dialog_attach_reset": "இணைப்பு முகவரி ஐ அகற்று", + "publish_dialog_filename_label": "கோப்புப்பெயர்", + "publish_dialog_filename_placeholder": "இணைப்பு கோப்பு பெயர்", + "publish_dialog_delay_label": "சுணக்கம்", + "publish_dialog_delay_placeholder": "நேரந்தவறுகை வழங்கல், எ.கா. {{unixTimestamp}}, {{relativeTime}}, அல்லது \"{{naturalLanguage}}\" (ஆங்கிலம் மட்டும்)", + "publish_dialog_delay_reset": "தாமதமான விநியோகத்தை அகற்று", + "publish_dialog_other_features": "பிற அம்சங்கள்:", + "publish_dialog_chip_click_label": "முகவரி ஐக் சொடுக்கு செய்க", + "publish_dialog_chip_call_label": "தொலைபேசி அழைப்பு", + "publish_dialog_chip_email_label": "மின்னஞ்சலுக்கு அனுப்பவும்", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "சரிபார்க்கப்பட்ட தொலைபேசி எண்கள் இல்லை", + "publish_dialog_chip_attach_url_label": "முகவரி மூலம் கோப்பை இணைக்கவும்", + "publish_dialog_details_examples_description": "எடுத்துக்காட்டுகள் மற்றும் அனைத்து அனுப்பும் அம்சங்களின் விரிவான விளக்கத்திற்கு, தயவுசெய்து ஆவணங்கள் ஐப் பார்க்கவும்.", + "publish_dialog_chip_attach_file_label": "உள்ளக கோப்பை இணைக்கவும்", + "publish_dialog_chip_delay_label": "நேரந்தவறுகை வழங்கல்", + "publish_dialog_chip_topic_label": "தலைப்பை மாற்றவும்", + "subscribe_dialog_subscribe_button_generate_topic_name": "பெயரை உருவாக்குங்கள்", + "subscribe_dialog_subscribe_use_another_background_info": "வலை பயன்பாடு திறக்கப்படாதபோது பிற சேவையகங்களிலிருந்து அறிவிப்புகள் பெறப்படாது", + "subscribe_dialog_subscribe_button_cancel": "ரத்துசெய்", + "subscribe_dialog_subscribe_button_subscribe": "குழுசேர்", + "subscribe_dialog_login_title": "உள்நுழைவு தேவை", + "account_basics_password_dialog_confirm_password_label": "கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "account_basics_password_dialog_current_password_incorrect": "கடவுச்சொல் தவறானது", + "account_basics_password_dialog_button_submit": "கடவுச்சொல்லை மாற்றவும்", + "account_basics_phone_numbers_title": "தொலைபேசி எண்கள்", + "account_basics_phone_numbers_dialog_description": "அழைப்பு அறிவிப்பு அம்சத்தைப் பயன்படுத்த, நீங்கள் குறைந்தது ஒரு தொலைபேசி எண்ணையாவது சேர்த்து சரிபார்க்க வேண்டும். சரிபார்ப்பு எச்எம்எச் அல்லது தொலைபேசி அழைப்பு வழியாக செய்யப்படலாம்.", + "account_basics_phone_numbers_description": "தொலைபேசி அழைப்பு அறிவிப்புகளுக்கு", + "account_basics_phone_numbers_no_phone_numbers_yet": "தொலைபேசி எண்கள் இதுவரை இல்லை", + "account_basics_phone_numbers_copied_to_clipboard": "தொலைபேசி எண் இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", + "account_basics_phone_numbers_dialog_title": "தொலைபேசி எண்ணைச் சேர்க்கவும்", + "account_basics_phone_numbers_dialog_number_placeholder": "எ.கா. +122333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "எச்எம்எச் அனுப்பு", + "account_basics_phone_numbers_dialog_code_placeholder": "எ.கா. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "குறியீட்டை உறுதிப்படுத்தவும்", + "account_basics_phone_numbers_dialog_verify_button_call": "என்னை அழைக்கவும்", + "account_basics_phone_numbers_dialog_code_label": "சரிபார்ப்பு குறியீடு", + "account_basics_phone_numbers_dialog_channel_sms": "எச்.எம்.எச்", + "account_basics_phone_numbers_dialog_channel_call": "அழைப்பு", + "account_usage_title": "பயன்பாடு", + "account_usage_unlimited": "வரம்பற்றது", + "account_usage_of_limit": "{{limit}} of", + "account_usage_limits_reset_daily": "பயன்பாட்டு வரம்புகள் நள்ளிரவில் தினமும் மீட்டமைக்கப்படுகின்றன (UTC)", + "account_basics_tier_title": "கணக்கு வகை", + "account_basics_tier_description": "உங்கள் கணக்கின் ஆற்றல் நிலை", + "account_basics_tier_admin": "நிர்வாகி", + "account_basics_tier_admin_suffix_with_tier": "({{tier}} அடுக்கு)", + "account_basics_tier_admin_suffix_no_tier": "(அடுக்கு இல்லை)", + "account_basics_tier_basic": "அடிப்படை", + "account_basics_tier_free": "இலவசம்", + "account_basics_tier_interval_monthly": "மாதாந்திர", + "account_basics_tier_interval_yearly": "ஆண்டுதோறும்", + "account_basics_tier_upgrade_button": "சார்புக்கு மேம்படுத்தவும்", + "account_basics_tier_change_button": "மாற்றம்", + "account_basics_tier_paid_until": "சந்தா {{date}} வரை செலுத்தப்படுகிறது, மேலும் தானாக புதுப்பிக்கப்படும்", + "account_basics_tier_canceled_subscription": "உங்கள் சந்தா ரத்து செய்யப்பட்டது மற்றும் {{date} at இல் இலவச கணக்கிற்கு தரமிறக்கப்படும்.", + "account_basics_tier_manage_billing_button": "பட்டியலிடல் நிர்வகிக்கவும்", + "account_basics_tier_payment_overdue": "உங்கள் கட்டணம் தாமதமானது. தயவுசெய்து உங்கள் கட்டண முறையைப் புதுப்பிக்கவும், அல்லது உங்கள் கணக்கு விரைவில் தரமிறக்கப்படும்.", + "account_usage_messages_title": "வெளியிடப்பட்ட செய்திகள்", + "account_usage_emails_title": "மின்னஞ்சல்கள் அனுப்பப்பட்டன", + "account_usage_calls_title": "தொலைபேசி அழைப்புகள் செய்யப்பட்டன", + "account_usage_calls_none": "இந்த கணக்கில் தொலைபேசி அழைப்புகள் எதுவும் செய்ய முடியாது", + "account_usage_reservations_title": "ஒதுக்கப்பட்ட தலைப்புகள்", + "account_usage_reservations_none": "இந்த கணக்கிற்கு ஒதுக்கப்பட்ட தலைப்புகள் இல்லை", + "account_usage_attachment_storage_title": "இணைப்பு சேமிப்பு", + "account_usage_attachment_storage_description": "கோப்பு {{filesize}} க்குப் பிறகு நீக்கப்பட்ட ஒரு கோப்பிற்கு {{expiry}}}}", + "account_usage_basis_ip_description": "இந்த கணக்கிற்கான பயன்பாட்டு புள்ளிவிவரங்கள் மற்றும் வரம்புகள் உங்கள் ஐபி முகவரியை அடிப்படையாகக் கொண்டவை, எனவே அவை மற்ற பயனர்களுடன் பகிரப்படலாம். மேலே காட்டப்பட்டுள்ள வரம்புகள் தற்போதுள்ள விகித வரம்புகளின் அடிப்படையில் தோராயங்கள்.", + "account_usage_cannot_create_portal_session": "பட்டியலிடல் போர்ட்டலைத் திறக்க முடியவில்லை", + "account_delete_title": "கணக்கை நீக்கு", + "account_delete_description": "உங்கள் கணக்கை நிரந்தரமாக நீக்கவும்", + "account_upgrade_dialog_cancel_warning": "இது உங்கள் சந்தாவை ரத்துசெய்யும் , மேலும் உங்கள் கணக்கை {{date} at இல் தரமிறக்குகிறது. அந்த தேதியில், தலைப்பு முன்பதிவு மற்றும் சேவையகத்தில் தற்காலிகமாக சேமிக்கப்பட்ட செய்திகளும் நீக்கப்படும் .", + "account_upgrade_dialog_proration_info": " புரோரேசன் : கட்டணத் திட்டங்களுக்கு இடையில் மேம்படுத்தும்போது, விலை வேறுபாடு உடனடியாக கட்டணம் வசூலிக்கப்படும் . குறைந்த அடுக்குக்கு தரமிறக்கும்போது, எதிர்கால பட்டியலிடல் காலங்களுக்கு செலுத்த இருப்பு பயன்படுத்தப்படும்.", + "account_upgrade_dialog_reservations_warning_one": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கை விட குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தது ஒரு முன்பதிவை நீக்கு . <இணைப்பு> அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", + "account_upgrade_dialog_reservations_warning_other": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கை விட குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தபட்சம் {{count}} முன்பதிவு ஐ நீக்கவும். <இணைப்பு> அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} ஒதுக்கப்பட்ட தலைப்புகள்", + "account_upgrade_dialog_tier_features_no_reservations": "ஒதுக்கப்பட்ட தலைப்புகள் இல்லை", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} நாள்தோறும் செய்தி", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} நாள்தோறும் செய்திகள்", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} நாள்தோறும் மின்னஞ்சல்", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} நாள்தோறும் மின்னஞ்சல்கள்", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} நாள்தோறும் தொலைபேசி அழைப்புகள்", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} நாள்தோறும் தொலைபேசி அழைப்புகள்", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} மொத்த சேமிப்பு", + "account_upgrade_dialog_tier_price_per_month": "மாதம்", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}}}}}}. மாதந்தோறும் பாடு.", + "account_upgrade_dialog_tier_features_no_calls": "தொலைபேசி அழைப்புகள் இல்லை", + "account_upgrade_dialog_tier_features_attachment_file_size": "கோப்பு {filesize}}} ஒரு கோப்பிற்கு", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} ஆண்டுதோறும் கட்டணம் செலுத்தப்படுகிறது. {{save}} சேமி.", + "account_upgrade_dialog_tier_selected_label": "தேர்ந்தெடுக்கப்பட்டது", + "account_upgrade_dialog_tier_current_label": "மின்னோட்ட்ம், ஓட்டம்", + "account_upgrade_dialog_billing_contact_email": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து <இணைப்பு> எங்களை தொடர்பு கொள்ளவும் நேரடியாக.", + "account_upgrade_dialog_button_cancel": "ரத்துசெய்", + "account_upgrade_dialog_billing_contact_website": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து எங்கள் <இணைப்பு> வலைத்தளம் ஐப் பார்க்கவும்.", + "account_upgrade_dialog_button_redirect_signup": "இப்போது பதிவுபெறுக", + "account_upgrade_dialog_button_pay_now": "இப்போது பணம் செலுத்தி குழுசேரவும்", + "account_upgrade_dialog_button_cancel_subscription": "சந்தாவை ரத்துசெய்", + "account_tokens_title": "டோக்கன்களை அணுகவும்", + "account_tokens_description": "NTFY பநிஇ வழியாக வெளியிடும் மற்றும் சந்தா செலுத்தும் போது அணுகல் டோக்கன்களைப் பயன்படுத்தவும், எனவே உங்கள் கணக்கு நற்சான்றிதழ்களை அனுப்ப வேண்டியதில்லை. மேலும் அறிய <இணைப்பு> ஆவணங்கள் ஐப் பாருங்கள்.", + "account_upgrade_dialog_button_update_subscription": "சந்தாவைப் புதுப்பிக்கவும்", + "account_tokens_table_token_header": "கிள்ளாக்கு", + "account_tokens_table_label_header": "சிட்டை", + "account_tokens_table_last_access_header": "கடைசி அணுகல்", + "account_tokens_table_expires_header": "காலாவதியாகிறது", + "account_tokens_table_never_expires": "ஒருபோதும் காலாவதியாகாது", + "account_tokens_table_cannot_delete_or_edit": "தற்போதைய அமர்வு டோக்கனைத் திருத்தவோ நீக்கவோ முடியாது", + "account_tokens_table_current_session": "தற்போதைய உலாவி அமர்வு", + "account_tokens_table_copied_to_clipboard": "அணுகல் கிள்ளாக்கு நகலெடுக்கப்பட்டது", + "account_tokens_table_create_token_button": "அணுகல் கிள்ளாக்கை உருவாக்கவும்", + "account_tokens_dialog_title_create": "அணுகல் கிள்ளாக்கை உருவாக்கவும்", + "account_tokens_table_last_origin_tooltip": "ஐபி முகவரி {{ip} இருந்து இலிருந்து, தேடலைக் சொடுக்கு செய்க", + "account_tokens_dialog_title_edit": "அணுகல் டோக்கனைத் திருத்தவும்", + "account_tokens_dialog_title_delete": "அணுகல் கிள்ளாக்கை நீக்கு", + "account_tokens_dialog_label": "சிட்டை, எ.கா. ராடார் அறிவிப்புகள்", + "account_tokens_dialog_button_create": "கிள்ளாக்கை உருவாக்கவும்", + "account_tokens_dialog_button_update": "கிள்ளாக்கைப் புதுப்பிக்கவும்", + "account_tokens_dialog_button_cancel": "ரத்துசெய்", + "account_tokens_dialog_expires_label": "அணுகல் கிள்ளாக்கு காலாவதியாகிறது", + "account_tokens_dialog_expires_unchanged": "காலாவதி தேதி மாறாமல் விடுங்கள்", + "account_tokens_dialog_expires_x_hours": "கிள்ளாக்கு {{hours}} மணிநேரங்களில் காலாவதியாகிறது", + "account_tokens_dialog_expires_x_days": "கிள்ளாக்கு {{days}} நாட்களில் காலாவதியாகிறது", + "account_tokens_dialog_expires_never": "கிள்ளாக்கு ஒருபோதும் காலாவதியாகாது", + "account_tokens_delete_dialog_title": "அணுகல் கிள்ளாக்கை நீக்கு", + "account_tokens_delete_dialog_description": "அணுகல் கிள்ளாக்கை நீக்குவதற்கு முன், பயன்பாடுகள் அல்லது ச்கிரிப்ட்கள் எதுவும் தீவிரமாகப் பயன்படுத்தவில்லை என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள். இந்த செயலை செயல்தவிர்க்க முடியாது .", + "account_tokens_delete_dialog_submit_button": "கிள்ளாக்கை நிரந்தரமாக நீக்கு", + "prefs_notifications_title": "அறிவிப்புகள்", + "prefs_notifications_sound_title": "அறிவிப்பு ஒலி", + "prefs_notifications_sound_description_none": "அறிவிப்புகள் வரும்போது எந்த ஒலியையும் இயக்காது", + "prefs_notifications_sound_description_some": "அறிவிப்புகள் வரும்போது {{sound}} ஒலியை இயக்குகின்றன", + "prefs_notifications_sound_no_sound": "ஒலி இல்லை", + "prefs_notifications_sound_play": "தேர்ந்தெடுக்கப்பட்ட ஒலி விளையாடுங்கள்", + "prefs_notifications_min_priority_title": "குறைந்தபட்ச முன்னுரிமை", + "prefs_notifications_min_priority_description_any": "முன்னுரிமையைப் பொருட்படுத்தாமல் அனைத்து அறிவிப்புகளையும் காட்டுகிறது", + "prefs_notifications_min_priority_description_x_or_higher": "முன்னுரிமை {{number}} ({{name}}) அல்லது அதற்கு மேல் இருந்தால் அறிவிப்புகளைக் காட்டு", + "prefs_notifications_min_priority_description_max": "முன்னுரிமை 5 (அதிகபட்சம்) என்றால் அறிவிப்புகளைக் காட்டு", + "prefs_notifications_min_priority_any": "எந்த முன்னுரிமையும்", + "prefs_notifications_min_priority_low_and_higher": "குறைந்த முன்னுரிமை மற்றும் அதிக", + "prefs_notifications_min_priority_default_and_higher": "இயல்புநிலை முன்னுரிமை மற்றும் அதிக", + "prefs_notifications_min_priority_high_and_higher": "அதிக முன்னுரிமை மற்றும் அதிக", + "prefs_notifications_min_priority_max_only": "அதிகபட்ச முன்னுரிமை மட்டுமே", + "prefs_notifications_delete_after_title": "அறிவிப்புகளை நீக்கு", + "prefs_notifications_delete_after_never": "ஒருபோதும்", + "prefs_notifications_delete_after_three_hours": "மூன்று மணி நேரம் கழித்து", + "prefs_notifications_delete_after_one_day": "ஒரு நாள் கழித்து", + "prefs_notifications_delete_after_one_week": "ஒரு வாரம் கழித்து", + "prefs_notifications_delete_after_one_month": "ஒரு மாதத்திற்குப் பிறகு", + "prefs_notifications_delete_after_never_description": "அறிவிப்புகள் ஒருபோதும் தானாக நீக்கப்படவில்லை", + "prefs_notifications_delete_after_three_hours_description": "அறிவிப்புகள் மூன்று மணி நேரத்திற்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_delete_after_one_day_description": "அறிவிப்புகள் ஒரு நாளுக்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_delete_after_one_week_description": "அறிவிப்புகள் ஒரு வாரத்திற்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_delete_after_one_month_description": "அறிவிப்புகள் ஒரு மாதத்திற்குப் பிறகு தானாக நீக்கப்படும்", + "prefs_notifications_web_push_title": "பின்னணி அறிவிப்புகள்", + "prefs_notifications_web_push_enabled_description": "வலை பயன்பாடு இயங்காதபோது கூட அறிவிப்புகள் பெறப்படுகின்றன (வலை புச் வழியாக)", + "prefs_notifications_web_push_disabled_description": "வலை பயன்பாடு இயங்கும்போது அறிவிப்பு பெறப்படுகிறது (வெப்சாக்கெட் வழியாக)", + "prefs_notifications_web_push_enabled": "{{server} க்கு க்கு இயக்கப்பட்டது", + "prefs_notifications_web_push_disabled": "முடக்கப்பட்டது", + "prefs_users_title": "பயனர்களை நிர்வகிக்கவும்", + "prefs_users_description": "உங்கள் பாதுகாக்கப்பட்ட தலைப்புகளுக்கு பயனர்களை இங்கே சேர்க்கவும்/அகற்றவும். பயனர்பெயர் மற்றும் கடவுச்சொல் உலாவியின் உள்ளக சேமிப்பகத்தில் சேமிக்கப்பட்டுள்ளன என்பதை நினைவில் கொள்க.", + "prefs_users_description_no_sync": "பயனர்கள் மற்றும் கடவுச்சொற்கள் உங்கள் கணக்கில் ஒத்திசைக்கப்படவில்லை.", + "prefs_users_table": "பயனர்கள் அட்டவணை", + "prefs_users_edit_button": "பயனரைத் திருத்து", + "prefs_users_delete_button": "பயனரை நீக்கு", + "prefs_users_table_cannot_delete_or_edit": "உள்நுழைந்த பயனரை நீக்கவோ திருத்தவோ முடியாது", + "prefs_users_table_user_header": "பயனர்", + "prefs_users_table_base_url_header": "பணி முகவரி", + "prefs_users_dialog_title_add": "பயனரைச் சேர்க்கவும்", + "prefs_users_dialog_title_edit": "பயனரைத் திருத்து", + "prefs_users_dialog_base_url_label": "பணி முகவரி, எ.கா. https://ntfy.sh", + "prefs_users_dialog_username_label": "பயனர்பெயர், எ.கா. பில்", + "prefs_users_dialog_password_label": "கடவுச்சொல்", + "prefs_appearance_title": "தோற்றம்", + "prefs_appearance_language_title": "மொழி", + "prefs_appearance_theme_title": "கருப்பொருள்", + "prefs_appearance_theme_system": "கணினி (இயல்புநிலை)", + "prefs_appearance_theme_dark": "இருண்ட முறை", + "prefs_appearance_theme_light": "ஒளி பயன்முறை", + "prefs_reservations_title": "ஒதுக்கப்பட்ட தலைப்புகள்", + "prefs_reservations_description": "தனிப்பட்ட பயன்பாட்டிற்காக தலைப்பு பெயர்களை இங்கே முன்பதிவு செய்யலாம். ஒரு தலைப்பை முன்பதிவு செய்வது தலைப்பின் மீது உங்களுக்கு உரிமையை அளிக்கிறது, மேலும் தலைப்பில் பிற பயனர்களுக்கான அணுகல் அனுமதிகளை வரையறுக்க உங்களை அனுமதிக்கிறது.", + "prefs_reservations_limit_reached": "உங்கள் ஒதுக்கப்பட்ட தலைப்புகளின் வரம்பை நீங்கள் அடைந்தீர்கள்.", + "prefs_reservations_add_button": "ஒதுக்கப்பட்ட தலைப்பைச் சேர்க்கவும்", + "prefs_reservations_edit_button": "தலைப்பு அணுகலைத் திருத்தவும்", + "prefs_reservations_delete_button": "தலைப்பு அணுகலை மீட்டமைக்கவும்", + "prefs_reservations_table": "ஒதுக்கப்பட்ட தலைப்புகள் அட்டவணை", + "prefs_reservations_table_topic_header": "தலைப்பு", + "prefs_reservations_table_access_header": "அணுகல்", + "prefs_reservations_table_everyone_deny_all": "நான் மட்டுமே வெளியிட்டு குழுசேர முடியும்", + "prefs_reservations_table_everyone_read_only": "நான் வெளியிட்டு குழுசேரலாம், அனைவரும் குழுசேரலாம்", + "prefs_reservations_table_everyone_write_only": "நான் வெளியிட்டு குழுசேரலாம், எல்லோரும் வெளியிடலாம்", + "prefs_reservations_table_everyone_read_write": "எல்லோரும் வெளியிட்டு குழுசேரலாம்", + "prefs_reservations_table_not_subscribed": "குழுசேரவில்லை", + "prefs_reservations_table_click_to_subscribe": "குழுசேர சொடுக்கு செய்க", + "prefs_reservations_dialog_title_add": "இருப்பு தலைப்பு", + "prefs_reservations_dialog_title_edit": "ஒதுக்கப்பட்ட தலைப்பைத் திருத்து", + "prefs_reservations_dialog_title_delete": "தலைப்பு முன்பதிவை நீக்கு", + "prefs_reservations_dialog_description": "ஒரு தலைப்பை முன்பதிவு செய்வது தலைப்பின் மீது உங்களுக்கு உரிமையை அளிக்கிறது, மேலும் தலைப்பில் பிற பயனர்களுக்கான அணுகல் அனுமதிகளை வரையறுக்க உங்களை அனுமதிக்கிறது.", + "prefs_reservations_dialog_topic_label": "தலைப்பு", + "prefs_reservations_dialog_access_label": "அணுகல்", + "reservation_delete_dialog_description": "முன்பதிவை அகற்றுவது தலைப்பின் மீது உரிமையை அளிக்கிறது, மேலும் மற்றவர்கள் அதை முன்பதிவு செய்ய அனுமதிக்கிறது. ஏற்கனவே உள்ள செய்திகளையும் இணைப்புகளையும் வைத்திருக்கலாம் அல்லது நீக்கலாம்.", + "reservation_delete_dialog_action_keep_title": "தற்காலிக சேமிப்பு செய்திகள் மற்றும் இணைப்புகளை வைத்திருங்கள்", + "reservation_delete_dialog_action_keep_description": "சேவையகத்தில் தற்காலிக சேமிப்பில் உள்ள செய்திகள் மற்றும் இணைப்புகள் தலைப்புப் பெயரைப் பற்றிய அறிவுள்ளவர்களுக்கு பகிரங்கமாகத் தெரியும்.", + "reservation_delete_dialog_action_delete_title": "தற்காலிக சேமிப்பு செய்திகள் மற்றும் இணைப்புகளை நீக்கவும்", + "reservation_delete_dialog_submit_button": "முன்பதிவை நீக்கு", + "reservation_delete_dialog_action_delete_description": "தற்காலிக சேமிக்கப்பட்ட செய்திகள் மற்றும் இணைப்புகள் நிரந்தரமாக நீக்கப்படும். இந்த செயலை செயல்தவிர்க்க முடியாது.", + "priority_min": "மணித்துளி", + "priority_low": "குறைந்த", + "priority_high": "உயர்ந்த", + "priority_max": "அதிகபட்சம்", + "priority_default": "இயல்புநிலை", + "error_boundary_title": "ஓ, NTFY செயலிழந்தது", + "error_boundary_description": "இது வெளிப்படையாக நடக்கக்கூடாது. இதைப் பற்றி மிகவும் வருந்துகிறேன். .", + "error_boundary_button_copy_stack_trace": "அடுக்கு சுவடு நகலெடுக்கவும்", + "error_boundary_button_reload_ntfy": "Ntfy ஐ மீண்டும் ஏற்றவும்", + "error_boundary_stack_trace": "ச்டாக் சுவடு", + "error_boundary_gathering_info": "மேலும் தகவலை சேகரிக்கவும்…", + "error_boundary_unsupported_indexeddb_title": "தனியார் உலாவல் ஆதரிக்கப்படவில்லை", + "common_cancel": "ரத்துசெய்", + "common_save": "சேமி", + "common_add": "கூட்டு", + "common_back": "பின்", + "common_copy_to_clipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கவும்", + "signup_title": "ஒரு NTFY கணக்கை உருவாக்கவும்", + "signup_form_username": "பயனர்பெயர்", + "signup_form_password": "கடவுச்சொல்", + "signup_form_confirm_password": "கடவுச்சொல்லை உறுதிப்படுத்தவும்", + "signup_form_button_submit": "பதிவு செய்க", + "signup_form_toggle_password_visibility": "கடவுச்சொல் தெரிவுநிலையை மாற்றவும்", + "signup_already_have_account": "ஏற்கனவே ஒரு கணக்கு இருக்கிறதா? உள்நுழைக!", + "signup_disabled": "கையொப்பம் முடக்கப்பட்டுள்ளது", + "signup_error_username_taken": "பயனர்பெயர் {{username}} ஏற்கனவே எடுக்கப்பட்டுள்ளது", + "signup_error_creation_limit_reached": "கணக்கு உருவாக்கும் வரம்பு எட்டப்பட்டது", + "login_title": "உங்கள் NTFY கணக்கில் உள்நுழைக", + "login_form_button_submit": "விடுபதிகை", + "login_link_signup": "பதிவு செய்க", + "login_disabled": "உள்நுழைவு முடக்கப்பட்டுள்ளது", + "action_bar_reservation_edit": "முன்பதிவை மாற்றவும்", + "action_bar_reservation_delete": "முன்பதிவை அகற்று", + "action_bar_reservation_limit_reached": "வரம்பு எட்டப்பட்டது", + "action_bar_send_test_notification": "சோதனை அறிவிப்பை அனுப்பவும்", + "action_bar_clear_notifications": "எல்லா அறிவிப்புகளையும் அழிக்கவும்", + "action_bar_mute_notifications": "முடக்கு அறிவிப்புகள்", + "action_bar_unmute_notifications": "ஊடுருவல் அறிவிப்புகள்", + "action_bar_unsubscribe": "குழுவிலகவும்", + "action_bar_toggle_mute": "முடக்கு/அசைவது அறிவிப்புகள்", + "action_bar_toggle_action_menu": "செயல் மெனுவைத் திறக்க/மூடு", + "action_bar_profile_title": "சுயவிவரம்", + "action_bar_profile_settings": "அமைப்புகள்", + "action_bar_profile_logout": "வெளியேற்றம்", + "action_bar_sign_in": "விடுபதிகை", + "action_bar_sign_up": "பதிவு செய்க", + "message_bar_type_message": "இங்கே ஒரு செய்தியைத் தட்டச்சு செய்க", + "message_bar_error_publishing": "பிழை வெளியீட்டு அறிவிப்பு", + "message_bar_show_dialog": "வெளியீட்டு உரையாடலைக் காட்டு", + "nav_button_subscribe": "தலைப்புக்கு குழுசேரவும்", + "nav_button_muted": "அறிவிப்புகள் முடக்கப்பட்டன", + "nav_button_connecting": "இணைத்தல்", + "nav_upgrade_banner_label": "Ntfy Pro க்கு மேம்படுத்தவும்", + "nav_upgrade_banner_description": "தலைப்புகள், கூடுதல் செய்திகள் மற்றும் மின்னஞ்சல்கள் மற்றும் பெரிய இணைப்புகளை முன்பதிவு செய்யுங்கள்", + "alert_notification_permission_required_title": "அறிவிப்புகள் முடக்கப்பட்டுள்ளன", + "alert_notification_permission_required_description": "டெச்க்டாப் அறிவிப்புகளைக் காண்பிக்க உங்கள் உலாவி இசைவு வழங்கவும்", + "alert_notification_permission_required_button": "இப்போது வழங்கவும்", + "alert_notification_permission_denied_title": "அறிவிப்புகள் தடுக்கப்பட்டுள்ளன", + "alert_notification_permission_denied_description": "தயவுசெய்து அவற்றை உங்கள் உலாவியில் மீண்டும் இயக்கவும்", + "alert_notification_ios_install_required_title": "ஐஇமு நிறுவல் தேவை", + "alert_notification_ios_install_required_description": "ஐஇமு இல் அறிவிப்புகளை இயக்க பகிர்வு ஐகானைக் சொடுக்கு செய்து முகப்புத் திரையில் சேர்க்கவும்", + "alert_not_supported_title": "அறிவிப்புகள் ஆதரிக்கப்படவில்லை", + "notifications_new_indicator": "புதிய அறிவிப்பு", + "notifications_attachment_image": "இணைப்பு படம்", + "notifications_attachment_copy_url_title": "இணைப்பு முகவரி ஐ இடைநிலைப்பலகைக்கு நகலெடுக்கவும்", + "notifications_attachment_copy_url_button": "முகவரி ஐ நகலெடுக்கவும்", + "notifications_attachment_open_title": "{{url}} க்குச் செல்லவும்", + "notifications_attachment_open_button": "திறந்த இணைப்பு", + "notifications_attachment_link_expires": "இணைப்பு காலாவதியாகிறது {{date}}", + "notifications_attachment_link_expired": "இணைப்பு காலாவதியான பதிவிறக்க", + "notifications_attachment_file_image": "பட கோப்பு", + "notifications_attachment_file_video": "வீடியோ கோப்பு", + "notifications_attachment_file_audio": "ஆடியோ கோப்பு", + "notifications_attachment_file_app": "ஆண்ட்ராய்டு பயன்பாட்டு கோப்பு", + "notifications_attachment_file_document": "பிற ஆவணம்", + "notifications_click_copy_url_title": "இடைநிலைப்பலகைக்கு இணைப்பு முகவரி ஐ நகலெடுக்கவும்", + "notifications_click_copy_url_button": "இணைப்பை நகலெடுக்கவும்", + "notifications_click_open_button": "இணைப்பை திற", + "notifications_actions_open_url_title": "{{url}} க்குச் செல்லவும்", + "notifications_none_for_any_title": "உங்களுக்கு எந்த அறிவிப்புகளும் கிடைக்கவில்லை.", + "notifications_none_for_any_description": "ஒரு தலைப்புக்கு அறிவிப்புகளை அனுப்ப, தலைப்பு முகவரி க்கு வைக்கவும் அல்லது இடுகையிடவும். உங்கள் தலைப்புகளில் ஒன்றைப் பயன்படுத்தி இங்கே ஒரு எடுத்துக்காட்டு.", + "notifications_no_subscriptions_title": "உங்களிடம் இன்னும் சந்தாக்கள் இல்லை என்று தெரிகிறது.", + "notifications_no_subscriptions_description": "ஒரு தலைப்பை உருவாக்க அல்லது குழுசேர \"{{linktext}}\" இணைப்பைக் சொடுக்கு செய்க. அதன்பிறகு, நீங்கள் புட் அல்லது இடுகை வழியாக செய்திகளை அனுப்பலாம், மேலும் நீங்கள் இங்கே அறிவிப்புகளைப் பெறுவீர்கள்.", + "notifications_example": "எடுத்துக்காட்டு", + "notifications_more_details": "மேலும் தகவலுக்கு, வலைத்தளம் அல்லது ஆவணங்கள் ஐப் பாருங்கள்.", + "display_name_dialog_title": "காட்சி பெயரை மாற்றவும்", + "display_name_dialog_description": "சந்தா பட்டியலில் காட்டப்படும் தலைப்புக்கு மாற்று பெயரை அமைக்கவும். சிக்கலான பெயர்களைக் கொண்ட தலைப்புகளை மிக எளிதாக அடையாளம் காண இது உதவுகிறது.", + "display_name_dialog_placeholder": "காட்சி பெயர்", + "reserve_dialog_checkbox_label": "தலைப்பை முன்பதிவு செய்து அணுகலை உள்ளமைக்கவும்", + "publish_dialog_button_cancel_sending": "அனுப்புவதை ரத்துசெய்", + "publish_dialog_button_cancel": "ரத்துசெய்", + "publish_dialog_button_send": "அனுப்பு", + "publish_dialog_checkbox_markdown": "மார்க் பேரூர் என வடிவம்", + "publish_dialog_checkbox_publish_another": "மற்றொன்றை வெளியிடுங்கள்", + "publish_dialog_attached_file_title": "இணைக்கப்பட்ட கோப்பு:", + "publish_dialog_attached_file_filename_placeholder": "இணைப்பு கோப்பு பெயர்", + "publish_dialog_attached_file_remove": "இணைக்கப்பட்ட கோப்பை அகற்று", + "publish_dialog_drop_file_here": "கோப்பை இங்கே விடுங்கள்", + "emoji_picker_search_placeholder": "ஈமோசியைத் தேடுங்கள்", + "emoji_picker_search_clear": "தேடலை அழி", + "subscribe_dialog_subscribe_title": "தலைப்புக்கு குழுசேரவும்", + "subscribe_dialog_subscribe_description": "தலைப்புகள் கடவுச்சொல் பாதுகாக்கப்பட்டதாக இருக்காது, எனவே யூகிக்க எளிதான பெயரைத் தேர்வுசெய்க. சந்தா செலுத்தியதும், நீங்கள் அறிவிப்புகளை வைக்கலாம்/இடுகையிடலாம்.", + "subscribe_dialog_subscribe_topic_placeholder": "தலைப்பு பெயர், எ.கா. phil_alerts", + "subscribe_dialog_subscribe_use_another_label": "மற்றொரு சேவையகத்தைப் பயன்படுத்தவும்", + "subscribe_dialog_subscribe_base_url_label": "பணி முகவரி", + "subscribe_dialog_login_description": "இந்த தலைப்பு கடவுச்சொல் பாதுகாக்கப்படுகிறது. குழுசேர பயனர்பெயர் மற்றும் கடவுச்சொல்லை உள்ளிடவும்.", + "subscribe_dialog_login_username_label": "பயனர்பெயர், எ.கா. பில்", + "subscribe_dialog_login_password_label": "கடவுச்சொல்", + "subscribe_dialog_login_button_login": "புகுபதிவு", + "subscribe_dialog_error_user_not_authorized": "பயனர் {{username}} அங்கீகரிக்கப்படவில்லை", + "subscribe_dialog_error_topic_already_reserved": "தலைப்பு ஏற்கனவே ஒதுக்கப்பட்டுள்ளது", + "subscribe_dialog_error_user_anonymous": "அநாமதேய", + "account_basics_title": "கணக்கு", + "account_basics_username_title": "பயனர்பெயர்", + "account_basics_username_description": "ஏய், அது நீங்கள் தான்", + "account_basics_username_admin_tooltip": "நீங்கள் நிர்வாகி", + "account_basics_password_title": "கடவுச்சொல்", + "account_basics_password_description": "உங்கள் கணக்கு கடவுச்சொல்லை மாற்றவும்", + "account_basics_password_dialog_title": "கடவுச்சொல்லை மாற்றவும்", + "account_basics_password_dialog_current_password_label": "தற்போதைய கடவுச்சொல்", + "account_basics_password_dialog_new_password_label": "புதிய கடவுச்சொல்", + "account_basics_phone_numbers_dialog_number_label": "தொலைபேசி எண்", + "account_delete_dialog_description": "இது சேவையகத்தில் சேமிக்கப்பட்டுள்ள அனைத்து தரவுகளும் உட்பட உங்கள் கணக்கை நிரந்தரமாக நீக்கும். நீக்கப்பட்ட பிறகு, உங்கள் பயனர்பெயர் 7 நாட்களுக்கு கிடைக்காது. நீங்கள் உண்மையிலேயே தொடர விரும்பினால், கீழே உள்ள பெட்டியில் உங்கள் கடவுச்சொல்லை உறுதிப்படுத்தவும்.", + "account_delete_dialog_label": "கடவுச்சொல்", + "account_delete_dialog_button_cancel": "ரத்துசெய்", + "account_delete_dialog_button_submit": "கணக்கை நிரந்தரமாக நீக்கு", + "account_delete_dialog_billing_warning": "உங்கள் கணக்கை நீக்குவது உடனடியாக உங்கள் பட்டியலிடல் சந்தாவை ரத்து செய்கிறது. உங்களுக்கு இனி பட்டியலிடல் டாச்போர்டுக்கு அணுகல் இருக்காது.", + "account_upgrade_dialog_title": "கணக்கு அடுக்கை மாற்றவும்", + "account_upgrade_dialog_interval_monthly": "மாதாந்திர", + "account_upgrade_dialog_interval_yearly": "ஆண்டுதோறும்", + "account_upgrade_dialog_interval_yearly_discount_save": "{{discount}}% சேமிக்கவும்", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "{{discount}}% வரை சேமிக்கவும்", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} முன்பதிவு செய்யப்பட்ட தலைப்பு", + "prefs_users_add_button": "பயனரைச் சேர்க்கவும்", + "error_boundary_unsupported_indexeddb_description": "NTFY வலை பயன்பாட்டிற்கு செயல்பட குறியீட்டு தேவை, மற்றும் உங்கள் உலாவி தனிப்பட்ட உலாவல் பயன்முறையில் IndexEDDB ஐ ஆதரிக்காது. எப்படியிருந்தாலும் தனிப்பட்ட உலாவல் பயன்முறையில் பயன்பாடு, ஏனென்றால் அனைத்தும் உலாவி சேமிப்பகத்தில் சேமிக்கப்படுகின்றன. இந்த அறிவிலிமையம் இதழில் இல் பற்றி நீங்கள் மேலும் படிக்கலாம் அல்லது டிச்கார்ட் அல்லது மேட்ரிக்ச் இல் எங்களுடன் பேசலாம்.", + "web_push_subscription_expiring_title": "அறிவிப்புகள் இடைநிறுத்தப்படும்", + "web_push_subscription_expiring_body": "தொடர்ந்து அறிவிப்புகளைப் பெற NTFY ஐத் திறக்கவும்", + "web_push_unknown_notification_title": "சேவையகத்திலிருந்து அறியப்படாத அறிவிப்பு பெறப்பட்டது", + "web_push_unknown_notification_body": "வலை பயன்பாட்டைத் திறப்பதன் மூலம் நீங்கள் NTFY ஐ புதுப்பிக்க வேண்டியிருக்கலாம்" +} diff --git a/web/public/static/langs/uk.json b/web/public/static/langs/uk.json index b09822dd..e3e04a19 100644 --- a/web/public/static/langs/uk.json +++ b/web/public/static/langs/uk.json @@ -88,7 +88,7 @@ "action_bar_unsubscribe": "Відписатися", "message_bar_publish": "Опублікувати повідомлення", "nav_button_all_notifications": "Усі сповіщення", - "alert_not_supported_description": "Ваш браузер не підтримує сповіщення.", + "alert_not_supported_description": "Ваш браузер не підтримує сповіщення", "notifications_list": "Список сповіщень", "notifications_mark_read": "Позначити як прочитане", "notifications_delete": "Видалити", @@ -381,5 +381,28 @@ "reservation_delete_dialog_action_delete_title": "Видалення кешованих повідомлень і вкладень", "reservation_delete_dialog_action_keep_title": "Збереження кешованих повідомлень і вкладень", "reservation_delete_dialog_action_keep_description": "Повідомлення і вкладення, які кешуються на сервері, стають загальнодоступними для людей, які знають назву теми.", - "reservation_delete_dialog_action_delete_description": "Кешовані повідомлення та вкладення будуть видалені назавжди. Ця дія не може бути скасована." + "reservation_delete_dialog_action_delete_description": "Кешовані повідомлення та вкладення будуть видалені назавжди. Ця дія не може бути скасована.", + "subscribe_dialog_subscribe_use_another_background_info": "Сповіщення з інших серверів не надходитимуть, якщо вебзастосунок не відкрито", + "publish_dialog_checkbox_markdown": "Форматувати як Markdown", + "alert_notification_ios_install_required_description": "Натисніть піктограму \"Поділитися\" та \"Додати на головний екран\", щоб увімкнути сповіщення на iOS", + "prefs_appearance_theme_dark": "Темний режим", + "web_push_unknown_notification_title": "Отримано невідоме сповіщення від сервера", + "action_bar_mute_notifications": "Вимкнути сповіщення", + "action_bar_unmute_notifications": "Увімкнути сповіщення", + "alert_notification_permission_denied_title": "Сповіщення заблоковано", + "alert_notification_permission_denied_description": "Будь ласка, увімкніть їх повторно у своєму браузері", + "notifications_actions_failed_notification": "Невдала дія", + "prefs_notifications_web_push_title": "Фонові сповіщення", + "prefs_notifications_web_push_enabled_description": "Сповіщення надходитимуть навіть якщо вебзастосунок не запущений (за допомоги Web Push)", + "prefs_notifications_web_push_disabled_description": "Сповіщення надходитимуть якщо вебзастосунок запущений (за допомоги WebSocket)", + "prefs_notifications_web_push_enabled": "Увімкнено для {{server}}", + "prefs_notifications_web_push_disabled": "Вимкнено", + "prefs_appearance_theme_title": "Тема", + "prefs_appearance_theme_system": "Система (за замовчуванням)", + "prefs_appearance_theme_light": "Світлий режим", + "error_boundary_button_reload_ntfy": "Перезавантажити ntfy", + "web_push_subscription_expiring_title": "Сповіщення буде призупинено", + "web_push_subscription_expiring_body": "Відкрийте ntfy, щоб продовжити отримувати сповіщення", + "web_push_unknown_notification_body": "Можливо вам потрібно оновити ntfy шляхом відкриття вебзастосунку", + "alert_notification_ios_install_required_title": "потрібно встановити на iOS" } diff --git a/web/public/static/langs/vi.json b/web/public/static/langs/vi.json index b2f94441..6167c4bc 100644 --- a/web/public/static/langs/vi.json +++ b/web/public/static/langs/vi.json @@ -9,13 +9,23 @@ "signup_already_have_account": "Đã có tài khoản? Đăng nhập!", "signup_disabled": "Đăng kí bị đóng", "signup_error_username_taken": "Tên {{username}} đã được sử dụng", - "signup_error_creation_limit_reached": "Đã bị giới hạn tạo tài khoản", + "signup_error_creation_limit_reached": "Đã đạt giới hạn tạo tài khoản", "login_title": "Đăng nhập vào tài khoản ntfy", "login_link_signup": "Đăng kí", - "login_disabled": "Đăng nhập bị đóng", + "login_disabled": "Đăng nhập bị vô hiệu hóa", "action_bar_show_menu": "Hiện menu", "signup_form_password": "Mật khẩu", "action_bar_settings": "Cài đặt", "signup_form_confirm_password": "Xác nhận mật khẩu", - "signup_form_button_submit": "Đăng kí" + "signup_form_button_submit": "Đăng kí", + "action_bar_change_display_name": "Đổi tên hiển thị", + "action_bar_send_test_notification": "Gửi thông báo thử", + "action_bar_clear_notifications": "Xóa tất cả thông báo", + "action_bar_logo_alt": "Logo ntfy", + "action_bar_account": "Tài khoản", + "action_bar_reservation_limit_reached": "Đã đạt giới hạn", + "action_bar_unsubscribe": "Hủy đăng kí", + "action_bar_unmute_notifications": "Bật thông báo", + "action_bar_toggle_mute": "Bật/tắt thông báo", + "action_bar_mute_notifications": "Tắt thông báo" } diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 0b8b2e7d..dceb5b91 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -189,6 +189,7 @@ const MarkdownContainer = styled("div")` } pre { + overflow-x: scroll; padding: 0.9rem; } diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index 6770f282..c733c23c 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -592,6 +592,7 @@ const Language = () => { Suomi Svenska Türkçe + தமிழ் From 849884c947640bcbc2591ba3a4a515e8c15dc688 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 31 May 2025 22:39:18 -0400 Subject: [PATCH 125/378] Change to "proxy-forwarded-header" and add "proxy-trusted-addrs" --- cmd/serve.go | 11 +- go.mod | 24 +-- go.sum | 138 ++++++++++++-- server/config.go | 5 +- server/server.go | 6 +- server/server.yml | 12 +- server/server_test.go | 28 +-- server/util.go | 60 +++--- server/util_test.go | 26 +++ util/util.go | 27 ++- util/util_test.go | 5 - web/package-lock.json | 420 ++++++++++++++++++++++-------------------- 12 files changed, 482 insertions(+), 280 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 51465c9d..3745ebce 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -88,8 +88,9 @@ var flagsServe = append( altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), - altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-client-ip-header", Aliases: []string{"proxy_client_ip_header"}, EnvVars: []string{"NTFY_PROXY_CLIENT_IP_HEADER"}, Value: "", Usage: "if set, use specified header to determine visitor IP address instead of XFF (for rate limiting)"}), + altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "if set, use specified header to determine visitor IP address instead of XFF (for rate limiting)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addrs", Aliases: []string{"proxy_trusted_addrs"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRS"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}), @@ -191,7 +192,8 @@ func execServe(c *cli.Context) error { visitorEmailLimitBurst := c.Int("visitor-email-limit-burst") visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish") behindProxy := c.Bool("behind-proxy") - proxyClientIPHeader := c.String("proxy-client-ip-header") + proxyForwardedHeader := c.String("proxy-forwarded-header") + proxyTrustedAddrs := util.SplitNoEmpty(c.String("proxy-trusted-addrs"), ",") stripeSecretKey := c.String("stripe-secret-key") stripeWebhookKey := c.String("stripe-webhook-key") billingContact := c.String("billing-contact") @@ -418,7 +420,8 @@ func execServe(c *cli.Context) error { conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.BehindProxy = behindProxy - conf.ProxyForwardedHeader = proxyClientIPHeader + conf.ProxyForwardedHeader = proxyForwardedHeader + conf.ProxyTrustedAddrs = proxyTrustedAddrs conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact diff --git a/go.mod b/go.mod index 0e1533d9..6100a25f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.0 require ( cloud.google.com/go/firestore v1.18.0 // indirect - cloud.google.com/go/storage v1.54.0 // indirect + cloud.google.com/go/storage v1.55.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 @@ -21,7 +21,7 @@ require ( golang.org/x/sync v0.14.0 golang.org/x/term v0.32.0 golang.org/x/time v0.11.0 - google.golang.org/api v0.234.0 + google.golang.org/api v0.235.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -47,21 +47,21 @@ require ( cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect @@ -73,7 +73,7 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect @@ -95,10 +95,10 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/grpc v1.72.1 // indirect + google.golang.org/genproto v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d2d2e7ab..d73473c5 100644 --- a/go.sum +++ b/go.sum @@ -1,83 +1,180 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg= cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= -cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +firebase.google.com/go/v4 v4.15.2 h1:KJtV4rAfO2CVCp40hBfVk+mqUqg7+jQKx7yOgFDnXBg= firebase.google.com/go/v4 v4.15.2/go.mod h1:qkD/HtSumrPMTLs0ahQrje5gTw2WKFKrzVFoqy4SbKA= +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 h1:VaFXBL0NJpiFBtw4aVJpKHeKULVTcHpD+/G0ibZkcBw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0/go.mod h1:JXkPazkEc/dZTHzOlzv2vT1DlpWSTbSLmu/1KY6Ly0I= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 h1:QFgWzcdmJlgEAwJz/zePYVJQxfoJGRtgIqZfIUFg5oQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0/go.mod h1:ayYHuYU7iNcNtEs1K9k6D/Bju7u1VEHMQm5qQ1n3GtM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.52.0 h1:0l8ynskVvq1dvIn5vJbFMf/a/3TqFpRmCMrruFbzlvk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.52.0/go.mod h1:f/ad5NuHnYz8AOZGuR0cY+l36oSCstdxD73YlIchr6I= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 h1:wbMd4eG/fOhsCa6+IP8uEDvWF5vl7rNoUWmP5f72Tbs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0/go.mod h1:gdIm9TxRk5soClCwuB0FtdXsbqtw0aqPwBEurK9tPkw= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olebedev/when v1.1.0 h1:dlpoRa7huImhNtEx4yl0WYfTHVEWmJmIWd7fEkTHayc= github.com/olebedev/when v1.1.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -85,6 +182,7 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -100,7 +198,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -109,6 +209,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -122,6 +223,7 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -132,6 +234,7 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -144,7 +247,9 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -154,16 +259,27 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237/go.mod h1:LhI4bRmX3rqllzQ+BGneexULkEjBf2gsAfkbeCA8IbU= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto v0.0.0-20250528174236-200df99c418a h1:KXuwdBmgjb4T3l4ZzXhP6HxxFKXD9FcK5/8qfJI4WwU= +google.golang.org/genproto v0.0.0-20250528174236-200df99c418a/go.mod h1:Nlk93rrS2X7rV8hiC2gh2A/AJspZhElz9Oh2KGsjLEY= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/config.go b/server/config.go index 1593290c..c7cfee1f 100644 --- a/server/config.go +++ b/server/config.go @@ -143,8 +143,9 @@ type Config struct { VisitorAuthFailureLimitReplenish time.Duration VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics - BehindProxy bool - ProxyForwardedHeader string + BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address + ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" + ProxyTrustedAddrs []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration diff --git a/server/server.go b/server/server.go index 22581d86..e73976b1 100644 --- a/server/server.go +++ b/server/server.go @@ -1936,8 +1936,8 @@ func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFun // This function will ALWAYS return a visitor, even if an error occurs (e.g. unauthorized), so // that subsequent logging calls still have a visitor context. func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) { - // Read "Authorization" header value, and exit out early if it's not set - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader) + // Read the "Authorization" header value and exit out early if it's not set + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddrs) vip := s.visitor(ip, nil) if s.userManager == nil { return vip, nil @@ -2012,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us if err != nil { return nil, err } - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddrs) go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{ LastAccess: time.Now(), LastOrigin: ip, diff --git a/server/server.yml b/server/server.yml index bb508cb4..30723c08 100644 --- a/server/server.yml +++ b/server/server.yml @@ -95,13 +95,21 @@ # auth-default-access: "read-write" # auth-startup-queries: -# If set, the X-Forwarded-For header is used to determine the visitor IP address +# If set, the X-Forwarded-For header (or whatever is configured) is used to determine the visitor IP address # instead of the remote address of the connection. # -# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate limited +# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate-limited # as if they are one. # +# - behind-proxy defines whether the server is behind a reverse proxy (e.g. nginx, traefik, ...) +# - proxy-forwarded-header defines the header used to determine the visitor IP address. This defaults +# to "X-Forwarded-For", but can be set to any other header, e.g. "X-Real-IP", "X-Client-IP", ... +# - proxy-trusted-addrs defines a list of trusted IP addresses that are stripped out of the +# forwarded header. This is useful if there are multiple trusted proxies involved. +# # behind-proxy: false +# proxy-forwarded-header: "X-Forwarded-For" +# proxy-trusted-addrs: # If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments # are "attachment-cache-dir" and "base-url". diff --git a/server/server_test.go b/server/server_test.go index 71a87162..2342759d 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2200,7 +2200,7 @@ func TestServer_Visitor_XForwardedFor_None(t *testing.T) { c.BehindProxy = true s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11" + r.RemoteAddr = "8.9.10.11:1234" r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty! v, err := s.maybeAuthenticate(r) require.Nil(t, err) @@ -2212,7 +2212,7 @@ func TestServer_Visitor_XForwardedFor_Single(t *testing.T) { c.BehindProxy = true s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11" + r.RemoteAddr = "8.9.10.11:1234" r.Header.Set("X-Forwarded-For", "1.1.1.1") v, err := s.maybeAuthenticate(r) require.Nil(t, err) @@ -2224,7 +2224,7 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) { c.BehindProxy = true s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11" + r.RemoteAddr = "8.9.10.11:1234" r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ") v, err := s.maybeAuthenticate(r) require.Nil(t, err) @@ -2237,7 +2237,7 @@ func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) { c.ProxyForwardedHeader = "X-Client-IP" s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11" + r.RemoteAddr = "8.9.10.11:1234" r.Header.Set("X-Client-IP", "1.2.3.4") v, err := s.maybeAuthenticate(r) require.Nil(t, err) @@ -2333,7 +2333,7 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) { // "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor subscriber1Fn := func(r *http.Request) { - r.RemoteAddr = "1.2.3.4" + r.RemoteAddr = "1.2.3.4:1234" } rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn) require.Equal(t, 200, rr.Code) @@ -2342,7 +2342,7 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) { // "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name) subscriber2Fn := func(r *http.Request) { - r.RemoteAddr = "8.7.7.1" + r.RemoteAddr = "8.7.7.1:1234" } rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, subscriber2Fn) require.Equal(t, 200, rr.Code) @@ -2385,7 +2385,7 @@ func TestServer_SubscriberRateLimiting_NotWrongTopic(t *testing.T) { s := newTestServer(t, c) subscriberFn := func(r *http.Request) { - r.RemoteAddr = "1.2.3.4" + r.RemoteAddr = "1.2.3.4:1234" } rr := request(t, s, "GET", "/alerts,upAAAAAAAAAAAA,upBBBBBBBBBBBB/json?poll=1", "", nil, subscriberFn) require.Equal(t, 200, rr.Code) @@ -2405,7 +2405,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) { // Registering visitor 1.2.3.4 to topic has no effect rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) { - r.RemoteAddr = "1.2.3.4" + r.RemoteAddr = "1.2.3.4:1234" }) require.Equal(t, 200, rr.Code) require.Equal(t, "", rr.Body.String()) @@ -2413,7 +2413,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) { // Registering visitor 8.7.7.1 to topic has no effect rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) { - r.RemoteAddr = "8.7.7.1" + r.RemoteAddr = "8.7.7.1:1234" }) require.Equal(t, 200, rr.Code) require.Equal(t, "", rr.Body.String()) @@ -2439,7 +2439,7 @@ func TestServer_SubscriberRateLimiting_UP_Only(t *testing.T) { // "Register" 5 different UnifiedPush visitors for i := 0; i < 5; i++ { subscriberFn := func(r *http.Request) { - r.RemoteAddr = fmt.Sprintf("1.2.3.%d", i+1) + r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1) } rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, subscriberFn) require.Equal(t, 200, rr.Code) @@ -2463,7 +2463,7 @@ func TestServer_Matrix_SubscriberRateLimiting_UP_Only(t *testing.T) { // "Register" 5 different UnifiedPush visitors for i := 0; i < 5; i++ { rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, func(r *http.Request) { - r.RemoteAddr = fmt.Sprintf("1.2.3.%d", i+1) + r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1) }) require.Equal(t, 200, rr.Code) } @@ -2490,7 +2490,7 @@ func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) { // "Register" rate visitor subscriberFn := func(r *http.Request) { - r.RemoteAddr = "1.2.3.4" + r.RemoteAddr = "1.2.3.4:1234" } rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn) require.Equal(t, 200, rr.Code) @@ -2529,7 +2529,7 @@ func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *t // - "up123456789012": Allowed, because no ACLs and nobody owns the topic // - "announcements": NOT allowed, because it has read-only permissions for everyone rr := request(t, s, "GET", "/up123456789012,announcements/json?poll=1", "", nil, func(r *http.Request) { - r.RemoteAddr = "1.2.3.4" + r.RemoteAddr = "1.2.3.4:1234" }) require.Equal(t, 200, rr.Code) require.Equal(t, "1.2.3.4", s.topics["up123456789012"].rateVisitor.ip.String()) @@ -2971,7 +2971,7 @@ func request(t *testing.T, s *Server, method, url, body string, headers map[stri if err != nil { t.Fatal(err) } - r.RemoteAddr = "9.9.9.9" // Used for tests + r.RemoteAddr = "9.9.9.9:1234" // Used for tests for k, v := range headers { r.Header.Set(k, v) } diff --git a/server/util.go b/server/util.go index c4ee7a79..936e74f8 100644 --- a/server/util.go +++ b/server/util.go @@ -10,6 +10,7 @@ import ( "net/http" "net/netip" "regexp" + "slices" "strings" ) @@ -73,34 +74,43 @@ func readQueryParam(r *http.Request, names ...string) string { return "" } -func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string) netip.Addr { - remoteAddr := r.RemoteAddr - addrPort, err := netip.ParseAddrPort(remoteAddr) - ip := addrPort.Addr() +func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddrs []string) netip.Addr { + if behindProxy && proxyForwardedHeader != "" { + if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddrs); err == nil { + return addr + } + // Fall back to the remote address if the header is not found or invalid + } + addrPort, err := netip.ParseAddrPort(r.RemoteAddr) if err != nil { - // This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified - ip, err = netip.ParseAddr(remoteAddr) - if err != nil { - ip = netip.IPv4Unspecified() - if remoteAddr != "@" && !behindProxy { // RemoteAddr is @ when unix socket is used - logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", remoteAddr) - } - } + logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", r.RemoteAddr) + return netip.IPv4Unspecified() } - if behindProxy && strings.TrimSpace(r.Header.Get(proxyForwardedHeader)) != "" { - // X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, - // only the right-most address can be trusted (as this is the one added by our proxy server). - // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. - ips := util.SplitNoEmpty(r.Header.Get(proxyForwardedHeader), ",") - realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr))) - if err != nil { - logr(r).Err(err).Error("invalid IP address %s received in %s header", ip, proxyForwardedHeader) - // Fall back to the regular remote address if X-Forwarded-For is damaged - } else { - ip = realIP - } + return addrPort.Addr() +} + +// extractIPAddressFromHeader extracts the last IP address from the specified header. +// +// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, +// only the right-most address can be trusted (as this is the one added by our proxy server). +// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. +func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddrs []string) (netip.Addr, error) { + value := strings.TrimSpace(r.Header.Get(forwardedHeader)) + if value == "" { + return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) } - return ip + addrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace) + clientAddrs := util.Filter(addrs, func(addr string) bool { + return !slices.Contains(trustedAddrs, addr) + }) + if len(clientAddrs) == 0 { + return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value) + } + clientAddr, err := netip.ParseAddr(clientAddrs[len(clientAddrs)-1]) + if err != nil { + return netip.IPv4Unspecified(), fmt.Errorf("invalid IP address %s received in %s header: %s: %w", clientAddr, forwardedHeader, value, err) + } + return clientAddr, nil } func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) { diff --git a/server/util_test.go b/server/util_test.go index 6555a81b..946f0d42 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -88,3 +88,29 @@ func TestMaybeDecodeHeaders(t *testing.T) { r.Header.Set("X-Priority", "5") // ntfy priority header require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p")) } + +func TestExtractIPAddress(t *testing.T) { + r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) + r.RemoteAddr = "10.0.0.1:1234" + r.Header.Set("X-Forwarded-For", " 1.2.3.4 , 5.6.7.8") + r.Header.Set("X-Client-IP", "9.10.11.12") + r.Header.Set("X-Real-IP", "13.14.15.16, 1.1.1.1") + + trustedProxies := []string{"1.1.1.1"} + + require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) + require.Equal(t, "9.10.11.12", extractIPAddress(r, true, "X-Client-IP", trustedProxies).String()) + require.Equal(t, "13.14.15.16", extractIPAddress(r, true, "X-Real-IP", trustedProxies).String()) + require.Equal(t, "10.0.0.1", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String()) +} + +func TestExtractIPAddress_UnixSocket(t *testing.T) { + r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) + r.RemoteAddr = "@" + r.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8, 1.1.1.1") + + trustedProxies := []string{"1.1.1.1"} + + require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) + require.Equal(t, "0.0.0.0", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String()) +} diff --git a/util/util.go b/util/util.go index 39bf8798..73b227af 100644 --- a/util/util.go +++ b/util/util.go @@ -17,10 +17,9 @@ import ( "sync" "time" - "golang.org/x/time/rate" - "github.com/gabriel-vasile/mimetype" "golang.org/x/term" + "golang.org/x/time/rate" ) const ( @@ -99,12 +98,26 @@ func SplitKV(s string, sep string) (key string, value string) { return "", strings.TrimSpace(kv[0]) } -// LastString returns the last string in a slice, or def if s is empty -func LastString(s []string, def string) string { - if len(s) == 0 { - return def +// Map applies a function to each element of a slice and returns a new slice with the results +// Example: Map([]int{1, 2, 3}, func(i int) int { return i * 2 }) -> []int{2, 4, 6} +func Map[T any, U any](slice []T, f func(T) U) []U { + result := make([]U, len(slice)) + for i, v := range slice { + result[i] = f(v) } - return s[len(s)-1] + return result +} + +// Filter returns a new slice containing only the elements of the original slice for which the +// given function returns true. +func Filter[T any](slice []T, f func(T) bool) []T { + result := make([]T, 0) + for _, v := range slice { + if f(v) { + result = append(result, v) + } + } + return result } // RandomString returns a random string with a given length diff --git a/util/util_test.go b/util/util_test.go index 9ddec58d..ae855147 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -167,11 +167,6 @@ func TestSplitKV(t *testing.T) { require.Equal(t, "value=with=separator", value) } -func TestLastString(t *testing.T) { - require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default")) - require.Equal(t, "default", LastString([]string{}, "default")) -} - func TestQuoteCommand(t *testing.T) { require.Equal(t, `ls -al "Document Folder"`, QuoteCommand([]string{"ls", "-al", "Document Folder"})) require.Equal(t, `rsync -av /home/phil/ root@example.com:/home/phil/`, QuoteCommand([]string{"rsync", "-av", "/home/phil/", "root@example.com:/home/phil/"})) diff --git a/web/package-lock.json b/web/package-lock.json index 3f428c2e..34e91d7c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -74,9 +74,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", + "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", "dev": true, "license": "MIT", "engines": { @@ -84,22 +84,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -122,13 +122,13 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", + "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -138,13 +138,13 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", - "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -252,15 +252,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -386,26 +386,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", + "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -629,9 +629,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", - "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz", + "integrity": "sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -717,9 +717,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", - "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", + "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", "dev": true, "license": "MIT", "dependencies": { @@ -1065,15 +1065,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", - "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", + "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { @@ -1233,9 +1233,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", - "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.4.tgz", + "integrity": "sha512-Glp/0n8xuj+E1588otw5rjJkTXfzW7FjH3IIUrfqiZOPQCd2vbg8e+DQE8jK9g4V5/zrxFW+D9WM9gboRPELpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1529,9 +1529,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz", + "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1552,16 +1552,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1570,9 +1570,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1741,9 +1741,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -1758,9 +1758,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -1775,9 +1775,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -1792,9 +1792,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -1809,9 +1809,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -1826,9 +1826,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -1843,9 +1843,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -1860,9 +1860,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -1877,9 +1877,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -1894,9 +1894,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -1911,9 +1911,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -1928,9 +1928,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -1945,9 +1945,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -1962,9 +1962,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -1979,9 +1979,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -1996,9 +1996,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -2013,9 +2013,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -2030,9 +2030,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -2047,9 +2047,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -2064,9 +2064,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -2081,9 +2081,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -2098,9 +2098,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -2115,9 +2115,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -2132,9 +2132,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -2149,9 +2149,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -3106,9 +3106,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", - "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", + "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "license": "MIT", "peer": true, "dependencies": { @@ -3569,9 +3569,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "dev": true, "funding": [ { @@ -3589,8 +3589,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -3668,9 +3668,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001720", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", + "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==", "dev": true, "funding": [ { @@ -4105,9 +4105,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.157", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz", - "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", + "version": "1.5.161", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz", + "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==", "dev": true, "license": "ISC" }, @@ -4137,9 +4137,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -4170,7 +4170,9 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", @@ -4185,6 +4187,7 @@ "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -4311,9 +4314,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4324,31 +4327,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { @@ -4884,9 +4887,9 @@ } }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5796,6 +5799,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -6879,9 +6895,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", "dev": true, "funding": [ { @@ -6899,7 +6915,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7804,6 +7820,20 @@ "stacktrace-gps": "^3.0.4" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -8060,9 +8090,9 @@ } }, "node_modules/terser": { - "version": "5.39.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", - "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz", + "integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8095,9 +8125,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { From 7a33e169458e1a79af88c8e389690ee3bc70013a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 31 May 2025 23:07:40 -0400 Subject: [PATCH 126/378] Cleanup, examples --- cmd/serve.go | 10 ++++++---- docs/config.md | 51 +++++++++++++++++++++++++++++++++++++++++------- server/config.go | 2 +- server/server.go | 4 ++-- server/util.go | 8 ++++---- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 3745ebce..576e72f0 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -89,8 +89,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "if set, use specified header to determine visitor IP address instead of XFF (for rate limiting)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addrs", Aliases: []string{"proxy_trusted_addrs"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRS"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addresses", Aliases: []string{"proxy_trusted_addresses"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRESSES"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}), @@ -193,7 +193,7 @@ func execServe(c *cli.Context) error { visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish") behindProxy := c.Bool("behind-proxy") proxyForwardedHeader := c.String("proxy-forwarded-header") - proxyTrustedAddrs := util.SplitNoEmpty(c.String("proxy-trusted-addrs"), ",") + proxyTrustedAddresses := util.SplitNoEmpty(c.String("proxy-trusted-addresses"), ",") stripeSecretKey := c.String("stripe-secret-key") stripeWebhookKey := c.String("stripe-webhook-key") billingContact := c.String("billing-contact") @@ -322,6 +322,8 @@ func execServe(c *cli.Context) error { } } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") + } else if behindProxy && proxyForwardedHeader == "" { + return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set") } // Backwards compatibility @@ -421,7 +423,7 @@ func execServe(c *cli.Context) error { conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.BehindProxy = behindProxy conf.ProxyForwardedHeader = proxyForwardedHeader - conf.ProxyTrustedAddrs = proxyTrustedAddrs + conf.ProxyTrustedAddresses = proxyTrustedAddresses conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact diff --git a/docs/config.md b/docs/config.md index 3c441fc4..3b89f247 100644 --- a/docs/config.md +++ b/docs/config.md @@ -554,15 +554,50 @@ using Let's Encrypt using certbot, or simply because you'd like to share the por Whatever your reasons may be, there are a few things to consider. If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the -[rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary identifier for a visitor, -as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will -be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address. If your proxy or CDN provider uses a custom header to securely pass the source IP/Client IP to your application, you can specify that header instead of using the XFF. Using the custom header (unique per provide/cdn/proxy), will disable the use of the XFF header. +[rate limiting](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`) +as the primary identifier for a visitor, as opposed to the remote IP address. -=== "/etc/ntfy/server.yml" +If the `behind-proxy` flag is not set, all visitors will be counted as one, because from the perspective of the +ntfy server, they all share the proxy's IP address. + +Relevant flags to consider: + +* `behind-proxy`: if set, ntfy will use the `proxy-forwarded-header` to identify visitors (default: `false`) +* `proxy-forwarded-header`: the header to use to identify visitors (default: `X-Forwarded-For`) +* `proxy-trusted-addresses`: a comma-separated list of IP addresses that are removed from the forwarded header + to determine the real IP address (default: empty) + +=== "/etc/ntfy/server.yml (behind a proxy)" ``` yaml - # Tell ntfy to use "X-Forwarded-For" to identify visitors + # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting + # + # Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set, + # the visitor IP will be 1.2.3.4 (right-most address). + # behind-proxy: true - proxy-client-ip-header: "X-Client-IP" + ``` + +=== "/etc/ntfy/server.yml (with custom header)" + ``` yaml + # Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting + # + # Example: If "X-Client-IP: 9.9.9.9" is set, + # the visitor IP will be 9.9.9.9. + # + behind-proxy: true + proxy-forwarded-header: "X-Client-IP" + ``` + +=== "/etc/ntfy/server.yml (multiple proxies)" + ``` yaml + # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting, + # and to strip the IP addresses of the proxies 1.2.3.4 and 1.2.3.5 + # + # Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set, + # the visitor IP will be 9.9.9.9 (right-most unknown address). + # + behind-proxy: true + proxy-trusted-addresses: "1.2.3.4, 1.2.3.5" ``` ### TLS/SSL @@ -1391,7 +1426,9 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) | | `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | | `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | -| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | +| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) | +| `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) | +| `proxy-trusted-addresses` | `NTFY_PROXY_TRUSTED_ADDRESSES` | *comma-separated list of IPs* | - | Comma-separated list of trusted IP addresses to remove from forwarded header | | `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | | `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | | `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | diff --git a/server/config.go b/server/config.go index c7cfee1f..75e6d488 100644 --- a/server/config.go +++ b/server/config.go @@ -145,7 +145,7 @@ type Config struct { VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" - ProxyTrustedAddrs []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true + ProxyTrustedAddresses []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration diff --git a/server/server.go b/server/server.go index e73976b1..e1126757 100644 --- a/server/server.go +++ b/server/server.go @@ -1937,7 +1937,7 @@ func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFun // that subsequent logging calls still have a visitor context. func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) { // Read the "Authorization" header value and exit out early if it's not set - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddrs) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses) vip := s.visitor(ip, nil) if s.userManager == nil { return vip, nil @@ -2012,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us if err != nil { return nil, err } - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddrs) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses) go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{ LastAccess: time.Now(), LastOrigin: ip, diff --git a/server/util.go b/server/util.go index 936e74f8..34194681 100644 --- a/server/util.go +++ b/server/util.go @@ -74,9 +74,9 @@ func readQueryParam(r *http.Request, names ...string) string { return "" } -func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddrs []string) netip.Addr { +func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddresses []string) netip.Addr { if behindProxy && proxyForwardedHeader != "" { - if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddrs); err == nil { + if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddresses); err == nil { return addr } // Fall back to the remote address if the header is not found or invalid @@ -94,14 +94,14 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, // only the right-most address can be trusted (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. -func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddrs []string) (netip.Addr, error) { +func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { value := strings.TrimSpace(r.Header.Get(forwardedHeader)) if value == "" { return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) } addrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace) clientAddrs := util.Filter(addrs, func(addr string) bool { - return !slices.Contains(trustedAddrs, addr) + return !slices.Contains(trustedAddresses, addr) }) if len(clientAddrs) == 0 { return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value) From db4ac158e360e2bd0d52ae48e58e927d46032f4c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 31 May 2025 23:09:51 -0400 Subject: [PATCH 127/378] Section --- docs/config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.md b/docs/config.md index 3b89f247..4e8fbab5 100644 --- a/docs/config.md +++ b/docs/config.md @@ -553,6 +553,7 @@ It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache), using Let's Encrypt using certbot, or simply because you'd like to share the ports (80/443) with other services. Whatever your reasons may be, there are a few things to consider. +### IP-based rate limiting If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the [rate limiting](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`) as the primary identifier for a visitor, as opposed to the remote IP address. From bbfaf2fc4d2367769749ac5f723f9ff3dc520d98 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 1 Jun 2025 09:57:39 -0400 Subject: [PATCH 128/378] Add Forwarded header parsing --- docs/config.md | 20 +++++++++++++++--- docs/releases.md | 6 ++++++ server/server.yml | 7 +++++-- server/util.go | 49 +++++++++++++++++++++++++++------------------ server/util_test.go | 4 ++++ 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/docs/config.md b/docs/config.md index 4e8fbab5..1dd0ee5e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -563,8 +563,11 @@ ntfy server, they all share the proxy's IP address. Relevant flags to consider: -* `behind-proxy`: if set, ntfy will use the `proxy-forwarded-header` to identify visitors (default: `false`) -* `proxy-forwarded-header`: the header to use to identify visitors (default: `X-Forwarded-For`) +* `behind-proxy` makes it so that the real visitor IP address is extracted from the header defined in `proxy-forwarded-header`. + Without this, the remote address of the incoming connection is used (default: `false`). +* `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`), + a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style + header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`). * `proxy-trusted-addresses`: a comma-separated list of IP addresses that are removed from the forwarded header to determine the real IP address (default: empty) @@ -578,7 +581,7 @@ Relevant flags to consider: behind-proxy: true ``` -=== "/etc/ntfy/server.yml (with custom header)" +=== "/etc/ntfy/server.yml (X-Client-IP header)" ``` yaml # Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting # @@ -589,6 +592,17 @@ Relevant flags to consider: proxy-forwarded-header: "X-Client-IP" ``` +=== "/etc/ntfy/server.yml (Forwarded header)" + ``` yaml + # Tell ntfy to use "Forwarded" header (RFC 7239) to identify visitors for rate limiting + # + # Example: If "Forwarded: for=1.2.3.4;by=proxy.example.com, for=9.9.9.9" is set, + # the visitor IP will be 9.9.9.9. + # + behind-proxy: true + proxy-forwarded-header: "Forwarded" + ``` + === "/etc/ntfy/server.yml (multiple proxies)" ``` yaml # Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting, diff --git a/docs/releases.md b/docs/releases.md index a1035310..8bf1cc4e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1433,6 +1433,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet +### ntfy server v2.13.0 (UNRELEASED) + +**Features:** + +* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-addresses` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) + ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** diff --git a/server/server.yml b/server/server.yml index 30723c08..c044efc5 100644 --- a/server/server.yml +++ b/server/server.yml @@ -95,8 +95,8 @@ # auth-default-access: "read-write" # auth-startup-queries: -# If set, the X-Forwarded-For header (or whatever is configured) is used to determine the visitor IP address -# instead of the remote address of the connection. +# If set, the X-Forwarded-For header (or whatever is configured in proxy-forwarded-header) is used to determine +# the visitor IP address instead of the remote address of the connection. # # WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate-limited # as if they are one. @@ -107,6 +107,9 @@ # - proxy-trusted-addrs defines a list of trusted IP addresses that are stripped out of the # forwarded header. This is useful if there are multiple trusted proxies involved. # +# The parsing of the forwarded header is very lenient. Here are some examples: +# - X-Forwarded-For: 1.2.3.4, 5.6.7.8 (-> +# # behind-proxy: false # proxy-forwarded-header: "X-Forwarded-For" # proxy-trusted-addrs: diff --git a/server/util.go b/server/util.go index 34194681..006dcce1 100644 --- a/server/util.go +++ b/server/util.go @@ -15,8 +15,13 @@ import ( ) var ( - mimeDecoder mime.WordDecoder + mimeDecoder mime.WordDecoder + + // priorityHeaderIgnoreRegex matches specific patterns of the "Priority" header (RFC 9218), so that it can be ignored priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`) + + // forwardedHeaderRegex parses IPv4 addresses from the "Forwarded" header (RFC 7239) + forwardedHeaderRegex = regexp.MustCompile(`(?i)\bfor="?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"?`) ) func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool { @@ -35,15 +40,11 @@ func toBool(value string) bool { return value == "1" || value == "yes" || value == "true" } -func readCommaSeparatedParam(r *http.Request, names ...string) (params []string) { - paramStr := readParam(r, names...) - if paramStr != "" { - params = make([]string, 0) - for _, s := range util.SplitNoEmpty(paramStr, ",") { - params = append(params, strings.TrimSpace(s)) - } +func readCommaSeparatedParam(r *http.Request, names ...string) []string { + if paramStr := readParam(r, names...); paramStr != "" { + return util.Map(util.SplitNoEmpty(paramStr, ","), strings.TrimSpace) } - return params + return []string{} } func readParam(r *http.Request, names ...string) string { @@ -95,22 +96,30 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // only the right-most address can be trusted (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { - value := strings.TrimSpace(r.Header.Get(forwardedHeader)) + value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader))) if value == "" { return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) } - addrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace) - clientAddrs := util.Filter(addrs, func(addr string) bool { - return !slices.Contains(trustedAddresses, addr) + // Extract valid addresses + addrsStrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace) + var validAddrs []netip.Addr + for _, addrStr := range addrsStrs { + if addr, err := netip.ParseAddr(addrStr); err == nil { + validAddrs = append(validAddrs, addr) + } else if m := forwardedHeaderRegex.FindStringSubmatch(addrStr); len(m) == 2 { + if addr, err := netip.ParseAddr(m[1]); err == nil { + validAddrs = append(validAddrs, addr) + } + } + } + // Filter out proxy addresses + clientAddrs := util.Filter(validAddrs, func(addr netip.Addr) bool { + return !slices.Contains(trustedAddresses, addr.String()) }) if len(clientAddrs) == 0 { return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value) } - clientAddr, err := netip.ParseAddr(clientAddrs[len(clientAddrs)-1]) - if err != nil { - return netip.IPv4Unspecified(), fmt.Errorf("invalid IP address %s received in %s header: %s: %w", clientAddr, forwardedHeader, value, err) - } - return clientAddr, nil + return clientAddrs[len(clientAddrs)-1], nil } func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) { @@ -143,7 +152,7 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) { // maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=", // or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader -// to ignore new HTTP "Priority" header. +// to ignore the new HTTP "Priority" header. func maybeDecodeHeader(name, value string) string { decoded, err := mimeDecoder.DecodeHeader(value) if err != nil { @@ -152,7 +161,7 @@ func maybeDecodeHeader(name, value string) string { return maybeIgnoreSpecialHeader(name, decoded) } -// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority) +// maybeIgnoreSpecialHeader ignores the new HTTP "Priority" header (RFC 9218, see https://datatracker.ietf.org/doc/html/rfc9218) // // Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy), // so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored. diff --git a/server/util_test.go b/server/util_test.go index 946f0d42..4b60e1a1 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -95,12 +95,14 @@ func TestExtractIPAddress(t *testing.T) { r.Header.Set("X-Forwarded-For", " 1.2.3.4 , 5.6.7.8") r.Header.Set("X-Client-IP", "9.10.11.12") r.Header.Set("X-Real-IP", "13.14.15.16, 1.1.1.1") + r.Header.Set("Forwarded", "for=17.18.19.20;by=proxy.example.com, by=2.2.2.2;for=1.1.1.1") trustedProxies := []string{"1.1.1.1"} require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) require.Equal(t, "9.10.11.12", extractIPAddress(r, true, "X-Client-IP", trustedProxies).String()) require.Equal(t, "13.14.15.16", extractIPAddress(r, true, "X-Real-IP", trustedProxies).String()) + require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String()) require.Equal(t, "10.0.0.1", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String()) } @@ -108,9 +110,11 @@ func TestExtractIPAddress_UnixSocket(t *testing.T) { r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) r.RemoteAddr = "@" r.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8, 1.1.1.1") + r.Header.Set("Forwarded", "by=bla.example.com;for=17.18.19.20") trustedProxies := []string{"1.1.1.1"} require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) + require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String()) require.Equal(t, "0.0.0.0", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String()) } From d3f7aa7008971ef11e905d46af54796ba4d13417 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 1 Jun 2025 10:12:06 -0400 Subject: [PATCH 129/378] Self-review --- docs/config.md | 5 +++-- server/server.yml | 19 +++++++++---------- server/server_test.go | 14 ++++++++++++++ server/util.go | 11 +++++++++-- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/config.md b/docs/config.md index 1dd0ee5e..1687c2ec 100644 --- a/docs/config.md +++ b/docs/config.md @@ -568,8 +568,9 @@ Relevant flags to consider: * `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`), a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`). -* `proxy-trusted-addresses`: a comma-separated list of IP addresses that are removed from the forwarded header - to determine the real IP address (default: empty) +* `proxy-trusted-addresses` is a comma-separated list of IP addresses that are removed from the forwarded header + to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to + the forwarded header (default: empty). === "/etc/ntfy/server.yml (behind a proxy)" ``` yaml diff --git a/server/server.yml b/server/server.yml index c044efc5..669805b8 100644 --- a/server/server.yml +++ b/server/server.yml @@ -101,18 +101,17 @@ # WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate-limited # as if they are one. # -# - behind-proxy defines whether the server is behind a reverse proxy (e.g. nginx, traefik, ...) -# - proxy-forwarded-header defines the header used to determine the visitor IP address. This defaults -# to "X-Forwarded-For", but can be set to any other header, e.g. "X-Real-IP", "X-Client-IP", ... -# - proxy-trusted-addrs defines a list of trusted IP addresses that are stripped out of the -# forwarded header. This is useful if there are multiple trusted proxies involved. -# -# The parsing of the forwarded header is very lenient. Here are some examples: -# - X-Forwarded-For: 1.2.3.4, 5.6.7.8 (-> +# - behind-proxy makes it so that the real visitor IP address is extracted from the header defined in +# proxy-forwarded-header. Without this, the remote address of the incoming connection is used. +# - proxy-forwarded-header is the header to use to identify visitors. It may be a single IP address (e.g. 1.2.3.4), +# a comma-separated list of IP addresses (e.g. "1.2.3.4, 5.6.7.8"), or an RFC 7239-style header (e.g. "for=1.2.3.4;by=proxy.example.com, for=5.6.7.8"). +# - proxy-trusted-addresses is a comma-separated list of IP addresses that are removed from the forwarded header +# to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to +# the forwarded header. # # behind-proxy: false # proxy-forwarded-header: "X-Forwarded-For" -# proxy-trusted-addrs: +# proxy-trusted-addresses: # If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments # are "attachment-cache-dir" and "base-url". @@ -149,7 +148,7 @@ # - smtp-server-domain is the e-mail domain, e.g. ntfy.sh # - smtp-server-addr-prefix is an optional prefix for the e-mail addresses to prevent spam. If set to "ntfy-", # for instance, only e-mails to ntfy-$topic@ntfy.sh will be accepted. If this is not set, all emails to -# $topic@ntfy.sh will be accepted (which may obviously be a spam problem). +# $topic@ntfy.sh will be accepted (which may be a spam problem). # # smtp-server-listen: # smtp-server-domain: diff --git a/server/server_test.go b/server/server_test.go index 2342759d..be0610ac 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2244,6 +2244,20 @@ func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) { require.Equal(t, "1.2.3.4", v.ip.String()) } +func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "Forwarded" + c.ProxyTrustedAddresses = []string{"1.2.3.4"} + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11:1234" + r.Header.Set("Forwarded", " for=5.6.7.8, by=example.com;for=1.2.3.4") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "5.6.7.8", v.ip.String()) +} + func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { t.Parallel() count := 50000 diff --git a/server/util.go b/server/util.go index 006dcce1..3db9e322 100644 --- a/server/util.go +++ b/server/util.go @@ -75,6 +75,8 @@ func readQueryParam(r *http.Request, names ...string) string { return "" } +// extractIPAddress extracts the IP address of the visitor from the request, +// either from the TCP socket or from a proxy header. func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddresses []string) netip.Addr { if behindProxy && proxyForwardedHeader != "" { if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddresses); err == nil { @@ -92,8 +94,13 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // extractIPAddressFromHeader extracts the last IP address from the specified header. // -// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy, -// only the right-most address can be trusted (as this is the one added by our proxy server). +// It supports multiple formats: +// - single IP address +// - comma-separated list +// - RFC 7239-style list (Forwarded header) +// +// If there are multiple addresses, we first remove the trusted IP addresses from the list, and +// then take the right-most address in the list (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader))) From 9d5891963a58ca9e97f47b80700618cdc72f2224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 2 Jun 2025 22:02:24 +0200 Subject: [PATCH 130/378] Translated using Weblate (Estonian) Currently translated at 44.1% (179 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 80 ++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index 7d2a5a85..eb0295e7 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -99,5 +99,83 @@ "account_tokens_table_create_token_button": "Loo ligipääsuks vajalik tunnusluba", "account_tokens_dialog_title_create": "Loo ligipääsuks vajalik tunnusluba", "account_tokens_dialog_title_edit": "Muuda ligipääsuks vajalikku tunnusluba", - "account_tokens_dialog_title_delete": "Kustuta ligipääsuks vajalik tunnusluba" + "account_tokens_dialog_title_delete": "Kustuta ligipääsuks vajalik tunnusluba", + "subscribe_dialog_login_password_label": "Salasõna", + "publish_dialog_filename_label": "Failinimi", + "prefs_reservations_table_access_header": "Ligipääs", + "publish_dialog_chip_click_label": "Klõpsi võrguaadressi", + "subscribe_dialog_subscribe_button_cancel": "Katkesta", + "publish_dialog_delay_label": "Viivitus", + "account_basics_password_title": "Salasõna", + "account_upgrade_dialog_button_cancel": "Katkesta", + "notifications_example": "Näide", + "account_usage_title": "Kasutus", + "account_basics_title": "Kasutajakonto", + "prefs_reservations_table_topic_header": "Teema", + "account_delete_dialog_button_cancel": "Katkesta", + "account_delete_dialog_label": "Salasõna", + "publish_dialog_message_label": "Sõnum", + "account_basics_phone_numbers_dialog_channel_call": "Kõne", + "prefs_users_dialog_password_label": "Salasõna", + "subscribe_dialog_subscribe_button_subscribe": "Telli", + "publish_dialog_priority_label": "Prioriteet", + "subscribe_dialog_login_button_login": "Logi sisse", + "subscribe_dialog_error_user_anonymous": "anonüümne", + "prefs_appearance_theme_title": "Kujundus", + "publish_dialog_button_cancel": "Katkesta", + "account_usage_unlimited": "Piiramatu", + "prefs_notifications_delete_after_never": "Mitte kunagi", + "account_upgrade_dialog_interval_monthly": "Iga kuu", + "account_upgrade_dialog_tier_price_per_month": "kuu", + "prefs_notifications_web_push_disabled": "Pole kasutusel", + "prefs_appearance_title": "Välimus", + "prefs_appearance_language_title": "Keel", + "prefs_reservations_dialog_topic_label": "Teema", + "publish_dialog_priority_min": "Väikseim tähtsus", + "notifications_actions_failed_notification": "Ebaõnnestunud toiming", + "publish_dialog_title_label": "Pealkiri", + "publish_dialog_tags_label": "Sildid", + "publish_dialog_email_label": "E-post", + "display_name_dialog_placeholder": "Kuvatav nimi", + "publish_dialog_title_no_topic": "Avalda teavitus", + "publish_dialog_progress_uploading": "Laadin üles…", + "publish_dialog_message_published": "Teavitus on saadetud", + "publish_dialog_emoji_picker_show": "Vali emoji", + "publish_dialog_priority_low": "Vähetähtis", + "publish_dialog_priority_default": "Vaikimisi tähtsus", + "publish_dialog_priority_high": "Oluline", + "publish_dialog_priority_max": "Väga oluline", + "publish_dialog_base_url_label": "Teenuse võrguaadress", + "publish_dialog_topic_label": "Teema nimi", + "publish_dialog_topic_reset": "Lähtesta teema", + "publish_dialog_click_label": "Klõpsi võrguaadressi", + "publish_dialog_call_label": "Telefonikõne", + "publish_dialog_button_send": "Saada", + "publish_dialog_attach_label": "Manuse võrguaadress", + "publish_dialog_filename_placeholder": "Manuse failinimi", + "publish_dialog_other_features": "Lisavõimalused:", + "publish_dialog_chip_call_label": "Telefonikõne", + "publish_dialog_chip_delay_label": "Viivita saatmisega", + "publish_dialog_chip_topic_label": "Muuda teemat", + "publish_dialog_button_cancel_sending": "Katkesta saatmine", + "account_basics_username_title": "Kasutajanimi", + "account_basics_phone_numbers_dialog_channel_sms": "Tekstisõnum", + "account_basics_tier_admin": "Peakasutaja", + "account_basics_tier_basic": "Baasteenus", + "account_basics_tier_free": "Tasuta", + "account_basics_tier_interval_monthly": "kord kuus", + "account_basics_tier_interval_yearly": "kord aastas", + "account_basics_tier_change_button": "Muuda", + "account_upgrade_dialog_interval_yearly": "Kord aastas", + "account_upgrade_dialog_tier_selected_label": "Valitud", + "account_upgrade_dialog_tier_current_label": "Praegune", + "account_tokens_dialog_button_cancel": "Katkesta", + "prefs_notifications_title": "Teavitused", + "prefs_users_table_user_header": "Kasutaja", + "prefs_reservations_dialog_access_label": "Ligipääs", + "priority_min": "min", + "priority_low": "madal", + "priority_default": "vaikimisi", + "priority_high": "kõrge", + "priority_max": "kõrgeim" } From 7b470a7f6fe0faad76e208b4a544cace913ef461 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 7 Jun 2025 06:45:43 -0400 Subject: [PATCH 131/378] Sponsors --- README.md | 39 +++++++++++++++++++--------------- assets/sponsors/magicbell.png | Bin 0 -> 12231 bytes 2 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 assets/sponsors/magicbell.png diff --git a/README.md b/README.md index 6255d5dd..07d983f6 100644 --- a/README.md +++ b/README.md @@ -56,20 +56,17 @@ For announcements of new releases and cutting-edge beta versions, please subscri topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas, join Discord/Matrix (I'll eventually make a testing channel in Google Play). -## Contributing -I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out -on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) -for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in -[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/). - - -Translation status - - ## Sponsors -I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), -and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer -account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks who have sponsored ntfy in the past, or are still sponsoring ntfy: +If you'd like to support the ntfy maintainers, please consider donating to [GitHub Sponsors](https://github.com/sponsors/binwiederhier) or +and [Liberapay](https://liberapay.com/ntfy). We would be humbled if you helped carry the server and developer +account costs. Even small donations are very much appreciated. + +Thank you to our commercial sponsors, who help keep the service running and the development going: + +
+ + +And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy: @@ -210,13 +207,21 @@ account costs. Even small donations are very much appreciated. A big fat **Thank -I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/), -and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project: +## Contributing +I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out +on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) +for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in +[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/). - + +Translation status + ## Code of Conduct -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, +color, religion, or sexual identity and orientation. **We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.** diff --git a/assets/sponsors/magicbell.png b/assets/sponsors/magicbell.png new file mode 100644 index 0000000000000000000000000000000000000000..24e8b9dd025ece2e2dc69c48641a4cd31623bb46 GIT binary patch literal 12231 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 Xd$hd{gfq`P|526X)ZsNU7NP$iCU~qS literal 0 HcmV?d00001 From 30301c8a7ff9e54ae505daf73a7f1571e7fefae3 Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Sat, 7 Jun 2025 06:49:22 -0400 Subject: [PATCH 132/378] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 07d983f6..61591ca6 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,9 @@ account costs. Even small donations are very much appreciated. Thank you to our commercial sponsors, who help keep the service running and the development going: -
- + + + And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy: From 86bec660bf70753b9265bc822063cbbbbc92d9f3 Mon Sep 17 00:00:00 2001 From: lazar Date: Sun, 8 Jun 2025 00:27:42 +0200 Subject: [PATCH 133/378] Translated using Weblate (Romanian) Currently translated at 60.2% (244 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/ --- web/public/static/langs/ro.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index df000eca..c309932c 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -228,5 +228,19 @@ "account_basics_password_dialog_new_password_label": "Parola nouă", "account_basics_password_title": "Parolă", "account_basics_tier_description": "Nivelul de putere al contului", - "account_basics_tier_free": "Gratuit" + "account_basics_tier_free": "Gratuit", + "account_delete_description": "Șterge definitiv contul tău", + "account_usage_messages_title": "Mesaje publicate", + "account_basics_tier_manage_billing_button": "Gestionare facturare", + "account_usage_emails_title": "Emailuri trimise", + "account_usage_calls_title": "Apeluri telefonice efectuate", + "account_usage_calls_none": "Nu se pot efectua apeluri telefonice cu acest cont", + "account_usage_reservations_title": "Subiecte rezervate", + "account_usage_cannot_create_portal_session": "Nu s-a putut deschide portalul de facturare", + "account_delete_title": "Șterge contul", + "account_usage_attachment_storage_description": "{{filesize}} per fișier, șters după {{expiry}}", + "account_usage_attachment_storage_title": "Stocare atașamente", + "account_usage_basis_ip_description": "Statistica și limitele de utilizare pentru acest cont se bazează pe adresa ta IP, așadar pot fi partajate cu alți utilizatori. Limitele afișate mai sus sunt aproximative, bazate pe limitele de viteză existente.", + "account_usage_reservations_none": "Nu există subiecte rezervate pentru acest cont", + "account_basics_tier_canceled_subscription": "Abonamentul tău a fost anulat și va fi retrogradat la un cont gratuit în data de {{date}}." } From a41e3a1e768b92014c7eecdd26700edb3bc54f15 Mon Sep 17 00:00:00 2001 From: Niko Diamadis Date: Fri, 20 Jun 2025 00:45:42 +0200 Subject: [PATCH 134/378] Update App Store badges and remove Docker compose versions --- docker-compose.yml | 2 -- docs/config.md | 2 -- docs/index.md | 6 +++--- docs/install.md | 2 -- docs/static/img/badge-appstore.png | Bin 5922 -> 24487 bytes docs/static/img/badge-fdroid.png | Bin 4524 -> 17302 bytes docs/static/img/badge-googleplay.png | Bin 3812 -> 4698 bytes docs/subscribe/phone.md | 6 +++--- 8 files changed, 6 insertions(+), 12 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d39492e8..d634600c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "2.1" services: ntfy: image: binwiederhier/ntfy @@ -14,4 +13,3 @@ services: ports: - 80:80 restart: unless-stopped - diff --git a/docs/config.md b/docs/config.md index 1687c2ec..74d1d1f4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -79,7 +79,6 @@ using Docker Compose (i.e. `docker-compose.yml`): === "Docker Compose (w/ auth, cache, attachments)" ``` yaml - version: '3' services: ntfy: image: binwiederhier/ntfy @@ -101,7 +100,6 @@ using Docker Compose (i.e. `docker-compose.yml`): === "Docker Compose (w/ auth, cache, web push, iOS)" ``` yaml - version: '3' services: ntfy: image: binwiederhier/ntfy diff --git a/docs/index.md b/docs/index.md index c63b7709..307463ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,9 +3,9 @@ ntfy lets you **send push notifications to your phone or desktop via scripts fro or POST requests. I use it to notify myself when scripts fail, or long-running commands complete. ## Step 1: Get the app - - - + + + To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store or F-Droid. Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just diff --git a/docs/install.md b/docs/install.md index e71bac52..42c868fc 100644 --- a/docs/install.md +++ b/docs/install.md @@ -280,8 +280,6 @@ docker run \ Using docker-compose with non-root user and healthchecks enabled: ```yaml -version: "2.3" - services: ntfy: image: binwiederhier/ntfy diff --git a/docs/static/img/badge-appstore.png b/docs/static/img/badge-appstore.png index 0b4ce1c06dd4973b32c2ada5a2d6db3b044a29cd..a8bc73a6f005fb42ebb2736ecfcac2b67bb37fd5 100644 GIT binary patch literal 24487 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 literal 5922 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 Ef;P&bCjbBd diff --git a/docs/static/img/badge-fdroid.png b/docs/static/img/badge-fdroid.png index 9464d38a13c4845e51160d126e8a4ba2b922c9d9..c1fc8492a9071e39a5df2ce92a315546609c3c7f 100644 GIT binary patch literal 17302 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 KMNUMnLSTZ&KbeI9 diff --git a/docs/static/img/badge-googleplay.png b/docs/static/img/badge-googleplay.png index 36036d8bdc64bdc8adf32905719fef19362fef67..7a06997a57236e18b5bd2a152435911b72371a89 100644 GIT binary patch literal 4698 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 ab}JoSS9!w3tD~s^0000 - - + + + You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that From 994266ab04ad2b8286bc8155cc142a743f2de85a Mon Sep 17 00:00:00 2001 From: Joan Date: Fri, 20 Jun 2025 12:07:37 +0200 Subject: [PATCH 135/378] Added translation using Weblate (Catalan) --- web/public/static/langs/ca.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/ca.json diff --git a/web/public/static/langs/ca.json b/web/public/static/langs/ca.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/ca.json @@ -0,0 +1 @@ +{} From 62c8a13ed4384486ce2f83ff4773c9e8f7616c0b Mon Sep 17 00:00:00 2001 From: Joan Date: Fri, 20 Jun 2025 12:08:54 +0200 Subject: [PATCH 136/378] Translated using Weblate (Catalan) Currently translated at 1.2% (5 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ca/ --- web/public/static/langs/ca.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ca.json b/web/public/static/langs/ca.json index 0967ef42..0d8b4bea 100644 --- a/web/public/static/langs/ca.json +++ b/web/public/static/langs/ca.json @@ -1 +1,7 @@ -{} +{ + "nav_button_documentation": "Documentació", + "action_bar_profile_title": "Perfil", + "action_bar_settings": "Configuració", + "action_bar_account": "Compte", + "common_add": "Afegir" +} From c1e657db8b123113789ae53dbec23f01194724a0 Mon Sep 17 00:00:00 2001 From: Kachelkaiser Date: Mon, 23 Jun 2025 17:08:27 +0200 Subject: [PATCH 137/378] Translated using Weblate (German) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 92dec374..88543c5e 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -31,7 +31,7 @@ "notifications_attachment_open_title": "Gehe zu {{url}}", "notifications_none_for_any_title": "Du hast keine Benachrichtigungen empfangen.", "action_bar_send_test_notification": "Test-Benachrichtigung senden", - "alert_notification_permission_required_description": "Dem Browser erlauben, Desktop-Benachrichtigungen anzuzeigen.", + "alert_notification_permission_required_description": "Browser erlauben, Desktop-Benachrichtigungen anzuzeigen", "notifications_tags": "Tags", "message_bar_type_message": "Gib hier eine Nachricht ein", "message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung", @@ -208,11 +208,11 @@ "action_bar_change_display_name": "Anzeigenamen ändern", "action_bar_reservation_add": "Thema reservieren", "action_bar_reservation_edit": "Reservierung ändern", - "action_bar_reservation_delete": "Reservierung löschen", + "action_bar_reservation_delete": "Reservierung entfernen", "action_bar_reservation_limit_reached": "Grenze erreicht", "action_bar_profile_title": "Profil", "action_bar_profile_settings": "Einstellungen", - "action_bar_profile_logout": "Abmelden", + "action_bar_profile_logout": "Ausloggen", "action_bar_sign_in": "Anmelden", "signup_form_password": "Kennwort", "signup_form_toggle_password_visibility": "Kennwort-Sichtbarkeit umschalten", @@ -382,7 +382,7 @@ "account_usage_calls_none": "Noch keine Anrufe mit diesem Account getätigt", "account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag", "action_bar_mute_notifications": "Benachrichtigungen stummschalten", - "action_bar_unmute_notifications": "Benachrichtigungen laut schalten", + "action_bar_unmute_notifications": "Benachrichtigungen einschalten", "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert", "alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser", "notifications_actions_failed_notification": "Aktion nicht erfolgreich", @@ -402,6 +402,6 @@ "web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert", "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom server empfangen", - "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest.", + "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest.", "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht läuft (über Web Push)" } From df73c6f655502100179c7978a4265e038f060792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 24 Jun 2025 01:19:39 +0200 Subject: [PATCH 138/378] Translated using Weblate (Estonian) Currently translated at 52.5% (213 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 36 ++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index eb0295e7..bf1fe734 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -177,5 +177,39 @@ "priority_low": "madal", "priority_default": "vaikimisi", "priority_high": "kõrge", - "priority_max": "kõrgeim" + "priority_max": "kõrgeim", + "alert_notification_ios_install_required_description": "Teavituste lubamiseks iOS-is klõpsi „Jaga“ ikooni ja vali „Lisa avaekraanile“", + "notifications_none_for_topic_title": "Sul pole selles teemas veel ühtegi teavitust.", + "notifications_none_for_topic_description": "Selles teemas teavituste saatmiseks tee PUT või POST meetodiga päring teema võrguaadressile.", + "publish_dialog_base_url_placeholder": "Teenuse võrguaadress, nt. https://toresait.com", + "notifications_loading": "Laadin teavitusi…", + "publish_dialog_title_topic": "Avalda teemas {{topic}}", + "publish_dialog_progress_uploading_detail": "Üleslaadimisel {{loaded}}/{{total}} ({{percent}}%) …", + "publish_dialog_topic_placeholder": "Teema nimi, nt. kati_teavitused", + "publish_dialog_title_placeholder": "Teavituse pealkiri, nt. Andmeruumi teavitus", + "publish_dialog_message_placeholder": "Siia sisesta sõnum", + "notifications_none_for_any_title": "Sa pole veel saanud ühtegi teavitust.", + "publish_dialog_chip_attach_file_label": "Lisa kohalik fail", + "publish_dialog_chip_attach_url_label": "Lisa fail võrguaadressilt", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Kinnitatud telefoninumbreid ei leidu", + "publish_dialog_chip_email_label": "Edasta e-posti aadressile", + "subscribe_dialog_subscribe_base_url_label": "Teenuse võrguaadress", + "subscribe_dialog_subscribe_button_generate_topic_name": "Loo nimi", + "publish_dialog_checkbox_markdown": "Kasuta Markdown-vormingut", + "subscribe_dialog_login_title": "Vajalik on sisselogimine", + "subscribe_dialog_login_username_label": "Kasutajanimi, nt. kadri", + "account_basics_phone_numbers_dialog_verify_button_sms": "Saada SMS", + "account_basics_username_description": "Hei, see oled sina ❤", + "account_basics_username_admin_tooltip": "Sina oled peakasutaja", + "account_basics_phone_numbers_dialog_verify_button_call": "Helista mulle", + "account_basics_phone_numbers_dialog_code_label": "Kinnituskood", + "account_basics_phone_numbers_dialog_code_placeholder": "nt. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Korda koodi", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} sõnum päevas", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} sõnumit päevas", + "account_upgrade_dialog_button_redirect_signup": "Liitu kohe", + "notifications_actions_http_request_title": "Tee päring HTTP {{method}}-meetodiga võrguaadressile {{url}}", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-kirja päevas", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-kiri päevas", + "alert_not_supported_context_description": "Teavitused võivad kasutada vaid HTTPS-ühendust. See on Teavituste API piirang." } From 9c8a8f87959da389b2f68e780b76a2a0a3d78989 Mon Sep 17 00:00:00 2001 From: "huy.phan" Date: Thu, 26 Jun 2025 15:04:43 +0200 Subject: [PATCH 139/378] Translated using Weblate (Vietnamese) Currently translated at 20.0% (81 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/vi/ --- web/public/static/langs/vi.json | 60 ++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/web/public/static/langs/vi.json b/web/public/static/langs/vi.json index 6167c4bc..cd1ad455 100644 --- a/web/public/static/langs/vi.json +++ b/web/public/static/langs/vi.json @@ -5,10 +5,10 @@ "signup_form_toggle_password_visibility": "Hiện mật khẩu", "login_form_button_submit": "Đăng nhập", "common_copy_to_clipboard": "Lưu vào clipboard", - "signup_form_username": "Tên user", + "signup_form_username": "Tên đăng nhập", "signup_already_have_account": "Đã có tài khoản? Đăng nhập!", - "signup_disabled": "Đăng kí bị đóng", - "signup_error_username_taken": "Tên {{username}} đã được sử dụng", + "signup_disabled": "Đăng kí bị khoá", + "signup_error_username_taken": "Tên đăng nhập {{username}} đã được sử dụng", "signup_error_creation_limit_reached": "Đã đạt giới hạn tạo tài khoản", "login_title": "Đăng nhập vào tài khoản ntfy", "login_link_signup": "Đăng kí", @@ -27,5 +27,57 @@ "action_bar_unsubscribe": "Hủy đăng kí", "action_bar_unmute_notifications": "Bật thông báo", "action_bar_toggle_mute": "Bật/tắt thông báo", - "action_bar_mute_notifications": "Tắt thông báo" + "action_bar_mute_notifications": "Tắt thông báo", + "common_save": "Lưu", + "common_cancel": "Hủy", + "nav_button_all_notifications": "Tất cả thông báo", + "nav_button_connecting": "đang kết nối", + "nav_upgrade_banner_label": "Nâng cấp tài khoản ntfy Pro", + "alert_not_supported_title": "Thông báo không được hỗ trợ", + "alert_not_supported_description": "Thông báo không được hỗ trợ trên trình duyệt của bạn", + "notifications_list": "Danh sách thông báo", + "notifications_list_item": "Thông báo", + "notifications_mark_read": "Đánh dấu đã đọc", + "notifications_delete": "Xoá", + "notifications_attachment_copy_url_title": "Sao chép URL đính kèm vào clipboard", + "notifications_attachment_copy_url_button": "Sao chép URL", + "notifications_attachment_open_title": "Truy cập {{url}}", + "notifications_click_copy_url_button": "Sao chép liên kết", + "notifications_click_open_button": "Mở liên kết", + "notifications_actions_not_supported": "Không được hỗ trợ trên nên tảng web", + "notifications_actions_http_request_title": "Gởi HTTP {{method}} tới {{url}}", + "action_bar_profile_settings": "Cài đặt", + "message_bar_type_message": "Gõ nội dung tại đây", + "nav_button_account": "Tài khoản", + "nav_button_settings": "Cài đặt", + "nav_button_documentation": "Tài liệu", + "alert_notification_permission_required_title": "Thông báo đã bị khoá", + "alert_notification_permission_required_button": "Cấp quyền ngay", + "alert_notification_permission_denied_title": "Thông báo đã bị chặn", + "alert_notification_ios_install_required_title": "Yêu cầu cài đặt iOS", + "alert_notification_ios_install_required_description": "Nhấn vào biểu tượng Chia sẻ và Thêm vào màn hình chính để kích hoạt thông báo trên iOS", + "alert_notification_permission_required_description": "Cấp quyền để trình duyệt hiển thị thông báo trên màn hình", + "alert_notification_permission_denied_description": "Hãy kích hoạt lại trên trình duyệt của bạn", + "notifications_copied_to_clipboard": "Đã lưu vào clipboard", + "notifications_attachment_file_video": "tập tin video", + "notifications_attachment_file_audio": "tập tin âm thanh", + "notifications_actions_failed_notification": "Thực thi thất bại", + "notifications_new_indicator": "Thông báo mới", + "notifications_click_copy_url_title": "Sao liên kết URL vào clipboard", + "notifications_actions_open_url_title": "Truy cập {{url}}", + "notifications_priority_x": "Độ ưu tiên {{priority}}", + "notifications_attachment_link_expired": "liên kết tải đã hết hạn", + "notifications_attachment_file_image": "tập tin hình ảnh", + "notifications_tags": "Thẻ", + "notifications_attachment_file_document": "tập tin khác", + "action_bar_sign_in": "Đăng nhập", + "notifications_attachment_image": "Hình ảnh đính kèm", + "action_bar_sign_up": "Đăng ký", + "action_bar_profile_title": "Hồ sơ", + "action_bar_toggle_action_menu": "Mở/Đóng bảng điều khiển", + "action_bar_profile_logout": "Đăng xuất", + "notifications_attachment_file_app": "tập tin Android", + "notifications_attachment_link_expires": "liên kết đã hết hạn {{date}}", + "alert_not_supported_context_description": "Thông báo chỉ được hỗ trợ qua giao thức HTTPS. Đây là hạn chế của API thông báo.", + "notifications_attachment_open_button": "Mở đính kèm" } From 8e7de8035389e9d7610c691670268747b7b053e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 25 Jun 2025 22:04:39 +0200 Subject: [PATCH 140/378] Translated using Weblate (Estonian) Currently translated at 67.1% (272 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 61 ++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index bf1fe734..f84ed426 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -211,5 +211,64 @@ "notifications_actions_http_request_title": "Tee päring HTTP {{method}}-meetodiga võrguaadressile {{url}}", "account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-kirja päevas", "account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-kiri päevas", - "alert_not_supported_context_description": "Teavitused võivad kasutada vaid HTTPS-ühendust. See on Teavituste API piirang." + "alert_not_supported_context_description": "Teavitused võivad kasutada vaid HTTPS-ühendust. See on Teavituste API piirang.", + "publish_dialog_tags_placeholder": "Komadega eraldatud siltide loend, nt. hoiatus, srv1-varundus", + "display_name_dialog_title": "Muuda kuvatavat nime", + "display_name_dialog_description": "Lisa teemale alternatiivne nimi, mida kuvatakse tellimuste loendis. See on näiteks abiks keerukate nimedega teemade tuvastamiseks.", + "reserve_dialog_checkbox_label": "Reserveeri teema ja seadista ligipääs", + "publish_dialog_attachment_limits_file_reached": "ületab failisuuruse piiri: {{fileSizeLimit}}", + "publish_dialog_attachment_limits_quota_reached": "ületab kvooti, jäänud on {{remainingBytes}}", + "publish_dialog_attachment_limits_file_and_quota_reached": "ületab failisuuruse ülempiiri ({{fileSizeLimit}}) ja kvooti, jäänud on {{remainingBytes}}", + "publish_dialog_click_placeholder": "Teavituse klõpsimisel avatav võrguaadress", + "publish_dialog_click_reset": "Eemalda klikatav võrguaadress", + "publish_dialog_email_placeholder": "Aadress, kuhu teavitus edastatakse, nt. kadri@torefirma.com", + "publish_dialog_email_reset": "Eemalda edastamiseks kasutatav e-posti aadress", + "publish_dialog_call_item": "Helista telefoninumbrile {{number}}", + "publish_dialog_call_reset": "Eemalda helistamine", + "publish_dialog_attach_placeholder": "Lisa fail võrguaadressilt, nt. https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "Eemalda manuse lisamisel kasutatav võrguaadress", + "publish_dialog_delay_reset": "Eemalda viivitus teavituse edastamisel", + "account_basics_password_description": "Muuda oma kasutajakonto salasõna", + "account_basics_password_dialog_title": "Salasõna muutmine", + "account_basics_password_dialog_current_password_label": "Senine salasõna", + "account_basics_password_dialog_button_submit": "Muuda salasõna", + "account_basics_password_dialog_current_password_incorrect": "Salasõna pole korrektne", + "account_basics_phone_numbers_title": "Telefoninumbrid", + "account_basics_phone_numbers_description": "Kõneteavituste jaoks", + "account_basics_tier_title": "Kasutajakonto tüüp", + "account_basics_tier_description": "Sinu kasutajakonto õigused", + "account_delete_dialog_button_submit": "Kustuta kasutajakonto jäädavalt", + "prefs_appearance_theme_system": "Süsteemi kujundus", + "prefs_appearance_theme_dark": "Tume kujundus", + "prefs_appearance_theme_light": "Hele kujundus", + "prefs_reservations_title": "Reserveeritud teemad", + "prefs_users_table": "Kasutajate loend", + "prefs_users_add_button": "Lisa kasutaja", + "prefs_users_edit_button": "Muuda kasutajat", + "prefs_users_delete_button": "Kustuta kasutaja", + "prefs_users_table_cannot_delete_or_edit": "Sisselogitud kasutajat ei saa kustutada ega muuta", + "prefs_users_table_base_url_header": "Teenuse võrguaadress", + "prefs_users_dialog_title_add": "Lisa kasutaja", + "prefs_users_dialog_title_edit": "Muuda kasutajat", + "prefs_users_dialog_base_url_label": "Teenuse võrguaadress, nt. https://ntfy.sh", + "prefs_users_dialog_username_label": "Kasutajanimi, nt. kadri", + "prefs_notifications_delete_after_three_hours": "Kolme tunni möödumisel", + "prefs_notifications_delete_after_three_hours_description": "Teavitused kustutatakse automaatselt kolme tunni möödumisel", + "prefs_notifications_delete_after_one_day_description": "Teavitused kustutatakse automaatselt ühe päeva möödumisel", + "prefs_notifications_delete_after_one_week_description": "Teavitused kustutatakse automaatselt ühe nädala möödumisel", + "prefs_notifications_delete_after_one_month_description": "Teavitused kustutatakse automaatselt ühe kuu möödumisel", + "prefs_notifications_delete_after_never_description": "Mitte kunagi ei kustutata teavitusi automaatselt", + "prefs_notifications_delete_after_title": "Kustuta teavitused", + "publish_dialog_delay_placeholder": "Viivitus teavituse edastamisel, nt. {{unixTimestamp}}, {{relativeTime}} või „{{naturalLanguage}}“ (vaid inglise keeles)", + "account_basics_password_dialog_new_password_label": "Uus salasõna", + "account_basics_password_dialog_confirm_password_label": "Korda salasõna", + "account_basics_phone_numbers_dialog_description": "Kõneteavituse kasutamiseks pead lisama ja kinnitama vähemalt ühe telefoninumbri. Kinnitamist saad teha SMS-i või kõne abil.", + "account_basics_phone_numbers_dialog_number_placeholder": "nt. +37256123456", + "account_basics_phone_numbers_no_phone_numbers_yet": "Telefoninumbreid veel pole", + "account_basics_phone_numbers_copied_to_clipboard": "Telefoninumber on kopeeritud lõikelauale", + "account_basics_phone_numbers_dialog_title": "Lisa telefoninumber", + "account_basics_phone_numbers_dialog_number_label": "Telefoninumber", + "prefs_notifications_delete_after_one_week": "Ühe nädala möödumisel", + "prefs_notifications_delete_after_one_day": "Ühe päeva möödumisel", + "prefs_notifications_delete_after_one_month": "Ühe kuu möödumisel" } From ff904a5ca62b8fe2dc8d82f9f21a16df9473634e Mon Sep 17 00:00:00 2001 From: Carl Fritze Date: Mon, 30 Jun 2025 14:29:37 +0200 Subject: [PATCH 141/378] Translated using Weblate (German) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 88543c5e..9aeedb95 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -36,7 +36,7 @@ "message_bar_type_message": "Gib hier eine Nachricht ein", "message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung", "alert_not_supported_title": "Benachrichtigungen werden nicht unterstützt", - "alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt", + "alert_not_supported_description": "Benachrichtigungen werden von deinem Browser nicht unterstützt:", "action_bar_settings": "Einstellungen", "action_bar_clear_notifications": "Alle Benachrichtigungen löschen", "alert_notification_permission_required_button": "Jetzt erlauben", @@ -401,7 +401,7 @@ "error_boundary_button_reload_ntfy": "ntfy neu laden", "web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert", "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", - "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom server empfangen", + "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom Server empfangen", "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest.", "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht läuft (über Web Push)" } From 48cb816111fb814f70ce24b9c9301f2a521c0b6d Mon Sep 17 00:00:00 2001 From: Kachelkaiser Date: Mon, 30 Jun 2025 14:31:10 +0200 Subject: [PATCH 142/378] Translated using Weblate (German) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 9aeedb95..95b7a1fc 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -36,7 +36,7 @@ "message_bar_type_message": "Gib hier eine Nachricht ein", "message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung", "alert_not_supported_title": "Benachrichtigungen werden nicht unterstützt", - "alert_not_supported_description": "Benachrichtigungen werden von deinem Browser nicht unterstützt:", + "alert_not_supported_description": "Benachrichtigungen werden von deinem Browser nicht unterstützt", "action_bar_settings": "Einstellungen", "action_bar_clear_notifications": "Alle Benachrichtigungen löschen", "alert_notification_permission_required_button": "Jetzt erlauben", @@ -382,7 +382,7 @@ "account_usage_calls_none": "Noch keine Anrufe mit diesem Account getätigt", "account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag", "action_bar_mute_notifications": "Benachrichtigungen stummschalten", - "action_bar_unmute_notifications": "Benachrichtigungen einschalten", + "action_bar_unmute_notifications": "Stummschaltung von Benachrichtigungen aufheben", "alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert", "alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser", "notifications_actions_failed_notification": "Aktion nicht erfolgreich", @@ -390,8 +390,8 @@ "alert_notification_ios_install_required_description": "Klicke auf das Teilen-Symbol und “Zum Home-Bildschirm” um auf iOS Benachrichtigungen zu aktivieren", "subscribe_dialog_subscribe_use_another_background_info": "Benachrichtigungen von anderen Servern werden nicht empfangen, wenn die Web App nicht geöffnet ist", "publish_dialog_checkbox_markdown": "Als Markdown formatieren", - "prefs_notifications_web_push_title": "Hintergrund-Benachrichtigungen", - "prefs_notifications_web_push_disabled_description": "Benachrichtigungen werden empfangen wenn die Web App läuft (über WebSocket)", + "prefs_notifications_web_push_title": "Hintergrundbenachrichtigung", + "prefs_notifications_web_push_disabled_description": "Benachrichtigungen werden empfangen, wenn die Web App geöffnet ist (via WebSocket)", "prefs_notifications_web_push_enabled": "Aktiviert für {{server}}", "prefs_notifications_web_push_disabled": "Deaktiviert", "prefs_appearance_theme_title": "Thema", @@ -402,6 +402,6 @@ "web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert", "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom Server empfangen", - "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest.", - "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht läuft (über Web Push)" + "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest", + "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht geöffnet ist (via Web Push)" } From ae27c3a5ab4d07fa11104d690d88257be3ffe884 Mon Sep 17 00:00:00 2001 From: Kachelkaiser Date: Mon, 30 Jun 2025 14:31:59 +0200 Subject: [PATCH 143/378] Translated using Weblate (German) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 95b7a1fc..0654483a 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -390,7 +390,7 @@ "alert_notification_ios_install_required_description": "Klicke auf das Teilen-Symbol und “Zum Home-Bildschirm” um auf iOS Benachrichtigungen zu aktivieren", "subscribe_dialog_subscribe_use_another_background_info": "Benachrichtigungen von anderen Servern werden nicht empfangen, wenn die Web App nicht geöffnet ist", "publish_dialog_checkbox_markdown": "Als Markdown formatieren", - "prefs_notifications_web_push_title": "Hintergrundbenachrichtigung", + "prefs_notifications_web_push_title": "Hintergrundbenachrichtigungen", "prefs_notifications_web_push_disabled_description": "Benachrichtigungen werden empfangen, wenn die Web App geöffnet ist (via WebSocket)", "prefs_notifications_web_push_enabled": "Aktiviert für {{server}}", "prefs_notifications_web_push_disabled": "Deaktiviert", From 5ccc131e73e80bc4295a375a8bd6a37928731257 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 4 Jul 2025 06:41:14 +0200 Subject: [PATCH 144/378] Derp --- server/server.go | 2 +- server/server_payments.go | 6 ++++- server/server_payments_dummy.go | 41 +++++++++++++++++++++++++++++++++ server/server_payments_test.go | 2 ++ stripe/types.go | 1 + 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 server/server_payments_dummy.go create mode 100644 stripe/types.go diff --git a/server/server.go b/server/server.go index e1126757..36fd25f2 100644 --- a/server/server.go +++ b/server/server.go @@ -158,7 +158,7 @@ func New(conf *Config) (*Server, error) { mailer = &smtpSender{config: conf} } var stripe stripeAPI - if conf.StripeSecretKey != "" { + if hasStripe && conf.StripeSecretKey != "" { stripe = newStripeAPI() } messageCache, err := createMessageCache(conf) diff --git a/server/server_payments.go b/server/server_payments.go index 334301bb..3c4b1fc6 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -1,3 +1,5 @@ +//go:build !nopayments + package server import ( @@ -22,7 +24,7 @@ import ( // Payments in ntfy are done via Stripe. // -// Pretty much all payments related things are in this file. The following processes +// Pretty much all payments-related things are in this file. The following processes // handle payments: // // - Checkout: @@ -42,6 +44,8 @@ import ( // This is used to keep the local user database fields up to date. Stripe is the source of truth. // What Stripe says is mirrored and not questioned. +const hasStripe = true + var ( errNotAPaidTier = errors.New("tier does not have billing price identifier") errMultipleBillingSubscriptions = errors.New("cannot have multiple billing subscriptions") diff --git a/server/server_payments_dummy.go b/server/server_payments_dummy.go new file mode 100644 index 00000000..3915453c --- /dev/null +++ b/server/server_payments_dummy.go @@ -0,0 +1,41 @@ +//go:build nopayments + +package server + +const hasStripe = false + +func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWriter, r *http.Request, v *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingPortalSessionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Request, v *visitor) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(r *http.Request, v *visitor, event stripe.Event) error { + return errHTTPNotFound +} + +func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(r *http.Request, v *visitor, event stripe.Event) error { + return errHTTPNotFound +} diff --git a/server/server_payments_test.go b/server/server_payments_test.go index 56d4cc6a..d72d2a6b 100644 --- a/server/server_payments_test.go +++ b/server/server_payments_test.go @@ -1,3 +1,5 @@ +//go:build !nopayments + package server import ( diff --git a/stripe/types.go b/stripe/types.go new file mode 100644 index 00000000..0e0d17df --- /dev/null +++ b/stripe/types.go @@ -0,0 +1 @@ +package stripe From d8c8f318464be290f9b9a44b2d89e140857d7fa2 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 4 Jul 2025 07:38:58 +0200 Subject: [PATCH 145/378] IPv6 WIP --- cmd/serve.go | 17 +++++++++-------- server/config.go | 6 +++--- server/smtp_server.go | 11 ++++++----- server/util.go | 23 +++++++++++++++-------- server/visitor.go | 6 ++++++ 5 files changed, 39 insertions(+), 24 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 576e72f0..373ba69e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,13 +5,6 @@ package cmd import ( "errors" "fmt" - "github.com/stripe/stripe-go/v74" - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" - "heckel.io/ntfy/v2/log" - "heckel.io/ntfy/v2/server" - "heckel.io/ntfy/v2/user" - "heckel.io/ntfy/v2/util" "io/fs" "math" "net" @@ -22,6 +15,14 @@ import ( "strings" "syscall" "time" + + "github.com/stripe/stripe-go/v74" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/server" + "heckel.io/ntfy/v2/user" + "heckel.io/ntfy/v2/util" ) func init() { @@ -473,7 +474,7 @@ func sigHandlerConfigReload(config string) { } func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) { - // Try parsing as prefix, e.g. 10.0.1.0/24 + // Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32 prefix, err := netip.ParsePrefix(host) if err == nil { prefixes = append(prefixes, prefix.Masked()) diff --git a/server/config.go b/server/config.go index 75e6d488..f3320c5b 100644 --- a/server/config.go +++ b/server/config.go @@ -143,9 +143,9 @@ type Config struct { VisitorAuthFailureLimitReplenish time.Duration VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics - BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address - ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" - ProxyTrustedAddresses []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true + BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported) + ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported) + ProxyTrustedAddresses []string // List of trusted proxy addresses (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration diff --git a/server/smtp_server.go b/server/smtp_server.go index 6efd5230..6de42e37 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -5,8 +5,6 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/emersion/go-smtp" - "github.com/microcosm-cc/bluemonday" "io" "mime" "mime/multipart" @@ -18,6 +16,9 @@ import ( "regexp" "strings" "sync" + + "github.com/emersion/go-smtp" + "github.com/microcosm-cc/bluemonday" ) var ( @@ -191,9 +192,9 @@ func (s *smtpSession) publishMessage(m *message) error { // Call HTTP handler with fake HTTP request url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic) req, err := http.NewRequest("POST", url, strings.NewReader(m.Message)) - req.RequestURI = "/" + m.Topic // just for the logs - req.RemoteAddr = remoteAddr // rate limiting!! - req.Header.Set("X-Forwarded-For", remoteAddr) + req.RequestURI = "/" + m.Topic // just for the logs + req.RemoteAddr = remoteAddr // rate limiting!! + req.Header.Set(s.backend.config.ProxyForwardedHeader, remoteAddr) // Set X-Forwarded-For header if err != nil { return err } diff --git a/server/util.go b/server/util.go index 3db9e322..54c1851b 100644 --- a/server/util.go +++ b/server/util.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "heckel.io/ntfy/v2/util" "io" "mime" "net/http" @@ -12,6 +11,8 @@ import ( "regexp" "slices" "strings" + + "heckel.io/ntfy/v2/util" ) var ( @@ -20,8 +21,9 @@ var ( // priorityHeaderIgnoreRegex matches specific patterns of the "Priority" header (RFC 9218), so that it can be ignored priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`) - // forwardedHeaderRegex parses IPv4 addresses from the "Forwarded" header (RFC 7239) - forwardedHeaderRegex = regexp.MustCompile(`(?i)\bfor="?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"?`) + // forwardedHeaderRegex parses IPv4 and IPv6 addresses from the "Forwarded" header (RFC 7239) + // IPv6 addresses in Forwarded header are enclosed in square brackets, e.g. for="[2001:db8::1]" + forwardedHeaderRegex = regexp.MustCompile(`(?i)\\bfor=\"?((?:[0-9]{1,3}\.){3}[0-9]{1,3}|\[[0-9a-fA-F:]+\])\"?`) ) func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool { @@ -103,7 +105,7 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // then take the right-most address in the list (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { - value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader))) + value := strings.TrimSpace(r.Header.Get(forwardedHeader)) if value == "" { return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) } @@ -111,12 +113,17 @@ func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trusted addrsStrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace) var validAddrs []netip.Addr for _, addrStr := range addrsStrs { - if addr, err := netip.ParseAddr(addrStr); err == nil { - validAddrs = append(validAddrs, addr) - } else if m := forwardedHeaderRegex.FindStringSubmatch(addrStr); len(m) == 2 { - if addr, err := netip.ParseAddr(m[1]); err == nil { + // Handle Forwarded header with for="[IPv6]" or for="IPv4" + if m := forwardedHeaderRegex.FindStringSubmatch(addrStr); len(m) == 2 { + addrRaw := m[1] + if strings.HasPrefix(addrRaw, "[") && strings.HasSuffix(addrRaw, "]") { + addrRaw = addrRaw[1 : len(addrRaw)-1] + } + if addr, err := netip.ParseAddr(addrRaw); err == nil { validAddrs = append(validAddrs, addr) } + } else if addr, err := netip.ParseAddr(addrStr); err == nil { + validAddrs = append(validAddrs, addr) } } // Filter out proxy addresses diff --git a/server/visitor.go b/server/visitor.go index d542e773..155d7be0 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -528,5 +528,11 @@ func visitorID(ip netip.Addr, u *user.User) string { if u != nil && u.Tier != nil { return fmt.Sprintf("user:%s", u.ID) } + if ip.Is6() { + // IPv6 addresses are too long to be used as visitor IDs, so we use the first 8 bytes + ip = netip.PrefixFrom(ip, 64).Masked().Addr() + } else if ip.Is4() { + ip = netip.PrefixFrom(ip, 20).Masked().Addr() + } return fmt.Sprintf("ip:%s", ip.String()) } From 54514454bf5f65c7610edbc3f3003ccdaa30c4f9 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 4 Jul 2025 10:16:49 +0200 Subject: [PATCH 146/378] Works --- cmd/serve.go | 16 ++++++-- server/config.go | 6 +++ server/server.go | 2 +- server/server_test.go | 92 +++++++++++++++++++++++++++++++++++++++++-- server/util.go | 11 ++++-- server/visitor.go | 18 ++++----- 6 files changed, 125 insertions(+), 20 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 373ba69e..27ae0fcb 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -80,6 +80,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}), + altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultVisitorAttachmentTotalSizeLimit), Usage: "total storage limit used for attachments per visitor"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", Aliases: []string{"visitor_attachment_daily_bandwidth_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", Aliases: []string{"visitor_request_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}), @@ -88,7 +89,8 @@ var flagsServe = append( altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}), - altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}), + altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv4", Aliases: []string{"visitor_prefix_bits_ipv4"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV4"}, Value: server.DefaultVisitorPrefixBitsIPv4, Usage: "number of bits of the IPv4 address to use for rate limiting (default: 32, full address)"}), + altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv6", Aliases: []string{"visitor_prefix_bits_ipv6"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV6"}, Value: server.DefaultVisitorPrefixBitsIPv6, Usage: "number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addresses", Aliases: []string{"proxy_trusted_addresses"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRESSES"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), @@ -192,6 +194,8 @@ func execServe(c *cli.Context) error { visitorMessageDailyLimit := c.Int("visitor-message-daily-limit") visitorEmailLimitBurst := c.Int("visitor-email-limit-burst") visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish") + visitorPrefixBitsIPv4 := c.Int("visitor-prefix-bits-ipv4") + visitorPrefixBitsIPv6 := c.Int("visitor-prefix-bits-ipv6") behindProxy := c.Bool("behind-proxy") proxyForwardedHeader := c.String("proxy-forwarded-header") proxyTrustedAddresses := util.SplitNoEmpty(c.String("proxy-trusted-addresses"), ",") @@ -325,6 +329,10 @@ func execServe(c *cli.Context) error { return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") } else if behindProxy && proxyForwardedHeader == "" { return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set") + } else if visitorPrefixBitsIPv4 < 1 || visitorPrefixBitsIPv4 > 32 { + return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32") + } else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 { + return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128") } // Backwards compatibility @@ -413,6 +421,7 @@ func execServe(c *cli.Context) error { conf.MessageDelayMax = messageDelayLimit conf.TotalTopicLimit = totalTopicLimit conf.VisitorSubscriptionLimit = visitorSubscriptionLimit + conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit conf.VisitorAttachmentDailyBandwidthLimit = visitorAttachmentDailyBandwidthLimit conf.VisitorRequestLimitBurst = visitorRequestLimitBurst @@ -421,7 +430,8 @@ func execServe(c *cli.Context) error { conf.VisitorMessageDailyLimit = visitorMessageDailyLimit conf.VisitorEmailLimitBurst = visitorEmailLimitBurst conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish - conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting + conf.VisitorPrefixBitsIPv4 = visitorPrefixBitsIPv4 + conf.VisitorPrefixBitsIPv6 = visitorPrefixBitsIPv6 conf.BehindProxy = behindProxy conf.ProxyForwardedHeader = proxyForwardedHeader conf.ProxyTrustedAddresses = proxyTrustedAddresses @@ -434,7 +444,6 @@ func execServe(c *cli.Context) error { conf.EnableMetrics = enableMetrics conf.MetricsListenHTTP = metricsListenHTTP conf.ProfileListenHTTP = profileListenHTTP - conf.Version = c.App.Version conf.WebPushPrivateKey = webPushPrivateKey conf.WebPushPublicKey = webPushPublicKey conf.WebPushFile = webPushFile @@ -442,6 +451,7 @@ func execServe(c *cli.Context) error { conf.WebPushStartupQueries = webPushStartupQueries conf.WebPushExpiryDuration = webPushExpiryDuration conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration + conf.Version = c.App.Version // Set up hot-reloading of config go sigHandlerConfigReload(config) diff --git a/server/config.go b/server/config.go index f3320c5b..c351ba85 100644 --- a/server/config.go +++ b/server/config.go @@ -61,6 +61,8 @@ const ( DefaultVisitorAuthFailureLimitReplenish = time.Minute DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB + DefaultVisitorPrefixBitsIPv4 = 32 // Use the entire IPv4 address for rate limiting + DefaultVisitorPrefixBitsIPv6 = 64 // Use /64 for IPv6 rate limiting ) var ( @@ -143,6 +145,8 @@ type Config struct { VisitorAuthFailureLimitReplenish time.Duration VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics + VisitorPrefixBitsIPv4 int // Number of bits for IPv4 rate limiting (default: 32) + VisitorPrefixBitsIPv6 int // Number of bits for IPv6 rate limiting (default: 64) BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported) ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported) ProxyTrustedAddresses []string // List of trusted proxy addresses (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true @@ -234,6 +238,8 @@ func NewConfig() *Config { VisitorAuthFailureLimitReplenish: DefaultVisitorAuthFailureLimitReplenish, VisitorStatsResetTime: DefaultVisitorStatsResetTime, VisitorSubscriberRateLimiting: false, + VisitorPrefixBitsIPv4: 32, // Default: use full IPv4 address + VisitorPrefixBitsIPv6: 64, // Default: use /64 for IPv6 BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs StripeSecretKey: "", diff --git a/server/server.go b/server/server.go index e1126757..8d33d396 100644 --- a/server/server.go +++ b/server/server.go @@ -2023,7 +2023,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor { s.mu.Lock() defer s.mu.Unlock() - id := visitorID(ip, user) + id := visitorID(ip, user, s.config) v, exists := s.visitors[id] if !exists { s.visitors[id] = newVisitor(s.config, s.messageCache, s.userManager, ip, user) diff --git a/server/server_test.go b/server/server_test.go index be0610ac..0a5bcc08 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1169,7 +1169,7 @@ func (t *testMailer) Count() int { return t.count } -func TestServer_PublishTooRequests_Defaults(t *testing.T) { +func TestServer_PublishTooManyRequests_Defaults(t *testing.T) { s := newTestServer(t, newTestConfig(t)) for i := 0; i < 60; i++ { response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) @@ -1179,7 +1179,50 @@ func TestServer_PublishTooRequests_Defaults(t *testing.T) { require.Equal(t, 429, response.Code) } -func TestServer_PublishTooRequests_Defaults_ExemptHosts(t *testing.T) { +func TestServer_PublishTooManyRequests_Defaults_IPv6(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + overrideRemoteAddr1 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999:8888:1::1]:1234" + } + overrideRemoteAddr2 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999:8888:2::1]:1234" // Same /64 + } + for i := 0; i < 30; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1) + require.Equal(t, 200, response.Code) + } + for i := 0; i < 30; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1) + require.Equal(t, 429, response.Code) +} + +func TestServer_PublishTooManyRequests_IPv6_Slash48(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 6 + c.VisitorPrefixBitsIPv6 = 48 // Use /48 for IPv6 prefixes + s := newTestServer(t, c) + overrideRemoteAddr1 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999::1]:1234" + } + overrideRemoteAddr2 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999::2]:1234" // Same /48 + } + for i := 0; i < 3; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1) + require.Equal(t, 200, response.Code) + } + for i := 0; i < 3; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1) + require.Equal(t, 429, response.Code) +} + +func TestServer_PublishTooManyRequests_Defaults_ExemptHosts(t *testing.T) { c := newTestConfig(t) c.VisitorRequestLimitBurst = 3 c.VisitorRequestExemptIPAddrs = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() @@ -1190,7 +1233,21 @@ func TestServer_PublishTooRequests_Defaults_ExemptHosts(t *testing.T) { } } -func TestServer_PublishTooRequests_Defaults_ExemptHosts_MessageDailyLimit(t *testing.T) { +func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_IPv6(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorRequestExemptIPAddrs = []netip.Prefix{netip.MustParsePrefix("2001:db8:9999::/48")} + s := newTestServer(t, c) + overrideRemoteAddr := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999::1]:1234" + } + for i := 0; i < 5; i++ { // > 3 + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr) + require.Equal(t, 200, response.Code) + } +} + +func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_MessageDailyLimit(t *testing.T) { c := newTestConfig(t) c.VisitorRequestLimitBurst = 10 c.VisitorMessageDailyLimit = 4 @@ -1202,7 +1259,7 @@ func TestServer_PublishTooRequests_Defaults_ExemptHosts_MessageDailyLimit(t *tes } } -func TestServer_PublishTooRequests_ShortReplenish(t *testing.T) { +func TestServer_PublishTooManyRequests_ShortReplenish(t *testing.T) { t.Parallel() c := newTestConfig(t) c.VisitorRequestLimitBurst = 60 @@ -2244,6 +2301,19 @@ func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) { require.Equal(t, "1.2.3.4", v.ip.String()) } +func TestServer_Visitor_Custom_ClientIP_Header_IPv6(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "X-Client-IP" + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "[2001:db8:9999::1]:1234" + r.Header.Set("X-Client-IP", "2001:db8:7777::1") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "2001:db8:7777::1", v.ip.String()) +} + func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) { c := newTestConfig(t) c.BehindProxy = true @@ -2258,6 +2328,20 @@ func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) { require.Equal(t, "5.6.7.8", v.ip.String()) } +func TestServer_Visitor_Custom_Forwarded_Header_IPv6(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "Forwarded" + c.ProxyTrustedAddresses = []string{"2001:db8:1111::1"} + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "[2001:db8:2222::1]:1234" + r.Header.Set("Forwarded", " for=[2001:db8:1111::1], by=example.com;for=[2001:db8:3333::1]") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "2001:db8:3333::1", v.ip.String()) +} + func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { t.Parallel() count := 50000 diff --git a/server/util.go b/server/util.go index 54c1851b..687e7d0e 100644 --- a/server/util.go +++ b/server/util.go @@ -22,8 +22,13 @@ var ( priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`) // forwardedHeaderRegex parses IPv4 and IPv6 addresses from the "Forwarded" header (RFC 7239) - // IPv6 addresses in Forwarded header are enclosed in square brackets, e.g. for="[2001:db8::1]" - forwardedHeaderRegex = regexp.MustCompile(`(?i)\\bfor=\"?((?:[0-9]{1,3}\.){3}[0-9]{1,3}|\[[0-9a-fA-F:]+\])\"?`) + // IPv6 addresses in Forwarded header are enclosed in square brackets. The port is optional. + // + // Examples: + // for="1.2.3.4" + // for="[2001:db8::1]"; for=1.2.3.4:8080, by=phil + // for="1.2.3.4:8080" + forwardedHeaderRegex = regexp.MustCompile(`(?i)\bfor="?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[0-9a-f:]+])(?::\d+)?"?`) ) func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool { @@ -105,7 +110,7 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // then take the right-most address in the list (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { - value := strings.TrimSpace(r.Header.Get(forwardedHeader)) + value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader))) if value == "" { return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) } diff --git a/server/visitor.go b/server/visitor.go index 155d7be0..f26729f1 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -2,13 +2,13 @@ package server import ( "fmt" - "heckel.io/ntfy/v2/log" - "heckel.io/ntfy/v2/user" "net/netip" "sync" "time" "golang.org/x/time/rate" + "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/util" ) @@ -151,7 +151,7 @@ func (v *visitor) Context() log.Context { func (v *visitor) contextNoLock() log.Context { info := v.infoLightNoLock() fields := log.Context{ - "visitor_id": visitorID(v.ip, v.user), + "visitor_id": visitorID(v.ip, v.user, v.config), "visitor_ip": v.ip.String(), "visitor_seen": util.FormatTime(v.seen), "visitor_messages": info.Stats.Messages, @@ -524,15 +524,15 @@ func dailyLimitToRate(limit int64) rate.Limit { return rate.Limit(limit) * rate.Every(oneDay) } -func visitorID(ip netip.Addr, u *user.User) string { +// visitorID returns a unique identifier for a visitor based on user or IP, using configurable prefix bits for IPv4/IPv6 +func visitorID(ip netip.Addr, u *user.User, conf *Config) string { if u != nil && u.Tier != nil { return fmt.Sprintf("user:%s", u.ID) } - if ip.Is6() { - // IPv6 addresses are too long to be used as visitor IDs, so we use the first 8 bytes - ip = netip.PrefixFrom(ip, 64).Masked().Addr() - } else if ip.Is4() { - ip = netip.PrefixFrom(ip, 20).Masked().Addr() + if ip.Is4() { + ip = netip.PrefixFrom(ip, conf.VisitorPrefixBitsIPv4).Masked().Addr() + } else if ip.Is6() { + ip = netip.PrefixFrom(ip, conf.VisitorPrefixBitsIPv6).Masked().Addr() } return fmt.Sprintf("ip:%s", ip.String()) } From c99d8b66c279417fd39c04eadd73d66fa1e5ed80 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 4 Jul 2025 10:19:27 +0200 Subject: [PATCH 147/378] Re-order --- server/config.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/config.go b/server/config.go index c351ba85..0fc39932 100644 --- a/server/config.go +++ b/server/config.go @@ -224,6 +224,7 @@ func NewConfig() *Config { TotalTopicLimit: DefaultTotalTopicLimit, TotalAttachmentSizeLimit: 0, VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit, + VisitorSubscriberRateLimiting: false, VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit, VisitorAttachmentDailyBandwidthLimit: DefaultVisitorAttachmentDailyBandwidthLimit, VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst, @@ -237,11 +238,10 @@ func NewConfig() *Config { VisitorAuthFailureLimitBurst: DefaultVisitorAuthFailureLimitBurst, VisitorAuthFailureLimitReplenish: DefaultVisitorAuthFailureLimitReplenish, VisitorStatsResetTime: DefaultVisitorStatsResetTime, - VisitorSubscriberRateLimiting: false, - VisitorPrefixBitsIPv4: 32, // Default: use full IPv4 address - VisitorPrefixBitsIPv6: 64, // Default: use /64 for IPv6 - BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address - ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs + VisitorPrefixBitsIPv4: DefaultVisitorPrefixBitsIPv4, // Default: use full IPv4 address + VisitorPrefixBitsIPv6: DefaultVisitorPrefixBitsIPv6, // Default: use /64 for IPv6 + BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address + ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs StripeSecretKey: "", StripeWebhookKey: "", StripePriceCacheDuration: DefaultStripePriceCacheDuration, From 7eeaeb839825b9a9d9b405f58b3d008e2da6e153 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 4 Jul 2025 16:51:55 +0200 Subject: [PATCH 148/378] server.yml update --- server/server.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/server.yml b/server/server.yml index 669805b8..37e0faf5 100644 --- a/server/server.yml +++ b/server/server.yml @@ -292,6 +292,18 @@ # visitor-email-limit-burst: 16 # visitor-email-limit-replenish: "1h" +# Rate limiting: IPv4/IPv6 address prefix bits used for rate limiting +# - visitor-prefix-bits-ipv4: number of bits of the IPv4 address to use for rate limiting (default: 32, full address) +# - visitor-prefix-bits-ipv6: number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet) +# +# This is used to group visitors by their IP address or subnet. For example, if you set visitor-prefix-bits-ipv4 to 24, +# all visitors in the 1.2.3.0/24 network are treated as one. +# +# By default, ntfy uses the full IPv4 address (32 bits) and the /64 subnet of the IPv6 address (64 bits). +# +# visitor-prefix-bits-ipv4: 32 +# visitor-prefix-bits-ipv6: 64 + # Rate limiting: Attachment size and bandwidth limits per visitor: # - visitor-attachment-total-size-limit is the total storage limit used for attachments per visitor # - visitor-attachment-daily-bandwidth-limit is the total daily attachment download/upload traffic limit per visitor From 60b858812912d3b84ce12fff435b46f585639186 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 4 Jul 2025 16:56:35 +0200 Subject: [PATCH 149/378] Tests --- server/util_test.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/server/util_test.go b/server/util_test.go index 4b60e1a1..2989b0b9 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -4,10 +4,11 @@ import ( "bytes" "crypto/rand" "fmt" - "github.com/stretchr/testify/require" "net/http" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestReadBoolParam(t *testing.T) { @@ -118,3 +119,27 @@ func TestExtractIPAddress_UnixSocket(t *testing.T) { require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String()) require.Equal(t, "0.0.0.0", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String()) } + +func TestExtractIPAddress_MixedIPv4IPv6(t *testing.T) { + r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) + r.RemoteAddr = "[2001:db8:abcd::1]:1234" + r.Header.Set("X-Forwarded-For", "1.2.3.4, 2001:db8:abcd::2, 5.6.7.8") + trustedProxies := []string{"1.2.3.4"} + require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) +} + +func TestExtractIPAddress_TrustedIPv6Prefix(t *testing.T) { + r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) + r.RemoteAddr = "[2001:db8:abcd::1]:1234" + r.Header.Set("X-Forwarded-For", "2001:db8:abcd::1, 2001:db8:abcd:1::2, 2001:db8:abcd:2::3") + trustedProxies := []string{"2001:db8:abcd::/48"} + require.Equal(t, "2001:db8:abcd:2::3", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) +} + +func TestExtractIPAddress_EdgeCases(t *testing.T) { + r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) + r.RemoteAddr = "[::ffff:192.0.2.128]:1234" // IPv4-mapped IPv6 + r.Header.Set("X-Forwarded-For", "::ffff:192.0.2.128, 2001:db8:abcd::1") + trustedProxies := []string{"::ffff:192.0.2.128"} + require.Equal(t, "2001:db8:abcd::1", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) +} From 34e9a771ce8d03c3af6400b964410312d7a4017c Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 5 Jul 2025 17:05:31 +1000 Subject: [PATCH 150/378] docs: add ntfyexec to integrations --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index c7da21f9..23c5f9e9 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -95,6 +95,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11 - [Ntfy_CSV_Reminders](https://github.com/thiswillbeyourgithub/Ntfy_CSV_Reminders) - A Python tool that sends random-timing phone notifications for recurring tasks by using daily probability checks based on CSV-defined frequencies. - [Daily Fact Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - Generate [llm](https://github.com/simonw/llm) generated fact every day about any topic you're interested in. +- [ntfyexec](https://github.com/alecthomas/ntfyexec) - Send a notification through ntfy.sh if a command fails ## Projects + scripts From 359c789c3406cba8327ba1c008fc9e6b17352913 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 5 Jul 2025 13:11:17 +0200 Subject: [PATCH 151/378] Test for visitorID --- server/util_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/server/util_test.go b/server/util_test.go index 2989b0b9..128a8160 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -4,7 +4,9 @@ import ( "bytes" "crypto/rand" "fmt" + "heckel.io/ntfy/v2/user" "net/http" + "net/netip" "strings" "testing" @@ -143,3 +145,24 @@ func TestExtractIPAddress_EdgeCases(t *testing.T) { trustedProxies := []string{"::ffff:192.0.2.128"} require.Equal(t, "2001:db8:abcd::1", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) } + +func TestVisitorID(t *testing.T) { + confWithDefaults := &Config{ + VisitorPrefixBitsIPv4: 32, + VisitorPrefixBitsIPv6: 64, + } + confWithShortenedPrefixes := &Config{ + VisitorPrefixBitsIPv4: 16, + VisitorPrefixBitsIPv6: 56, + } + userWithTier := &user.User{ + ID: "u_123", + Tier: &user.Tier{}, + } + require.Equal(t, "ip:1.2.3.4", visitorID(netip.MustParseAddr("1.2.3.4"), nil, confWithDefaults)) + require.Equal(t, "ip:2a01:599:b26:2397::", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), nil, confWithDefaults)) + require.Equal(t, "user:u_123", visitorID(netip.MustParseAddr("1.2.3.4"), userWithTier, confWithDefaults)) + require.Equal(t, "user:u_123", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), userWithTier, confWithDefaults)) + require.Equal(t, "ip:1.2.0.0", visitorID(netip.MustParseAddr("1.2.3.4"), nil, confWithShortenedPrefixes)) + require.Equal(t, "ip:2a01:599:b26:2300::", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), nil, confWithShortenedPrefixes)) +} From 677b44ce613389cd0d0f1633a2bba75b3e3a25db Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 5 Jul 2025 22:35:26 +0200 Subject: [PATCH 152/378] Docs, rename proxy-trusted-(addresses->hosts) --- cmd/serve.go | 24 +- docs/config.md | 48 ++- docs/releases.md | 3 +- go.mod | 58 +-- go.sum | 122 +++--- server/config.go | 20 +- server/server.go | 6 +- server/server.yml | 4 +- server/server_middleware.go | 4 +- server/server_test.go | 10 +- server/util.go | 14 +- server/util_test.go | 18 +- web/package-lock.json | 716 ++++++++++++++++++------------------ 13 files changed, 532 insertions(+), 515 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 27ae0fcb..ef4d98d5 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -93,7 +93,7 @@ var flagsServe = append( altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv6", Aliases: []string{"visitor_prefix_bits_ipv6"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV6"}, Value: server.DefaultVisitorPrefixBitsIPv6, Usage: "number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addresses", Aliases: []string{"proxy_trusted_addresses"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRESSES"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-hosts", Aliases: []string{"proxy_trusted_hosts"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_HOSTS"}, Value: "", Usage: "comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}), @@ -198,7 +198,7 @@ func execServe(c *cli.Context) error { visitorPrefixBitsIPv6 := c.Int("visitor-prefix-bits-ipv6") behindProxy := c.Bool("behind-proxy") proxyForwardedHeader := c.String("proxy-forwarded-header") - proxyTrustedAddresses := util.SplitNoEmpty(c.String("proxy-trusted-addresses"), ",") + proxyTrustedHosts := util.SplitNoEmpty(c.String("proxy-trusted-hosts"), ",") stripeSecretKey := c.String("stripe-secret-key") stripeWebhookKey := c.String("stripe-webhook-key") billingContact := c.String("billing-contact") @@ -358,14 +358,24 @@ func execServe(c *cli.Context) error { } // Resolve hosts - visitorRequestLimitExemptIPs := make([]netip.Prefix, 0) + visitorRequestLimitExemptPrefixes := make([]netip.Prefix, 0) for _, host := range visitorRequestLimitExemptHosts { - ips, err := parseIPHostPrefix(host) + prefixes, err := parseIPHostPrefix(host) if err != nil { log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error()) continue } - visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ips...) + visitorRequestLimitExemptPrefixes = append(visitorRequestLimitExemptPrefixes, prefixes...) + } + + // Parse trusted prefixes + trustedProxyPrefixes := make([]netip.Prefix, 0) + for _, host := range proxyTrustedHosts { + prefixes, err := parseIPHostPrefix(host) + if err != nil { + return fmt.Errorf("cannot resolve trusted proxy host %s: %s", host, err.Error()) + } + trustedProxyPrefixes = append(trustedProxyPrefixes, prefixes...) } // Stripe things @@ -426,7 +436,7 @@ func execServe(c *cli.Context) error { conf.VisitorAttachmentDailyBandwidthLimit = visitorAttachmentDailyBandwidthLimit conf.VisitorRequestLimitBurst = visitorRequestLimitBurst conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish - conf.VisitorRequestExemptIPAddrs = visitorRequestLimitExemptIPs + conf.VisitorRequestExemptPrefixes = visitorRequestLimitExemptPrefixes conf.VisitorMessageDailyLimit = visitorMessageDailyLimit conf.VisitorEmailLimitBurst = visitorEmailLimitBurst conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish @@ -434,7 +444,7 @@ func execServe(c *cli.Context) error { conf.VisitorPrefixBitsIPv6 = visitorPrefixBitsIPv6 conf.BehindProxy = behindProxy conf.ProxyForwardedHeader = proxyForwardedHeader - conf.ProxyTrustedAddresses = proxyTrustedAddresses + conf.ProxyTrustedPrefixes = trustedProxyPrefixes conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact diff --git a/docs/config.md b/docs/config.md index 1687c2ec..6675b875 100644 --- a/docs/config.md +++ b/docs/config.md @@ -18,8 +18,8 @@ get a list of [command line options](#command-line-options). ## Example config !!! info - Definitely check out the **[server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)** file. - It contains examples and detailed descriptions of all the settings. + Definitely check out the **[server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)** file. It contains examples and detailed descriptions of all the settings. + You may also want to look at how ntfy.sh is configured in the [ntfy-ansible](https://github.com/binwiederhier/ntfy-ansible) repository. The most basic settings are `base-url` (the external URL of the ntfy server), the HTTP/HTTPS listen address (`listen-http` and `listen-https`), and socket path (`listen-unix`). All the other things are additional features. @@ -559,7 +559,17 @@ If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. as the primary identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will be counted as one, because from the perspective of the -ntfy server, they all share the proxy's IP address. +ntfy server, they all share the proxy's IP address. + +In IPv4 environments, by default, a visitor's IP address is used as-is for rate limiting (**full IPv4 address**). This +means that if a visitor publishes messages from multiple IP addresses, they will be counted as separate visitors. +You can adjust this by setting the `visitor-prefix-bits-ipv4` config option (default is `32`, which is the entire IP address). +To limit visitors to a /24 subnet, for instance, set it to `24`. In that case, `1.2.3.4` and `1.2.3.99` are treated as the same visitor. + +In IPv6 environments, by default, a visitor's IP address is **truncated to the /64 subnet**, meaning that +`2001:db8::1` and `2001:db8::2` are treated as the same visitor. Use the `visitor-prefix-bits-ipv6` config option to +adjust this behavior (default is `64`, which is the entire /64 subnet). See [IPv6 considerations](#ipv6-considerations) +for more details. Relevant flags to consider: @@ -568,7 +578,7 @@ Relevant flags to consider: * `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`), a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`). -* `proxy-trusted-addresses` is a comma-separated list of IP addresses that are removed from the forwarded header +* `proxy-trusted-hosts` is a comma-separated list of IP addresses, hosts or CIDRs that are removed from the forwarded header to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to the forwarded header (default: empty). @@ -613,7 +623,7 @@ Relevant flags to consider: # the visitor IP will be 9.9.9.9 (right-most unknown address). # behind-proxy: true - proxy-trusted-addresses: "1.2.3.4, 1.2.3.5" + proxy-trusted-hosts: "1.2.3.0/24, 1.2.2.2, 2001:db8::/64" ``` ### TLS/SSL @@ -1138,6 +1148,18 @@ If this ever happens, there will be a log message that looks something like this WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor ``` +### IPv6 considerations +By default, rate limiting for IPv6 is done using the `/64` subnet of the visitor's IPv6 address. This means that all visitors +in the same `/64` subnet are treated as one visitor. This is done to prevent abuse, as IPv6 subnet assignments are typically +much larger than IPv4 subnets (and much cheaper), and it is common for ISPs to assign large subnets to their customers. + +Other than that, rate limiting for IPv6 is done the same way as for IPv4, using the visitor's IP address or subnet to identify them. + +There are two options to configure the number of bits used for rate limiting (for IPv4 and IPv6): + +- `visitor-prefix-bits-ipv4` is number of bits of the IPv4 address to use for rate limiting (default: 32, full address) +- `visitor-prefix-bits-ipv6` is number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet) + ### Subscriber-based rate limiting By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits @@ -1444,7 +1466,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | | `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) | | `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) | -| `proxy-trusted-addresses` | `NTFY_PROXY_TRUSTED_ADDRESSES` | *comma-separated list of IPs* | - | Comma-separated list of trusted IP addresses to remove from forwarded header | +| `proxy-trusted-hosts` | `NTFY_PROXY_TRUSTED_HOSTS` | *comma-separated host/IP/CIDR list* | - | Comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header | | `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | | `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | | `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | @@ -1474,9 +1496,11 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `visitor-message-daily-limit` | `NTFY_VISITOR_MESSAGE_DAILY_LIMIT` | *number* | - | Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset. | | `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | | `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | -| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting | +| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP/CIDR list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting | | `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) | | `visitor-subscriber-rate-limiting` | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING` | *bool* | `false` | Rate limiting: Enables subscriber-based rate limiting | +| `visitor-prefix-bits-ipv4` | `NTFY_VISITOR_PREFIX_BITS_IPV4` | *number* | 32 | Rate limiting: Number of bits to use for IPv4 visitor prefix, e.g. 24 for /24 | +| `visitor-prefix-bits-ipv6` | `NTFY_VISITOR_PREFIX_BITS_IPV6` | *number* | 64 | Rate limiting: Number of bits to use for IPv6 visitor prefix, e.g. 48 for /48 | | `web-root` | `NTFY_WEB_ROOT` | *path*, e.g. `/` or `/app`, or `disable` | `/` | Sets root of the web app (e.g. /, or /app), or disables it entirely (disable) | | `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API | | `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API | @@ -1572,6 +1596,7 @@ OPTIONS: --message-delay-limit value, --message_delay_limit value max duration a message can be scheduled into the future (default: "3d") [$NTFY_MESSAGE_DELAY_LIMIT] --global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT] --visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT] + --visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING] --visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT] --visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT] --visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST] @@ -1580,8 +1605,11 @@ OPTIONS: --visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT] --visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST] --visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: "1h") [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH] - --visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING] - --behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] + --visitor-prefix-bits-ipv4 value, --visitor_prefix_bits_ipv4 value number of bits of the IPv4 address to use for rate limiting (default: 32, full address) (default: 32) [$NTFY_VISITOR_PREFIX_BITS_IPV4] + --visitor-prefix-bits-ipv6 value, --visitor_prefix_bits_ipv6 value number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet) (default: 64) [$NTFY_VISITOR_PREFIX_BITS_IPV6] + --behind-proxy, --behind_proxy, -P if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] + --proxy-forwarded-header value, --proxy_forwarded_header value use specified header to determine visitor IP address (for rate limiting) (default: "X-Forwarded-For") [$NTFY_PROXY_FORWARDED_HEADER] + --proxy-trusted-hosts value, --proxy_trusted_hosts value comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header [$NTFY_PROXY_TRUSTED_HOSTS] --stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY] --stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY] --billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT] @@ -1595,5 +1623,5 @@ OPTIONS: --web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES] --web-push-expiry-duration value, --web_push_expiry_duration value automatically expire unused subscriptions after this time (default: "60d") [$NTFY_WEB_PUSH_EXPIRY_DURATION] --web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value send web push warning notification after this time before expiring unused subscriptions (default: "55d") [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION] - --help, -h show help + --help, -h ``` diff --git a/docs/releases.md b/docs/releases.md index 8bf1cc4e..dca68cc8 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1437,7 +1437,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-addresses` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) +* Full support for IPv6 ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) +* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) ### ntfy Android app v1.16.1 (UNRELEASED) diff --git a/go.mod b/go.mod index 6100a25f..7bddeb07 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,13 @@ require ( github.com/mattn/go-sqlite3 v1.14.28 github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.10.0 - github.com/urfave/cli/v2 v2.27.6 - golang.org/x/crypto v0.38.0 + github.com/urfave/cli/v2 v2.27.7 + golang.org/x/crypto v0.39.0 golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.14.0 + golang.org/x/sync v0.15.0 golang.org/x/term v0.32.0 - golang.org/x/time v0.11.0 - google.golang.org/api v0.235.0 + golang.org/x/time v0.12.0 + google.golang.org/api v0.240.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -30,7 +30,7 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi require github.com/pkg/errors v0.9.1 // indirect require ( - firebase.google.com/go/v4 v4.15.2 + firebase.google.com/go/v4 v4.16.1 github.com/SherClockHolmes/webpush-go v1.4.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.22.0 @@ -39,17 +39,17 @@ require ( require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.2 // indirect - cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go v0.121.3 // indirect + cloud.google.com/go/auth v0.16.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -60,7 +60,7 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -75,30 +75,30 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - golang.org/x/net v0.40.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/grpc v1.72.2 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d73473c5..18815b70 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg= -cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= -cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= -cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo= +cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= @@ -22,20 +22,20 @@ cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2zn cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -firebase.google.com/go/v4 v4.15.2 h1:KJtV4rAfO2CVCp40hBfVk+mqUqg7+jQKx7yOgFDnXBg= -firebase.google.com/go/v4 v4.15.2/go.mod h1:qkD/HtSumrPMTLs0ahQrje5gTw2WKFKrzVFoqy4SbKA= +firebase.google.com/go/v4 v4.16.1 h1:Kl5cgXmM0VOWDGT1UAx6b0T2UFWa14ak0CvYqeI7Py4= +firebase.google.com/go/v4 v4.16.1/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 h1:VaFXBL0NJpiFBtw4aVJpKHeKULVTcHpD+/G0ibZkcBw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0/go.mod h1:JXkPazkEc/dZTHzOlzv2vT1DlpWSTbSLmu/1KY6Ly0I= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 h1:QFgWzcdmJlgEAwJz/zePYVJQxfoJGRtgIqZfIUFg5oQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0/go.mod h1:ayYHuYU7iNcNtEs1K9k6D/Bju7u1VEHMQm5qQ1n3GtM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.52.0 h1:0l8ynskVvq1dvIn5vJbFMf/a/3TqFpRmCMrruFbzlvk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.52.0/go.mod h1:f/ad5NuHnYz8AOZGuR0cY+l36oSCstdxD73YlIchr6I= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 h1:wbMd4eG/fOhsCa6+IP8uEDvWF5vl7rNoUWmP5f72Tbs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0/go.mod h1:gdIm9TxRk5soClCwuB0FtdXsbqtw0aqPwBEurK9tPkw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= @@ -70,8 +70,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= -github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -131,10 +131,10 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -149,41 +149,43 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -198,8 +200,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -209,8 +211,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -247,10 +249,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -259,18 +261,18 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= -google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= +google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20250528174236-200df99c418a h1:KXuwdBmgjb4T3l4ZzXhP6HxxFKXD9FcK5/8qfJI4WwU= -google.golang.org/genproto v0.0.0-20250528174236-200df99c418a/go.mod h1:Nlk93rrS2X7rV8hiC2gh2A/AJspZhElz9Oh2KGsjLEY= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/server/config.go b/server/config.go index 0fc39932..59b11c16 100644 --- a/server/config.go +++ b/server/config.go @@ -135,7 +135,7 @@ type Config struct { VisitorAttachmentDailyBandwidthLimit int64 VisitorRequestLimitBurst int VisitorRequestLimitReplenish time.Duration - VisitorRequestExemptIPAddrs []netip.Prefix + VisitorRequestExemptPrefixes []netip.Prefix VisitorMessageDailyLimit int VisitorEmailLimitBurst int VisitorEmailLimitReplenish time.Duration @@ -143,13 +143,13 @@ type Config struct { VisitorAccountCreationLimitReplenish time.Duration VisitorAuthFailureLimitBurst int VisitorAuthFailureLimitReplenish time.Duration - VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats - VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics - VisitorPrefixBitsIPv4 int // Number of bits for IPv4 rate limiting (default: 32) - VisitorPrefixBitsIPv6 int // Number of bits for IPv6 rate limiting (default: 64) - BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported) - ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported) - ProxyTrustedAddresses []string // List of trusted proxy addresses (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true + VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats + VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics + VisitorPrefixBitsIPv4 int // Number of bits for IPv4 rate limiting (default: 32) + VisitorPrefixBitsIPv6 int // Number of bits for IPv6 rate limiting (default: 64) + BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported) + ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported) + ProxyTrustedPrefixes []netip.Prefix // List of trusted proxy networks (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true StripeSecretKey string StripeWebhookKey string StripePriceCacheDuration time.Duration @@ -159,7 +159,6 @@ type Config struct { EnableReservations bool // Allow users with role "user" to own/reserve topics EnableMetrics bool AccessControlAllowOrigin string // CORS header field to restrict access from web clients - Version string // injected by App WebPushPrivateKey string WebPushPublicKey string WebPushFile string @@ -167,6 +166,7 @@ type Config struct { WebPushStartupQueries string WebPushExpiryDuration time.Duration WebPushExpiryWarningDuration time.Duration + Version string // injected by App } // NewConfig instantiates a default new server config @@ -229,7 +229,7 @@ func NewConfig() *Config { VisitorAttachmentDailyBandwidthLimit: DefaultVisitorAttachmentDailyBandwidthLimit, VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst, VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish, - VisitorRequestExemptIPAddrs: make([]netip.Prefix, 0), + VisitorRequestExemptPrefixes: make([]netip.Prefix, 0), VisitorMessageDailyLimit: DefaultVisitorMessageDailyLimit, VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst, VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish, diff --git a/server/server.go b/server/server.go index 8d33d396..bfa7eb6b 100644 --- a/server/server.go +++ b/server/server.go @@ -760,7 +760,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e // the subscription as invalid if any 400-499 code (except 429/408) is returned. // See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46 return nil, errHTTPInsufficientStorageUnifiedPush.With(t) - } else if !util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) && !vrate.MessageAllowed() { + } else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { return nil, errHTTPTooManyRequestsLimitMessages.With(t) } else if email != "" && !vrate.EmailAllowed() { return nil, errHTTPTooManyRequestsLimitEmails.With(t) @@ -1937,7 +1937,7 @@ func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFun // that subsequent logging calls still have a visitor context. func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) { // Read the "Authorization" header value and exit out early if it's not set - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedPrefixes) vip := s.visitor(ip, nil) if s.userManager == nil { return vip, nil @@ -2012,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us if err != nil { return nil, err } - ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses) + ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedPrefixes) go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{ LastAccess: time.Now(), LastOrigin: ip, diff --git a/server/server.yml b/server/server.yml index 37e0faf5..e1a58232 100644 --- a/server/server.yml +++ b/server/server.yml @@ -105,13 +105,13 @@ # proxy-forwarded-header. Without this, the remote address of the incoming connection is used. # - proxy-forwarded-header is the header to use to identify visitors. It may be a single IP address (e.g. 1.2.3.4), # a comma-separated list of IP addresses (e.g. "1.2.3.4, 5.6.7.8"), or an RFC 7239-style header (e.g. "for=1.2.3.4;by=proxy.example.com, for=5.6.7.8"). -# - proxy-trusted-addresses is a comma-separated list of IP addresses that are removed from the forwarded header +# - proxy-trusted-hosts is a comma-separated list of IP addresses, hostnames or CIDRs that are removed from the forwarded header # to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to # the forwarded header. # # behind-proxy: false # proxy-forwarded-header: "X-Forwarded-For" -# proxy-trusted-addresses: +# proxy-trusted-hosts: # If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments # are "attachment-cache-dir" and "base-url". diff --git a/server/server_middleware.go b/server/server_middleware.go index b2ce6f70..17ae0963 100644 --- a/server/server_middleware.go +++ b/server/server_middleware.go @@ -16,7 +16,7 @@ const ( func (s *Server) limitRequests(next handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { - if util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) { + if util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) { return next(w, r, v) } else if !v.RequestAllowed() { return errHTTPTooManyRequestsLimitRequests @@ -40,7 +40,7 @@ func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc { contextRateVisitor: vrate, contextTopic: t, }) - if util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) { + if util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) { return next(w, r, v) } else if !vrate.RequestAllowed() { return errHTTPTooManyRequestsLimitRequests diff --git a/server/server_test.go b/server/server_test.go index 0a5bcc08..e09f67a2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1225,7 +1225,7 @@ func TestServer_PublishTooManyRequests_IPv6_Slash48(t *testing.T) { func TestServer_PublishTooManyRequests_Defaults_ExemptHosts(t *testing.T) { c := newTestConfig(t) c.VisitorRequestLimitBurst = 3 - c.VisitorRequestExemptIPAddrs = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() + c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() s := newTestServer(t, c) for i := 0; i < 5; i++ { // > 3 response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) @@ -1236,7 +1236,7 @@ func TestServer_PublishTooManyRequests_Defaults_ExemptHosts(t *testing.T) { func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_IPv6(t *testing.T) { c := newTestConfig(t) c.VisitorRequestLimitBurst = 3 - c.VisitorRequestExemptIPAddrs = []netip.Prefix{netip.MustParsePrefix("2001:db8:9999::/48")} + c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:9999::/48")} s := newTestServer(t, c) overrideRemoteAddr := func(r *http.Request) { r.RemoteAddr = "[2001:db8:9999::1]:1234" @@ -1251,7 +1251,7 @@ func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_MessageDailyLimit(t c := newTestConfig(t) c.VisitorRequestLimitBurst = 10 c.VisitorMessageDailyLimit = 4 - c.VisitorRequestExemptIPAddrs = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() + c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() s := newTestServer(t, c) for i := 0; i < 8; i++ { // 4 response := request(t, s, "PUT", "/mytopic", "message", nil) @@ -2318,7 +2318,7 @@ func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) { c := newTestConfig(t) c.BehindProxy = true c.ProxyForwardedHeader = "Forwarded" - c.ProxyTrustedAddresses = []string{"1.2.3.4"} + c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24")} s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) r.RemoteAddr = "8.9.10.11:1234" @@ -2332,7 +2332,7 @@ func TestServer_Visitor_Custom_Forwarded_Header_IPv6(t *testing.T) { c := newTestConfig(t) c.BehindProxy = true c.ProxyForwardedHeader = "Forwarded" - c.ProxyTrustedAddresses = []string{"2001:db8:1111::1"} + c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:1111::/64")} s := newTestServer(t, c) r, _ := http.NewRequest("GET", "/bla", nil) r.RemoteAddr = "[2001:db8:2222::1]:1234" diff --git a/server/util.go b/server/util.go index 687e7d0e..305f63ea 100644 --- a/server/util.go +++ b/server/util.go @@ -9,7 +9,6 @@ import ( "net/http" "net/netip" "regexp" - "slices" "strings" "heckel.io/ntfy/v2/util" @@ -84,9 +83,9 @@ func readQueryParam(r *http.Request, names ...string) string { // extractIPAddress extracts the IP address of the visitor from the request, // either from the TCP socket or from a proxy header. -func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddresses []string) netip.Addr { +func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedPrefixes []netip.Prefix) netip.Addr { if behindProxy && proxyForwardedHeader != "" { - if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddresses); err == nil { + if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedPrefixes); err == nil { return addr } // Fall back to the remote address if the header is not found or invalid @@ -109,7 +108,7 @@ func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader st // If there are multiple addresses, we first remove the trusted IP addresses from the list, and // then take the right-most address in the list (as this is the one added by our proxy server). // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details. -func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) { +func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedPrefixes []netip.Prefix) (netip.Addr, error) { value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader))) if value == "" { return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader) @@ -133,7 +132,12 @@ func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trusted } // Filter out proxy addresses clientAddrs := util.Filter(validAddrs, func(addr netip.Addr) bool { - return !slices.Contains(trustedAddresses, addr.String()) + for _, prefix := range trustedPrefixes { + if prefix.Contains(addr) { + return false // Address is in the trusted range, ignore it + } + } + return true }) if len(clientAddrs) == 0 { return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value) diff --git a/server/util_test.go b/server/util_test.go index 128a8160..13dcb1e9 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -100,7 +100,7 @@ func TestExtractIPAddress(t *testing.T) { r.Header.Set("X-Real-IP", "13.14.15.16, 1.1.1.1") r.Header.Set("Forwarded", "for=17.18.19.20;by=proxy.example.com, by=2.2.2.2;for=1.1.1.1") - trustedProxies := []string{"1.1.1.1"} + trustedProxies := []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")} require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) require.Equal(t, "9.10.11.12", extractIPAddress(r, true, "X-Client-IP", trustedProxies).String()) @@ -115,7 +115,7 @@ func TestExtractIPAddress_UnixSocket(t *testing.T) { r.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8, 1.1.1.1") r.Header.Set("Forwarded", "by=bla.example.com;for=17.18.19.20") - trustedProxies := []string{"1.1.1.1"} + trustedProxies := []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")} require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String()) @@ -126,26 +126,18 @@ func TestExtractIPAddress_MixedIPv4IPv6(t *testing.T) { r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) r.RemoteAddr = "[2001:db8:abcd::1]:1234" r.Header.Set("X-Forwarded-For", "1.2.3.4, 2001:db8:abcd::2, 5.6.7.8") - trustedProxies := []string{"1.2.3.4"} + trustedProxies := []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24")} require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) } func TestExtractIPAddress_TrustedIPv6Prefix(t *testing.T) { r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) r.RemoteAddr = "[2001:db8:abcd::1]:1234" - r.Header.Set("X-Forwarded-For", "2001:db8:abcd::1, 2001:db8:abcd:1::2, 2001:db8:abcd:2::3") - trustedProxies := []string{"2001:db8:abcd::/48"} + r.Header.Set("X-Forwarded-For", "2001:db8:aaaa::1, 2001:db8:aaaa::2, 2001:db8:abcd:2::3") + trustedProxies := []netip.Prefix{netip.MustParsePrefix("2001:db8:aaaa::/48")} require.Equal(t, "2001:db8:abcd:2::3", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) } -func TestExtractIPAddress_EdgeCases(t *testing.T) { - r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) - r.RemoteAddr = "[::ffff:192.0.2.128]:1234" // IPv4-mapped IPv6 - r.Header.Set("X-Forwarded-For", "::ffff:192.0.2.128, 2001:db8:abcd::1") - trustedProxies := []string{"::ffff:192.0.2.128"} - require.Equal(t, "2001:db8:abcd::1", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String()) -} - func TestVisitorID(t *testing.T) { confWithDefaults := &Config{ VisitorPrefixBitsIPv4: 32, diff --git a/web/package-lock.json b/web/package-lock.json index 34e91d7c..1597e504 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -74,9 +74,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", - "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -84,22 +84,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -122,15 +122,15 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", - "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.3", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -208,22 +208,31 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", - "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -386,26 +395,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", - "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", - "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -577,15 +586,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -629,9 +638,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz", - "integrity": "sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -679,18 +688,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", + "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -717,13 +726,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", - "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -798,6 +808,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", @@ -1065,16 +1092,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", - "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.3", - "@babel/plugin-transform-parameters": "^7.27.1" + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1134,9 +1162,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1233,9 +1261,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.4.tgz", - "integrity": "sha512-Glp/0n8xuj+E1588otw5rjJkTXfzW7FjH3IIUrfqiZOPQCd2vbg8e+DQE8jK9g4V5/zrxFW+D9WM9gboRPELpQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz", + "integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1430,13 +1458,13 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", - "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", + "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -1450,19 +1478,20 @@ "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.0", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", @@ -1479,15 +1508,15 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.0", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1500,10 +1529,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "engines": { @@ -1529,9 +1558,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz", - "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1552,27 +1581,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1691,9 +1720,9 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -2218,35 +2247,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -2296,17 +2296,13 @@ "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2318,19 +2314,10 @@ "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/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2339,15 +2326,15 @@ } }, "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==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "license": "MIT" }, "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==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2648,9 +2635,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.9", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", - "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", "dev": true, "license": "MIT" }, @@ -2703,9 +2690,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -2726,9 +2713,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", - "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", + "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", "cpu": [ "arm" ], @@ -2740,9 +2727,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", - "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", + "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", "cpu": [ "arm64" ], @@ -2754,9 +2741,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", - "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", + "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", "cpu": [ "arm64" ], @@ -2768,9 +2755,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", - "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", + "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", "cpu": [ "x64" ], @@ -2782,9 +2769,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", - "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", + "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", "cpu": [ "arm64" ], @@ -2796,9 +2783,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", - "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", + "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", "cpu": [ "x64" ], @@ -2810,9 +2797,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", - "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", + "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", "cpu": [ "arm" ], @@ -2824,9 +2811,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", - "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", + "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", "cpu": [ "arm" ], @@ -2838,9 +2825,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", - "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", + "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", "cpu": [ "arm64" ], @@ -2852,9 +2839,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", - "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", + "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", "cpu": [ "arm64" ], @@ -2866,9 +2853,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", - "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", + "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", "cpu": [ "loong64" ], @@ -2880,9 +2867,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", - "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", + "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", "cpu": [ "ppc64" ], @@ -2894,9 +2881,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", - "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", + "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", "cpu": [ "riscv64" ], @@ -2908,9 +2895,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", - "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", + "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", "cpu": [ "riscv64" ], @@ -2922,9 +2909,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", - "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", + "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", "cpu": [ "s390x" ], @@ -2936,9 +2923,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", - "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", + "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", "cpu": [ "x64" ], @@ -2950,9 +2937,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", - "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", + "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", "cpu": [ "x64" ], @@ -2964,9 +2951,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", - "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", + "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", "cpu": [ "arm64" ], @@ -2978,9 +2965,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", - "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", + "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", "cpu": [ "ia32" ], @@ -2992,9 +2979,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", - "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", + "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", "cpu": [ "x64" ], @@ -3071,9 +3058,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -3100,15 +3087,15 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", - "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "license": "MIT", "peer": true, "dependencies": { @@ -3152,16 +3139,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", - "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@rolldown/pluginutils": "1.0.0-beta.9", + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -3169,13 +3156,13 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -3273,18 +3260,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3499,14 +3488,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", - "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.4", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { @@ -3514,27 +3503,27 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", - "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.4" + "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3558,9 +3547,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3569,9 +3558,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -3589,8 +3578,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -3668,9 +3657,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001720", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", - "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "dev": true, "funding": [ { @@ -3812,13 +3801,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.0" }, "funding": { "type": "opencollective", @@ -4105,9 +4094,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.161", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz", - "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==", + "version": "1.5.179", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", + "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", "dev": true, "license": "ISC" }, @@ -4511,9 +4500,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -4539,30 +4528,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -4732,35 +4721,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4887,9 +4847,9 @@ } }, "node_modules/fdir": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", - "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4925,9 +4885,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5196,12 +5156,19 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -5385,9 +5352,9 @@ } }, "node_modules/humanize-duration": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.2.tgz", - "integrity": "sha512-jcTwWYeCJf4dN5GJnjBmHd42bNyK94lY49QTkrsAQrMTUoIYLevvDpmQtg5uv8ZrdIRIbzdasmSNZ278HHUPEg==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.0.tgz", + "integrity": "sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ==", "license": "Unlicense" }, "node_modules/i18next": { @@ -6895,9 +6862,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -7397,13 +7364,13 @@ } }, "node_modules/rollup": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", - "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", + "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -7413,26 +7380,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.1", - "@rollup/rollup-android-arm64": "4.41.1", - "@rollup/rollup-darwin-arm64": "4.41.1", - "@rollup/rollup-darwin-x64": "4.41.1", - "@rollup/rollup-freebsd-arm64": "4.41.1", - "@rollup/rollup-freebsd-x64": "4.41.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", - "@rollup/rollup-linux-arm-musleabihf": "4.41.1", - "@rollup/rollup-linux-arm64-gnu": "4.41.1", - "@rollup/rollup-linux-arm64-musl": "4.41.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-musl": "4.41.1", - "@rollup/rollup-linux-s390x-gnu": "4.41.1", - "@rollup/rollup-linux-x64-gnu": "4.41.1", - "@rollup/rollup-linux-x64-musl": "4.41.1", - "@rollup/rollup-win32-arm64-msvc": "4.41.1", - "@rollup/rollup-win32-ia32-msvc": "4.41.1", - "@rollup/rollup-win32-x64-msvc": "4.41.1", + "@rollup/rollup-android-arm-eabi": "4.44.2", + "@rollup/rollup-android-arm64": "4.44.2", + "@rollup/rollup-darwin-arm64": "4.44.2", + "@rollup/rollup-darwin-x64": "4.44.2", + "@rollup/rollup-freebsd-arm64": "4.44.2", + "@rollup/rollup-freebsd-x64": "4.44.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", + "@rollup/rollup-linux-arm-musleabihf": "4.44.2", + "@rollup/rollup-linux-arm64-gnu": "4.44.2", + "@rollup/rollup-linux-arm64-musl": "4.44.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-musl": "4.44.2", + "@rollup/rollup-linux-s390x-gnu": "4.44.2", + "@rollup/rollup-linux-x64-gnu": "4.44.2", + "@rollup/rollup-linux-x64-musl": "4.44.2", + "@rollup/rollup-win32-arm64-msvc": "4.44.2", + "@rollup/rollup-win32-ia32-msvc": "4.44.2", + "@rollup/rollup-win32-x64-msvc": "4.44.2", "fsevents": "~2.3.2" } }, @@ -8089,10 +8056,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terser": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz", - "integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8197,9 +8177,9 @@ } }, "node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8626,9 +8606,9 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.0.0.tgz", - "integrity": "sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.0.1.tgz", + "integrity": "sha512-STyUomQbydj7vGamtgQYIJI0YsUZ3T4pJLGBQDQPhzMse6aGSncmEN21OV35PrFsmCvmtiH+Nu1JS1ke4RqBjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8646,7 +8626,7 @@ }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, From 8f60294c5b4cc8f0d0720474a869a6014d86b2b3 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 5 Jul 2025 22:48:45 +0200 Subject: [PATCH 153/378] Docs --- docs/config.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/config.md b/docs/config.md index 6675b875..0e677f75 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1324,6 +1324,22 @@ Note that if you run nginx in a container, append `, chain=DOCKER-USER` to the j is `INPUT`, but `FORWARD` is used when using docker networks. `DOCKER-USER`, available when using docker, is part of the `FORWARD` chain. +## IPv6 support +ntfy fully supports IPv6, though there are a few things to keep in mind. + +- **Listening on an IPv6 address**: By default, ntfy listens on `:80` (IPv4-only). If you want to listen on an IPv6 address, you need to + explicitly set the `listen-http` and/or `listen-https` options in your `server.yml` file to an IPv6 address, e.g. `[::]:80`. Alternatively, + if you're running ntfy behind a reverse proxy, make sure that the proxy is configured to listen on an IPv6 address (e.g. `listen [::]:80;` in nginx). +- **Rate limiting:** By default, ntfy uses the `/64` subnet of the visitor's IPv6 address for rate limiting. This means that all visitors in the same `/64` + subnet are treated as one visitor. If you want to change this, you can set the `visitor-prefix-bits-ipv6` option in your `server.yml` file to a different + value (e.g. `48` for `/48` subnets). See [IPv6 considerations](#ipv6-considerations) and [IP-based rate limiting](#ip-based-rate-limiting) for more details. +- **Banning IPs with fail2ban:** If you use fail2ban to ban IPs, please ensure that your `actionban` and `actionunban` commands + support IPv6 and also ban the entire prefix (e.g. `/48`). See [Banning bad actors](#banning-bad-actors-fail2ban) for details. + +!!! info + The official ntfy.sh server supports IPv6. Check out ntfy.sh's [Ansible repository](https://github.com/binwiederhier/ntfy-ansible) for examples of how to + configure [ntfy](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/ntfy), [nginx](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/nginx) and [fail2ban](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban). + ## Health checks A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below. If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy. From 6fbcd85d17568fed75d847c563f75d04bdaf51ca Mon Sep 17 00:00:00 2001 From: srevn Date: Sun, 6 Jul 2025 10:23:32 +0300 Subject: [PATCH 154/378] Add piping support --- cmd/publish.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cmd/publish.go b/cmd/publish.go index aaec35e9..89475fbd 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -254,6 +254,16 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com if c.String("message") != "" { message = c.String("message") } + + // If no message provided and stdin has data, read from stdin + if message == "" && stdinHasData() { + var stdinBytes []byte + stdinBytes, err = io.ReadAll(c.App.Reader) + if err != nil { + return + } + message = strings.TrimSpace(string(stdinBytes)) + } return } @@ -312,3 +322,11 @@ func runAndWaitForCommand(command []string) (message string, err error) { log.Debug("Command succeeded after %s: %s", runtime, prettyCmd) return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil } + +func stdinHasData() bool { + stat, err := os.Stdin.Stat() + if err != nil { + return false + } + return (stat.Mode() & os.ModeCharDevice) == 0 +} From 04aff72631b1b23a7a327cee097023211bed009e Mon Sep 17 00:00:00 2001 From: srevn Date: Sun, 6 Jul 2025 10:51:28 +0300 Subject: [PATCH 155/378] Add example and logging --- cmd/publish.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/publish.go b/cmd/publish.go index 89475fbd..1d04b537 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -69,6 +69,7 @@ Examples: ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment + echo 'message' | ntfy publish mytopic # Send message from stdin ntfy pub -u phil:mypass secret Psst # Publish with username/password ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes @@ -260,6 +261,7 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com var stdinBytes []byte stdinBytes, err = io.ReadAll(c.App.Reader) if err != nil { + log.Debug("Failed to read from stdin: %v", err) return } message = strings.TrimSpace(string(stdinBytes)) @@ -326,6 +328,7 @@ func runAndWaitForCommand(command []string) (message string, err error) { func stdinHasData() bool { stat, err := os.Stdin.Stat() if err != nil { + log.Debug("Failed to stat stdin: %v", err) return false } return (stat.Mode() & os.ModeCharDevice) == 0 From 9ed96e5d8b16944d40a6e2e6a2ddf924112a2fc5 Mon Sep 17 00:00:00 2001 From: srevn Date: Sun, 6 Jul 2025 16:31:03 +0300 Subject: [PATCH 156/378] Small cosmetic fixes --- cmd/publish.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/cmd/publish.go b/cmd/publish.go index 1d04b537..d1ccf79a 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -255,16 +255,14 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com if c.String("message") != "" { message = c.String("message") } - - // If no message provided and stdin has data, read from stdin - if message == "" && stdinHasData() { - var stdinBytes []byte - stdinBytes, err = io.ReadAll(c.App.Reader) + if message == "" && isStdinRedirected() { + var bytes []byte + bytes, err = io.ReadAll(c.App.Reader) if err != nil { - log.Debug("Failed to read from stdin: %v", err) + log.Debug("Failed to read from stdin: %s", err.Error()) return } - message = strings.TrimSpace(string(stdinBytes)) + message = strings.TrimSpace(string(bytes)) } return } @@ -325,10 +323,10 @@ func runAndWaitForCommand(command []string) (message string, err error) { return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil } -func stdinHasData() bool { +func isStdinRedirected() bool { stat, err := os.Stdin.Stat() if err != nil { - log.Debug("Failed to stat stdin: %v", err) + log.Debug("Failed to stat stdin: %s", err.Error()) return false } return (stat.Mode() & os.ModeCharDevice) == 0 From 47da3aeea6ab36cbe7f5990282bb1028334ba8c6 Mon Sep 17 00:00:00 2001 From: srevn Date: Sun, 6 Jul 2025 17:53:04 +0300 Subject: [PATCH 157/378] fix unbounded read --- cmd/publish.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/publish.go b/cmd/publish.go index d1ccf79a..c15761ab 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -256,13 +256,13 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com message = c.String("message") } if message == "" && isStdinRedirected() { - var bytes []byte - bytes, err = io.ReadAll(c.App.Reader) + var data []byte + data, err = io.ReadAll(io.LimitReader(c.App.Reader, 1024*1024)) if err != nil { log.Debug("Failed to read from stdin: %s", err.Error()) return } - message = strings.TrimSpace(string(bytes)) + message = strings.TrimSpace(string(data)) } return } From 4578835a8f049dc7e7d859b9a1dc2c426f78aeb5 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 7 Jul 2025 11:04:33 +0200 Subject: [PATCH 158/378] stdin --- docs/releases.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/releases.md b/docs/releases.md index dca68cc8..266f68e6 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1439,6 +1439,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Full support for IPv6 ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) +* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) ### ntfy Android app v1.16.1 (UNRELEASED) From 19a4e95a3a897326e108f6d9306c8beaf6b18987 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 7 Jul 2025 16:49:15 +0200 Subject: [PATCH 159/378] Docs --- docs/config.md | 41 ++++++++++++++++++++++++++++------------- docs/releases.md | 2 +- server/util_test.go | 4 ++++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/config.md b/docs/config.md index cbf34847..be15c9fc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -559,16 +559,6 @@ as the primary identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address. -In IPv4 environments, by default, a visitor's IP address is used as-is for rate limiting (**full IPv4 address**). This -means that if a visitor publishes messages from multiple IP addresses, they will be counted as separate visitors. -You can adjust this by setting the `visitor-prefix-bits-ipv4` config option (default is `32`, which is the entire IP address). -To limit visitors to a /24 subnet, for instance, set it to `24`. In that case, `1.2.3.4` and `1.2.3.99` are treated as the same visitor. - -In IPv6 environments, by default, a visitor's IP address is **truncated to the /64 subnet**, meaning that -`2001:db8::1` and `2001:db8::2` are treated as the same visitor. Use the `visitor-prefix-bits-ipv6` config option to -adjust this behavior (default is `64`, which is the entire /64 subnet). See [IPv6 considerations](#ipv6-considerations) -for more details. - Relevant flags to consider: * `behind-proxy` makes it so that the real visitor IP address is extracted from the header defined in `proxy-forwarded-header`. @@ -579,6 +569,14 @@ Relevant flags to consider: * `proxy-trusted-hosts` is a comma-separated list of IP addresses, hosts or CIDRs that are removed from the forwarded header to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to the forwarded header (default: empty). +* `visitor-prefix-bits-ipv4` is the number of bits of the IPv4 address to use for rate limiting (default is `32`, which is the entire + IP address). In IPv4 environments, by default, a visitor's **full IPv4 address** is used as-is for rate limiting. This means that + if someone publishes messages from multiple IP addresses, they will be counted as separate visitors. You can adjust this by setting the `visitor-prefix-bits-ipv4` config option. To group visitors in a /24 subnet and count them as one, for instance, + set it to `24`. In that case, `1.2.3.4` and `1.2.3.99` are treated as the same visitor. +* `visitor-prefix-bits-ipv6` is the number of bits of the IPv6 address to use for rate limiting (default is `64`, which is a /64 subnet). + In IPv6 environments, by default, a visitor's IP address is **truncated to the /64 subnet**, meaning that `2001:db8:25:86:1::1` and + `2001:db8:25:86:2::1` are treated as the same visitor. Use the `visitor-prefix-bits-ipv6` config option to adjust this behavior. + See [IPv6 considerations](#ipv6-considerations) for more details. === "/etc/ntfy/server.yml (behind a proxy)" ``` yaml @@ -624,6 +622,20 @@ Relevant flags to consider: proxy-trusted-hosts: "1.2.3.0/24, 1.2.2.2, 2001:db8::/64" ``` +=== "/etc/ntfy/server.yml (adjusted IPv4/IPv6 prefixes proxies)" + ``` yaml + # Tell ntfy to treat visitors as being in a /24 subnet (IPv4) or /48 subnet (IPv6) + # as one visitor, so that they are counted as one for rate limiting. + # + # Example 1: If 1.2.3.4 and 1.2.3.5 publish a message, the visitor 1.2.3.0 will have + # used 2 messages. + # Example 2: If 2001:db8:2500:1::1 and 2001:db8:2500:2::1 publish a message, the visitor + # 2001:db8:2500:: will have used 2 messages. + # + visitor-prefix-bits-ipv4: 24 + visitor-prefix-bits-ipv6: 48 + ``` + ### TLS/SSL ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below). @@ -1322,16 +1334,19 @@ Note that if you run nginx in a container, append `, chain=DOCKER-USER` to the j is `INPUT`, but `FORWARD` is used when using docker networks. `DOCKER-USER`, available when using docker, is part of the `FORWARD` chain. +The official ntfy.sh server uses fail2ban to ban IPs. Check out ntfy.sh's [Ansible fail2ban role](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban) for details. Ban actors are banned for 1 hour initially, and up to +4 hours at a time for repeated offenses. IPv4 addresses are banned individually, while IPv6 addresses are banned by their `/56` prefix. + ## IPv6 support ntfy fully supports IPv6, though there are a few things to keep in mind. - **Listening on an IPv6 address**: By default, ntfy listens on `:80` (IPv4-only). If you want to listen on an IPv6 address, you need to - explicitly set the `listen-http` and/or `listen-https` options in your `server.yml` file to an IPv6 address, e.g. `[::]:80`. Alternatively, - if you're running ntfy behind a reverse proxy, make sure that the proxy is configured to listen on an IPv6 address (e.g. `listen [::]:80;` in nginx). + explicitly set the `listen-http` and/or `listen-https` options in your `server.yml` file to an IPv6 address, e.g. `[::]:80`. To listen on + IPv4 and IPv6, you must run ntfy behind a reverse proxy, e.g. `listen :80; listen [::]:80;` in nginx. - **Rate limiting:** By default, ntfy uses the `/64` subnet of the visitor's IPv6 address for rate limiting. This means that all visitors in the same `/64` subnet are treated as one visitor. If you want to change this, you can set the `visitor-prefix-bits-ipv6` option in your `server.yml` file to a different value (e.g. `48` for `/48` subnets). See [IPv6 considerations](#ipv6-considerations) and [IP-based rate limiting](#ip-based-rate-limiting) for more details. -- **Banning IPs with fail2ban:** If you use fail2ban to ban IPs, please ensure that your `actionban` and `actionunban` commands +- **Banning IPs with fail2ban:** By default, if you're using the `iptables-multiport` action, fail2ban bans individual IPv4 and IPv6 addresses via `iptables` and `ip6tables`. While this behavior is fine for IPv4, it is not for IPv6, because every host can technically have up to 2^64 addresses. Please ensure that your `actionban` and `actionunban` commands support IPv6 and also ban the entire prefix (e.g. `/48`). See [Banning bad actors](#banning-bad-actors-fail2ban) for details. !!! info diff --git a/docs/releases.md b/docs/releases.md index 266f68e6..c3dc54e1 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1437,7 +1437,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* Full support for IPv6 ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) +* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) * Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) diff --git a/server/util_test.go b/server/util_test.go index 13dcb1e9..9530ca6a 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -153,8 +153,12 @@ func TestVisitorID(t *testing.T) { } require.Equal(t, "ip:1.2.3.4", visitorID(netip.MustParseAddr("1.2.3.4"), nil, confWithDefaults)) require.Equal(t, "ip:2a01:599:b26:2397::", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), nil, confWithDefaults)) + require.Equal(t, "ip:2001:db8:25:86::", visitorID(netip.MustParseAddr("2001:db8:25:86:1::1"), nil, confWithDefaults)) + require.Equal(t, "ip:2001:db8:25:86::", visitorID(netip.MustParseAddr("2001:db8:25:86:2::1"), nil, confWithDefaults)) + require.Equal(t, "user:u_123", visitorID(netip.MustParseAddr("1.2.3.4"), userWithTier, confWithDefaults)) require.Equal(t, "user:u_123", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), userWithTier, confWithDefaults)) + require.Equal(t, "ip:1.2.0.0", visitorID(netip.MustParseAddr("1.2.3.4"), nil, confWithShortenedPrefixes)) require.Equal(t, "ip:2a01:599:b26:2300::", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), nil, confWithShortenedPrefixes)) } From de7b7218e41c2d5a35259ba6a84e5689d89b21e4 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 7 Jul 2025 18:28:16 +0200 Subject: [PATCH 160/378] Add languages --- go.sum | 1 + web/package-lock.json | 14 +++++++------- web/src/components/Preferences.jsx | 4 ++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/go.sum b/go.sum index 18815b70..6224fb65 100644 --- a/go.sum +++ b/go.sum @@ -263,6 +263,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= diff --git a/web/package-lock.json b/web/package-lock.json index 1597e504..ea4962a4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3657,9 +3657,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true, "funding": [ { @@ -3801,13 +3801,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.43.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", - "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.0" + "browserslist": "^4.25.1" }, "funding": { "type": "opencollective", diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index c733c23c..8621a263 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -576,8 +576,10 @@ const Language = () => { 简体中文 Dansk Deutsch + Eesti Español Français + Galego Italiano Magyar 한국어 @@ -589,6 +591,8 @@ const Language = () => { Português (Brasil) Polski Русский + Română + Slovenčina Suomi Svenska Türkçe From 1edbda4f318239865f5c0c742b4be1a229ab55af Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 7 Jul 2025 18:34:05 +0200 Subject: [PATCH 161/378] Release notes --- docs/releases.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index c3dc54e1..0877527e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1441,6 +1441,11 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) * Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) +**Languages** + +* Update new languages from Weblate. Thanks to all the contributors! +* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app + ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** From f5247c50f4276b064880e7542271a873feb1540a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 7 Jul 2025 21:24:43 +0200 Subject: [PATCH 162/378] Bump --- go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/go.sum b/go.sum index 6224fb65..18815b70 100644 --- a/go.sum +++ b/go.sum @@ -263,7 +263,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= From efef5876717f82e9a2b449ff590dd65a7ae8c02c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 7 Jul 2025 22:36:01 +0200 Subject: [PATCH 163/378] WIP: Predefined users --- cmd/serve.go | 3 +++ cmd/user.go | 1 - server/config.go | 1 + server/server.go | 9 +++++++- user/manager.go | 55 +++++++++++++++++++++++++++++++----------------- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index ef4d98d5..516356c5 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -52,6 +52,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), @@ -157,6 +158,7 @@ func execServe(c *cli.Context) error { authFile := c.String("auth-file") authStartupQueries := c.String("auth-startup-queries") authDefaultAccess := c.String("auth-default-access") + authUsers := c.StringSlice("auth-users") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -406,6 +408,7 @@ func execServe(c *cli.Context) error { conf.AuthFile = authFile conf.AuthStartupQueries = authStartupQueries conf.AuthDefault = authDefault + conf.AuthUsers = nil // FIXME conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit diff --git a/cmd/user.go b/cmd/user.go index e6867b11..9902dace 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -94,7 +94,6 @@ Example: You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass directly the bcrypt hash. This is useful if you are updating users via scripts. - `, }, { diff --git a/server/config.go b/server/config.go index 59b11c16..67554021 100644 --- a/server/config.go +++ b/server/config.go @@ -93,6 +93,7 @@ type Config struct { AuthFile string AuthStartupQueries string AuthDefault user.Permission + AuthUsers []user.User AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration AttachmentCacheDir string diff --git a/server/server.go b/server/server.go index bfa7eb6b..10ad7d8e 100644 --- a/server/server.go +++ b/server/server.go @@ -189,7 +189,14 @@ func New(conf *Config) (*Server, error) { } var userManager *user.Manager if conf.AuthFile != "" { - userManager, err = user.NewManager(conf.AuthFile, conf.AuthStartupQueries, conf.AuthDefault, conf.AuthBcryptCost, conf.AuthStatsQueueWriterInterval) + authConfig := &user.Config{ + Filename: conf.AuthFile, + StartupQueries: conf.AuthStartupQueries, + DefaultAccess: conf.AuthDefault, + BcryptCost: conf.AuthBcryptCost, + QueueWriterInterval: conf.AuthStatsQueueWriterInterval, + } + userManager, err = user.NewManager(authConfig) if err != nil { return nil, err } diff --git a/user/manager.go b/user/manager.go index 814ee827..04c3c878 100644 --- a/user/manager.go +++ b/user/manager.go @@ -441,36 +441,53 @@ var ( // Manager is an implementation of Manager. It stores users and access control list // in a SQLite database. type Manager struct { - db *sql.DB - defaultAccess Permission // Default permission if no ACL matches - statsQueue map[string]*Stats // "Queue" to asynchronously write user stats to the database (UserID -> Stats) - tokenQueue map[string]*TokenUpdate // "Queue" to asynchronously write token access stats to the database (Token ID -> TokenUpdate) - bcryptCost int // Makes testing easier - mu sync.Mutex + config *Config + db *sql.DB + statsQueue map[string]*Stats // "Queue" to asynchronously write user stats to the database (UserID -> Stats) + tokenQueue map[string]*TokenUpdate // "Queue" to asynchronously write token access stats to the database (Token ID -> TokenUpdate) + mu sync.Mutex +} + +type Config struct { + Filename string + StartupQueries string + DefaultAccess Permission // Default permission if no ACL matches + ProvisionedUsers []*User // Predefined users to create on startup + ProvisionedAccess map[string][]*Grant // Predefined access grants to create on startup + BcryptCost int // Makes testing easier + QueueWriterInterval time.Duration } var _ Auther = (*Manager)(nil) // NewManager creates a new Manager instance -func NewManager(filename, startupQueries string, defaultAccess Permission, bcryptCost int, queueWriterInterval time.Duration) (*Manager, error) { - db, err := sql.Open("sqlite3", filename) +func NewManager(config *Config) (*Manager, error) { + // Set defaults + if config.BcryptCost <= 0 { + config.BcryptCost = DefaultUserPasswordBcryptCost + } + if config.QueueWriterInterval.Seconds() <= 0 { + config.QueueWriterInterval = DefaultUserStatsQueueWriterInterval + } + + // Open DB and run setup queries + db, err := sql.Open("sqlite3", config.Filename) if err != nil { return nil, err } if err := setupDB(db); err != nil { return nil, err } - if err := runStartupQueries(db, startupQueries); err != nil { + if err := runStartupQueries(db, config.StartupQueries); err != nil { return nil, err } manager := &Manager{ - db: db, - defaultAccess: defaultAccess, - statsQueue: make(map[string]*Stats), - tokenQueue: make(map[string]*TokenUpdate), - bcryptCost: bcryptCost, + db: db, + config: config, + statsQueue: make(map[string]*Stats), + tokenQueue: make(map[string]*TokenUpdate), } - go manager.asyncQueueWriter(queueWriterInterval) + go manager.asyncQueueWriter(config.QueueWriterInterval) return manager, nil } @@ -843,7 +860,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error { } defer rows.Close() if !rows.Next() { - return a.resolvePerms(a.defaultAccess, perm) + return a.resolvePerms(a.config.DefaultAccess, perm) } var read, write bool if err := rows.Scan(&read, &write); err != nil { @@ -873,7 +890,7 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err if hashed { hash = []byte(password) } else { - hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) if err != nil { return err } @@ -1205,7 +1222,7 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error { if hashed { hash = []byte(password) } else { - hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) if err != nil { return err } @@ -1387,7 +1404,7 @@ func (a *Manager) RemoveReservations(username string, topics ...string) error { // DefaultAccess returns the default read/write access if no access control entry matches func (a *Manager) DefaultAccess() Permission { - return a.defaultAccess + return a.config.DefaultAccess } // AddTier creates a new tier in the database From 1f2c76e63d3c256f335918f9653527ed5526c6a6 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Mon, 7 Jul 2025 22:23:32 -0600 Subject: [PATCH 164/378] copy subset of Sprig template functions --- README.md | 1 + docs/publish.md | 5 +- docs/releases.md | 1 + docs/sprig.md | 24 ++ docs/sprig/conversion.md | 36 +++ docs/sprig/crypto.md | 41 +++ docs/sprig/date.md | 126 ++++++++ docs/sprig/defaults.md | 169 ++++++++++ docs/sprig/dicts.md | 172 ++++++++++ docs/sprig/encoding.md | 6 + docs/sprig/flow_control.md | 11 + docs/sprig/integer_slice.md | 41 +++ docs/sprig/lists.md | 188 +++++++++++ docs/sprig/math.md | 78 +++++ docs/sprig/os.md | 24 ++ docs/sprig/paths.md | 114 +++++++ docs/sprig/reflection.md | 50 +++ docs/sprig/semver.md | 151 +++++++++ docs/sprig/string_slice.md | 72 +++++ docs/sprig/strings.md | 309 ++++++++++++++++++ docs/sprig/url.md | 33 ++ docs/sprig/uuid.md | 9 + mkdocs.yml | 1 + server/server.go | 7 +- server/server_test.go | 45 +++ util/sprig/LICENSE.txt | 19 ++ util/sprig/crypto.go | 37 +++ util/sprig/crypto_test.go | 54 ++++ util/sprig/date.go | 152 +++++++++ util/sprig/date_test.go | 120 +++++++ util/sprig/defaults.go | 163 ++++++++++ util/sprig/defaults_test.go | 196 +++++++++++ util/sprig/dict.go | 118 +++++++ util/sprig/dict_test.go | 166 ++++++++++ util/sprig/doc.go | 19 ++ util/sprig/example_test.go | 25 ++ util/sprig/flow_control_test.go | 16 + util/sprig/functions.go | 302 +++++++++++++++++ util/sprig/functions_linux_test.go | 28 ++ util/sprig/functions_test.go | 70 ++++ util/sprig/functions_windows_test.go | 28 ++ util/sprig/list.go | 464 +++++++++++++++++++++++++++ util/sprig/list_test.go | 364 +++++++++++++++++++++ util/sprig/numeric.go | 228 +++++++++++++ util/sprig/numeric_test.go | 307 ++++++++++++++++++ util/sprig/reflect.go | 28 ++ util/sprig/reflect_test.go | 73 +++++ util/sprig/regex.go | 83 +++++ util/sprig/regex_test.go | 203 ++++++++++++ util/sprig/strings.go | 189 +++++++++++ util/sprig/strings_test.go | 233 ++++++++++++++ util/sprig/url.go | 66 ++++ util/sprig/url_test.go | 87 +++++ 53 files changed, 5550 insertions(+), 2 deletions(-) create mode 100644 docs/sprig.md create mode 100644 docs/sprig/conversion.md create mode 100644 docs/sprig/crypto.md create mode 100644 docs/sprig/date.md create mode 100644 docs/sprig/defaults.md create mode 100644 docs/sprig/dicts.md create mode 100644 docs/sprig/encoding.md create mode 100644 docs/sprig/flow_control.md create mode 100644 docs/sprig/integer_slice.md create mode 100644 docs/sprig/lists.md create mode 100644 docs/sprig/math.md create mode 100644 docs/sprig/os.md create mode 100644 docs/sprig/paths.md create mode 100644 docs/sprig/reflection.md create mode 100644 docs/sprig/semver.md create mode 100644 docs/sprig/string_slice.md create mode 100644 docs/sprig/strings.md create mode 100644 docs/sprig/url.md create mode 100644 docs/sprig/uuid.md create mode 100644 util/sprig/LICENSE.txt create mode 100644 util/sprig/crypto.go create mode 100644 util/sprig/crypto_test.go create mode 100644 util/sprig/date.go create mode 100644 util/sprig/date_test.go create mode 100644 util/sprig/defaults.go create mode 100644 util/sprig/defaults_test.go create mode 100644 util/sprig/dict.go create mode 100644 util/sprig/dict_test.go create mode 100644 util/sprig/doc.go create mode 100644 util/sprig/example_test.go create mode 100644 util/sprig/flow_control_test.go create mode 100644 util/sprig/functions.go create mode 100644 util/sprig/functions_linux_test.go create mode 100644 util/sprig/functions_test.go create mode 100644 util/sprig/functions_windows_test.go create mode 100644 util/sprig/list.go create mode 100644 util/sprig/list_test.go create mode 100644 util/sprig/numeric.go create mode 100644 util/sprig/numeric_test.go create mode 100644 util/sprig/reflect.go create mode 100644 util/sprig/reflect_test.go create mode 100644 util/sprig/regex.go create mode 100644 util/sprig/regex_test.go create mode 100644 util/sprig/strings.go create mode 100644 util/sprig/strings_test.go create mode 100644 util/sprig/url.go create mode 100644 util/sprig/url_test.go diff --git a/README.md b/README.md index 61591ca6..9942e138 100644 --- a/README.md +++ b/README.md @@ -253,3 +253,4 @@ Third-party libraries and resources: * [Statically linking go-sqlite3](https://www.arp242.net/static-go.html) * [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs) * [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications +* [Sprig](https://github.com/Masterminds/sprig) (MIT) is used to add template parsing functions diff --git a/docs/publish.md b/docs/publish.md index 25bff035..91f75e3d 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -953,13 +953,16 @@ is valid JSON). You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes` or `1`, or (more appropriately for webhooks) by setting the `?template=yes` query parameter. Then, include templates in your `message` and/or `title`, using the following stanzas (see [Go docs](https://pkg.go.dev/text/template) for detailed syntax): -* Variables,, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` +* Variables, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` * Conditionals (if/else, e.g. `{{if eq .action "opened"}}..{{else}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6Ilt7ey5wdWxsX3JlcXVlc3QuaGVhZC5yZXBvLmZ1bGxfbmFtZX19XSBQdWxsIHJlcXVlc3Qge3tpZiBlcSAuYWN0aW9uIFwib3BlbmVkXCJ9fU9QRU5FRHt7ZWxzZX19Q0xPU0VEe3tlbmR9fToge3sucHVsbF9yZXF1ZXN0LnRpdGxlfX0iLCJpbnB1dCI6IntcbiAgXCJhY3Rpb25cIjogXCJvcGVuZWRcIixcbiAgXCJudW1iZXJcIjogMSxcbiAgXCJwdWxsX3JlcXVlc3RcIjoge1xuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy8xXCIsXG4gICAgXCJpZFwiOiAxNzgzNDIwOTcyLFxuICAgIFwibm9kZV9pZFwiOiBcIlBSX2t3RE9IQWJkbzg1cVROZ3NcIixcbiAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiLFxuICAgIFwiZGlmZl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUvcHVsbC8xLmRpZmZcIixcbiAgICBcInBhdGNoX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsLzEucGF0Y2hcIixcbiAgICBcImlzc3VlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIixcbiAgICBcIm51bWJlclwiOiAxLFxuICAgIFwic3RhdGVcIjogXCJvcGVuXCIsXG4gICAgXCJsb2NrZWRcIjogZmFsc2UsXG4gICAgXCJ0aXRsZVwiOiBcIkEgc2FtcGxlIFBSIGZyb20gUGhpbFwiLFxuICAgIFwidXNlclwiOiB7XG4gICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgfSxcbiAgICBcImJvZHlcIjogbnVsbCxcbiAgICBcImNyZWF0ZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjA5WlwiLFxuICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjQtMDMtMjFUMDI6NTI6MDlaXCIsXG4gICAgXCJjbG9zZWRfYXRcIjogbnVsbCxcbiAgICBcIm1lcmdlZF9hdFwiOiBudWxsLFxuICAgIFwibWVyZ2VfY29tbWl0X3NoYVwiOiBudWxsLFxuICAgIFwiYXNzaWduZWVcIjogbnVsbCxcbiAgICBcImFzc2lnbmVlc1wiOiBbXSxcbiAgICBcInJlcXVlc3RlZF9yZXZpZXdlcnNcIjogW10sXG4gICAgXCJyZXF1ZXN0ZWRfdGVhbXNcIjogW10sXG4gICAgXCJsYWJlbHNcIjogW10sXG4gICAgXCJtaWxlc3RvbmVcIjogbnVsbCxcbiAgICBcImRyYWZ0XCI6IGZhbHNlLFxuICAgIFwiY29tbWl0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzLzEvY29tbWl0c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvMS9jb21tZW50c1wiLFxuICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCIsXG4gICAgXCJoZWFkXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOmFhXCIsXG4gICAgICBcInJlZlwiOiBcImFhXCIsXG4gICAgICBcInNoYVwiOiBcIjU3MDM4NDJjYzU3MTVlZDFlMzU4ZDIzZWJiNjkzZGIwOTc0N2FlOWJcIixcbiAgICAgIFwidXNlclwiOiB7XG4gICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgICB9LFxuICAgICAgXCJyZXBvXCI6IHtcbiAgICAgICAgXCJpZFwiOiA0NzAyMTIwMDMsXG4gICAgICAgIFwibm9kZV9pZFwiOiBcIlJfa2dET0hBYmRvd1wiLFxuICAgICAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICAgICAgXCJmdWxsX25hbWVcIjogXCJiaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcInByaXZhdGVcIjogZmFsc2UsXG4gICAgICAgIFwib3duZXJcIjoge1xuICAgICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgICB9LFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgICAgIFwiZm9ya1wiOiBmYWxzZSxcbiAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgICAgICBcImtleXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9rZXlzey9rZXlfaWR9XCIsXG4gICAgICAgIFwiY29sbGFib3JhdG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbGxhYm9yYXRvcnN7L2NvbGxhYm9yYXRvcn1cIixcbiAgICAgICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgICAgIFwiaG9va3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ob29rc1wiLFxuICAgICAgICBcImlzc3VlX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9ldmVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICAgICAgXCJhc3NpZ25lZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9hc3NpZ25lZXN7L3VzZXJ9XCIsXG4gICAgICAgIFwiYnJhbmNoZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9icmFuY2hlc3svYnJhbmNofVwiLFxuICAgICAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgICAgIFwiYmxvYnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvYmxvYnN7L3NoYX1cIixcbiAgICAgICAgXCJnaXRfdGFnc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90YWdzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgICAgICBcInRyZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3RyZWVzey9zaGF9XCIsXG4gICAgICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy97c2hhfVwiLFxuICAgICAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgICAgICBcInN0YXJnYXplcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGFyZ2F6ZXJzXCIsXG4gICAgICAgIFwiY29udHJpYnV0b3JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udHJpYnV0b3JzXCIsXG4gICAgICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgICAgICBcInN1YnNjcmlwdGlvbl91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N1YnNjcmlwdGlvblwiLFxuICAgICAgICBcImNvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21taXRzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImNvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJpc3N1ZV9jb21tZW50X3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgICAgIFwiY29tcGFyZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9XCIsXG4gICAgICAgIFwibWVyZ2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWVyZ2VzXCIsXG4gICAgICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICAgICAgXCJkb3dubG9hZHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9kb3dubG9hZHNcIixcbiAgICAgICAgXCJpc3N1ZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibWlsZXN0b25lc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21pbGVzdG9uZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJub3RpZmljYXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9XCIsXG4gICAgICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgICAgICBcInJlbGVhc2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcmVsZWFzZXN7L2lkfVwiLFxuICAgICAgICBcImRlcGxveW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZGVwbG95bWVudHNcIixcbiAgICAgICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJ1cGRhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJwdXNoZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjEwWlwiLFxuICAgICAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInNzaF91cmxcIjogXCJnaXRAZ2l0aHViLmNvbTpiaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJjbG9uZV91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImhvbWVwYWdlXCI6IG51bGwsXG4gICAgICAgIFwic2l6ZVwiOiAxLFxuICAgICAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJ3YXRjaGVyc19jb3VudFwiOiAwLFxuICAgICAgICBcImxhbmd1YWdlXCI6IG51bGwsXG4gICAgICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgICAgICBcImhhc19wcm9qZWN0c1wiOiB0cnVlLFxuICAgICAgICBcImhhc19kb3dubG9hZHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgICAgICBcImhhc19wYWdlc1wiOiBmYWxzZSxcbiAgICAgICAgXCJoYXNfZGlzY3Vzc2lvbnNcIjogZmFsc2UsXG4gICAgICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICAgICAgXCJtaXJyb3JfdXJsXCI6IG51bGwsXG4gICAgICAgIFwiYXJjaGl2ZWRcIjogZmFsc2UsXG4gICAgICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNfY291bnRcIjogMSxcbiAgICAgICAgXCJsaWNlbnNlXCI6IG51bGwsXG4gICAgICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgICAgICBcImlzX3RlbXBsYXRlXCI6IGZhbHNlLFxuICAgICAgICBcIndlYl9jb21taXRfc2lnbm9mZl9yZXF1aXJlZFwiOiBmYWxzZSxcbiAgICAgICAgXCJ0b3BpY3NcIjogW10sXG4gICAgICAgIFwidmlzaWJpbGl0eVwiOiBcInB1YmxpY1wiLFxuICAgICAgICBcImZvcmtzXCI6IDAsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICAgICAgXCJ3YXRjaGVyc1wiOiAwLFxuICAgICAgICBcImRlZmF1bHRfYnJhbmNoXCI6IFwibWFpblwiLFxuICAgICAgICBcImFsbG93X3NxdWFzaF9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X21lcmdlX2NvbW1pdFwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X3JlYmFzZV9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X2F1dG9fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiZGVsZXRlX2JyYW5jaF9vbl9tZXJnZVwiOiBmYWxzZSxcbiAgICAgICAgXCJhbGxvd191cGRhdGVfYnJhbmNoXCI6IGZhbHNlLFxuICAgICAgICBcInVzZV9zcXVhc2hfcHJfdGl0bGVfYXNfZGVmYXVsdFwiOiBmYWxzZSxcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJDT01NSVRfTUVTU0FHRVNcIixcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X3RpdGxlXCI6IFwiQ09NTUlUX09SX1BSX1RJVExFXCIsXG4gICAgICAgIFwibWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJQUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF90aXRsZVwiOiBcIk1FUkdFX01FU1NBR0VcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJiYXNlXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOm1haW5cIixcbiAgICAgIFwicmVmXCI6IFwibWFpblwiLFxuICAgICAgXCJzaGFcIjogXCI3MmQ5MzFhMjBiYjgzZDEyM2FiNDVhY2NhZjc2MTE1MGM4YjAxMjExXCIsXG4gICAgICBcInVzZXJcIjoge1xuICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImlkXCI6IDY2NDU5NyxcbiAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgfSxcbiAgICAgIFwicmVwb1wiOiB7XG4gICAgICAgIFwiaWRcIjogNDcwMjEyMDAzLFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICAgICAgXCJuYW1lXCI6IFwiZGFiYmxlXCIsXG4gICAgICAgIFwiZnVsbF9uYW1lXCI6IFwiYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgICAgICBcIm93bmVyXCI6IHtcbiAgICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICAgIFwibm9kZV9pZFwiOiBcIk1EUTZWWE5sY2pZMk5EVTVOdz09XCIsXG4gICAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgICAgXCJmb2xsb3dpbmdfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2luZ3svb3RoZXJfdXNlcn1cIixcbiAgICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgICBcInN1YnNjcmlwdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N1YnNjcmlwdGlvbnNcIixcbiAgICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9ldmVudHN7L3ByaXZhY3l9XCIsXG4gICAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgICBcInNpdGVfYWRtaW5cIjogZmFsc2VcbiAgICAgICAgfSxcbiAgICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQSByZXBvIGZvciBkYWJibGluZ1wiLFxuICAgICAgICBcImZvcmtcIjogZmFsc2UsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImZvcmtzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZm9ya3NcIixcbiAgICAgICAgXCJrZXlzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUva2V5c3sva2V5X2lkfVwiLFxuICAgICAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgICAgIFwidGVhbXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90ZWFtc1wiLFxuICAgICAgICBcImhvb2tzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaG9va3NcIixcbiAgICAgICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZXZlbnRzXCIsXG4gICAgICAgIFwiYXNzaWduZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYXNzaWduZWVzey91c2VyfVwiLFxuICAgICAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICAgICAgXCJ0YWdzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvdGFnc1wiLFxuICAgICAgICBcImJsb2JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2Jsb2Jzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgICAgICBcImdpdF9yZWZzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3JlZnN7L3NoYX1cIixcbiAgICAgICAgXCJ0cmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90cmVlc3svc2hhfVwiLFxuICAgICAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICAgICAgXCJsYW5ndWFnZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9sYW5ndWFnZXNcIixcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhcmdhemVyc1wiLFxuICAgICAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgICAgICBcInN1YnNjcmliZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaWJlcnNcIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25fdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpcHRpb25cIixcbiAgICAgICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImdpdF9jb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2NvbW1pdHN7L3NoYX1cIixcbiAgICAgICAgXCJjb21tZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgICAgICBcImNvbnRlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udGVudHMveytwYXRofVwiLFxuICAgICAgICBcImNvbXBhcmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21wYXJlL3tiYXNlfS4uLntoZWFkfVwiLFxuICAgICAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgICAgICBcImFyY2hpdmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS97YXJjaGl2ZV9mb3JtYXR9ey9yZWZ9XCIsXG4gICAgICAgIFwiZG93bmxvYWRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZG93bmxvYWRzXCIsXG4gICAgICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwicHVsbHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsc3svbnVtYmVyfVwiLFxuICAgICAgICBcIm1pbGVzdG9uZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9taWxlc3RvbmVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgICAgICBcImxhYmVsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhYmVsc3svbmFtZX1cIixcbiAgICAgICAgXCJyZWxlYXNlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3JlbGVhc2Vzey9pZH1cIixcbiAgICAgICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgICAgIFwiY3JlYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICAgICAgXCJnaXRfdXJsXCI6IFwiZ2l0Oi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJzc2hfdXJsXCI6IFwiZ2l0QGdpdGh1Yi5jb206Ymlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInN2bl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJob21lcGFnZVwiOiBudWxsLFxuICAgICAgICBcInNpemVcIjogMSxcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX2NvdW50XCI6IDAsXG4gICAgICAgIFwid2F0Y2hlcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgICAgICBcImhhc19pc3N1ZXNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcHJvamVjdHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgICAgIFwiaGFzX3dpa2lcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcGFnZXNcIjogZmFsc2UsXG4gICAgICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgICAgICBcImZvcmtzX2NvdW50XCI6IDAsXG4gICAgICAgIFwibWlycm9yX3VybFwiOiBudWxsLFxuICAgICAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgICAgICBcImRpc2FibGVkXCI6IGZhbHNlLFxuICAgICAgICBcIm9wZW5faXNzdWVzX2NvdW50XCI6IDEsXG4gICAgICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgICAgICBcImFsbG93X2ZvcmtpbmdcIjogdHJ1ZSxcbiAgICAgICAgXCJpc190ZW1wbGF0ZVwiOiBmYWxzZSxcbiAgICAgICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgICAgIFwidG9waWNzXCI6IFtdLFxuICAgICAgICBcInZpc2liaWxpdHlcIjogXCJwdWJsaWNcIixcbiAgICAgICAgXCJmb3Jrc1wiOiAwLFxuICAgICAgICBcIm9wZW5faXNzdWVzXCI6IDEsXG4gICAgICAgIFwid2F0Y2hlcnNcIjogMCxcbiAgICAgICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIixcbiAgICAgICAgXCJhbGxvd19zcXVhc2hfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19tZXJnZV9jb21taXRcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19yZWJhc2VfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19hdXRvX21lcmdlXCI6IGZhbHNlLFxuICAgICAgICBcImRlbGV0ZV9icmFuY2hfb25fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiYWxsb3dfdXBkYXRlX2JyYW5jaFwiOiBmYWxzZSxcbiAgICAgICAgXCJ1c2Vfc3F1YXNoX3ByX3RpdGxlX2FzX2RlZmF1bHRcIjogZmFsc2UsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiQ09NTUlUX01FU1NBR0VTXCIsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF90aXRsZVwiOiBcIkNPTU1JVF9PUl9QUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiUFJfVElUTEVcIixcbiAgICAgICAgXCJtZXJnZV9jb21taXRfdGl0bGVcIjogXCJNRVJHRV9NRVNTQUdFXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiX2xpbmtzXCI6IHtcbiAgICAgIFwic2VsZlwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMVwiXG4gICAgICB9LFxuICAgICAgXCJodG1sXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiXG4gICAgICB9LFxuICAgICAgXCJpc3N1ZVwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWVudHNcIjoge1xuICAgICAgICBcImhyZWZcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy8xL2NvbW1lbnRzXCJcbiAgICAgIH0sXG4gICAgICBcInJldmlld19jb21tZW50c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiXG4gICAgICB9LFxuICAgICAgXCJyZXZpZXdfY29tbWVudFwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvY29tbWVudHN7L251bWJlcn1cIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWl0c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21taXRzXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXR1c2VzXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiYXV0aG9yX2Fzc29jaWF0aW9uXCI6IFwiT1dORVJcIixcbiAgICBcImF1dG9fbWVyZ2VcIjogbnVsbCxcbiAgICBcImFjdGl2ZV9sb2NrX3JlYXNvblwiOiBudWxsLFxuICAgIFwibWVyZ2VkXCI6IGZhbHNlLFxuICAgIFwibWVyZ2VhYmxlXCI6IG51bGwsXG4gICAgXCJyZWJhc2VhYmxlXCI6IG51bGwsXG4gICAgXCJtZXJnZWFibGVfc3RhdGVcIjogXCJ1bmtub3duXCIsXG4gICAgXCJtZXJnZWRfYnlcIjogbnVsbCxcbiAgICBcImNvbW1lbnRzXCI6IDAsXG4gICAgXCJyZXZpZXdfY29tbWVudHNcIjogMCxcbiAgICBcIm1haW50YWluZXJfY2FuX21vZGlmeVwiOiBmYWxzZSxcbiAgICBcImNvbW1pdHNcIjogMSxcbiAgICBcImFkZGl0aW9uc1wiOiAxLFxuICAgIFwiZGVsZXRpb25zXCI6IDEsXG4gICAgXCJjaGFuZ2VkX2ZpbGVzXCI6IDFcbiAgfSxcbiAgXCJyZXBvc2l0b3J5XCI6IHtcbiAgICBcImlkXCI6IDQ3MDIxMjAwMyxcbiAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICBcImZ1bGxfbmFtZVwiOiBcImJpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgIFwib3duZXJcIjoge1xuICAgICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgIH0sXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgXCJmb3JrXCI6IGZhbHNlLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgIFwia2V5c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2tleXN7L2tleV9pZH1cIixcbiAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgXCJob29rc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2hvb2tzXCIsXG4gICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICBcImFzc2lnbmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Fzc2lnbmVlc3svdXNlcn1cIixcbiAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgXCJibG9ic191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC9ibG9ic3svc2hhfVwiLFxuICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgIFwidHJlZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdHJlZXN7L3NoYX1cIixcbiAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgIFwic3RhcmdhemVyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N0YXJnYXplcnNcIixcbiAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgIFwic3Vic2NyaXB0aW9uX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaXB0aW9uXCIsXG4gICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgXCJjb21wYXJlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tcGFyZS97YmFzZX0uLi57aGVhZH1cIixcbiAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICBcImRvd25sb2Fkc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Rvd25sb2Fkc1wiLFxuICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgXCJtaWxlc3RvbmVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWlsZXN0b25lc3svbnVtYmVyfVwiLFxuICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgIFwicmVsZWFzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9yZWxlYXNlc3svaWR9XCIsXG4gICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICBcInVwZGF0ZWRfYXRcIjogXCIyMDIyLTAzLTE1VDE1OjA2OjE3WlwiLFxuICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3NoX3VybFwiOiBcImdpdEBnaXRodWIuY29tOmJpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiaG9tZXBhZ2VcIjogbnVsbCxcbiAgICBcInNpemVcIjogMSxcbiAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICBcIndhdGNoZXJzX2NvdW50XCI6IDAsXG4gICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgIFwiaGFzX3Byb2plY3RzXCI6IHRydWUsXG4gICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgIFwiaGFzX3BhZ2VzXCI6IGZhbHNlLFxuICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICBcIm1pcnJvcl91cmxcIjogbnVsbCxcbiAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgXCJvcGVuX2lzc3Vlc19jb3VudFwiOiAxLFxuICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgIFwiaXNfdGVtcGxhdGVcIjogZmFsc2UsXG4gICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgXCJ0b3BpY3NcIjogW10sXG4gICAgXCJ2aXNpYmlsaXR5XCI6IFwicHVibGljXCIsXG4gICAgXCJmb3Jrc1wiOiAwLFxuICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICBcIndhdGNoZXJzXCI6IDAsXG4gICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIlxuICB9LFxuICBcInNlbmRlclwiOiB7XG4gICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICBcImlkXCI6IDY2NDU5NyxcbiAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gIH1cbn1cbiIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) * Loops (e.g. `{{range .errors}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6IlNldmVyZSBVUkxzOlxue3tyYW5nZSAuZXJyb3JzfX17e2lmIGVxIC5sZXZlbCBcInNldmVyZVwifX0tIHt7LnVybH19XG57e2VuZH19e3tlbmR9fSIsImlucHV0Ijoie1wiZm9vXCI6IFwiYmFyXCIsIFwiZXJyb3JzXCI6IFt7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMS5jb21cIn0se1wibGV2ZWxcIjogXCJ3YXJuaW5nXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly93YXJuaW5nLmNvbVwifSx7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMi5jb21cIn1dfSIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) A good way to experiment with Go templates is the **[Go Template Playground](https://repeatit.io)**. It is _highly recommended_ to test your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). +ntfy supports a subset of the Sprig template functions that are included in the **[Go Template Playground](https://repeatit.io)**. Please see +[Template Functions](sprig.md) for a list of supported template functions. + !!! info Please note that the Go templating language is quite terrible. My apologies for using it for this feature. It is the best option for Go-based programs like ntfy. Stay calm and don't harm yourself or others in despair. **You can do it. I believe in you!** diff --git a/docs/releases.md b/docs/releases.md index 0877527e..ed728fcb 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1440,6 +1440,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) * Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) +* You can now use [Slim-Sprig](https://github.com/go-task/slim-sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) **Languages** diff --git a/docs/sprig.md b/docs/sprig.md new file mode 100644 index 00000000..be4e6c9c --- /dev/null +++ b/docs/sprig.md @@ -0,0 +1,24 @@ +# Template Functions + +ntfy includes a (reduced) version of [Sprig](https://github.com/Masterminds/sprig) to add functions that can be used +when you are using the [message template](publish.md#message-templating) feature. + +Below are the functions that are available to use inside your message/title templates. + +* [String Functions](./sprig/strings.md): `trim`, `trunc`, `substr`, `plural`, etc. + * [String List Functions](./sprig/string_slice.md): `splitList`, `sortAlpha`, etc. +* [Integer Math Functions](./sprig/math.md): `add`, `max`, `mul`, etc. + * [Integer List Functions](./sprig/integer_slice.md): `until`, `untilStep` +* [Date Functions](./sprig/date.md): `now`, `date`, etc. +* [Defaults Functions](./sprig/defaults.md): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` +* [Encoding Functions](./sprig/encoding.md): `b64enc`, `b64dec`, etc. +* [Lists and List Functions](./sprig/lists.md): `list`, `first`, `uniq`, etc. +* [Dictionaries and Dict Functions](./sprig/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. +* [Type Conversion Functions](./sprig/conversion.md): `atoi`, `int64`, `toString`, etc. +* [Path and Filepath Functions](./sprig/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` +* [Flow Control Functions](./sprig/flow_control.md): `fail` +* Advanced Functions + * [UUID Functions](./sprig/uuid.md): `uuidv4` + * [Reflection](./sprig/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. + * [Cryptographic and Security Functions](./sprig/crypto.md): `sha256sum`, etc. + * [URL](./sprig/url.md): `urlParse`, `urlJoin` diff --git a/docs/sprig/conversion.md b/docs/sprig/conversion.md new file mode 100644 index 00000000..af952682 --- /dev/null +++ b/docs/sprig/conversion.md @@ -0,0 +1,36 @@ +# Type Conversion Functions + +The following type conversion functions are provided by Sprig: + +- `atoi`: Convert a string to an integer. +- `float64`: Convert to a `float64`. +- `int`: Convert to an `int` at the system's width. +- `int64`: Convert to an `int64`. +- `toDecimal`: Convert a unix octal to a `int64`. +- `toString`: Convert to a string. +- `toStrings`: Convert a list, slice, or array to a list of strings. + +Only `atoi` requires that the input be a specific type. The others will attempt +to convert from any type to the destination type. For example, `int64` can convert +floats to ints, and it can also convert strings to ints. + +## toStrings + +Given a list-like collection, produce a slice of strings. + +``` +list 1 2 3 | toStrings +``` + +The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns +them as a list. + +## toDecimal + +Given a unix octal permission, produce a decimal. + +``` +"0777" | toDecimal +``` + +The above converts `0777` to `511` and returns the value as an int64. diff --git a/docs/sprig/crypto.md b/docs/sprig/crypto.md new file mode 100644 index 00000000..c66a269d --- /dev/null +++ b/docs/sprig/crypto.md @@ -0,0 +1,41 @@ +# Cryptographic and Security Functions + +Sprig provides a couple of advanced cryptographic functions. + +## sha1sum + +The `sha1sum` function receives a string, and computes it's SHA1 digest. + +``` +sha1sum "Hello world!" +``` + +## sha256sum + +The `sha256sum` function receives a string, and computes it's SHA256 digest. + +``` +sha256sum "Hello world!" +``` + +The above will compute the SHA 256 sum in an "ASCII armored" format that is +safe to print. + +## sha512sum + +The `sha512sum` function receives a string, and computes it's SHA512 digest. + +``` +sha512sum "Hello world!" +``` + +The above will compute the SHA 512 sum in an "ASCII armored" format that is +safe to print. + +## adler32sum + +The `adler32sum` function receives a string, and computes its Adler-32 checksum. + +``` +adler32sum "Hello world!" +``` diff --git a/docs/sprig/date.md b/docs/sprig/date.md new file mode 100644 index 00000000..7410c08d --- /dev/null +++ b/docs/sprig/date.md @@ -0,0 +1,126 @@ +# Date Functions + +## now + +The current date/time. Use this in conjunction with other date functions. + +## ago + +The `ago` function returns duration from time.Now in seconds resolution. + +``` +ago .CreatedAt +``` + +returns in `time.Duration` String() format + +``` +2h34m7s +``` + +## date + +The `date` function formats a date. + +Format the date to YEAR-MONTH-DAY: + +``` +now | date "2006-01-02" +``` + +Date formatting in Go is a [little bit different](https://pauladamsmith.com/blog/2011/05/go_time.html). + +In short, take this as the base date: + +``` +Mon Jan 2 15:04:05 MST 2006 +``` + +Write it in the format you want. Above, `2006-01-02` is the same date, but +in the format we want. + +## dateInZone + +Same as `date`, but with a timezone. + +``` +dateInZone "2006-01-02" (now) "UTC" +``` + +## duration + +Formats a given amount of seconds as a `time.Duration`. + +This returns 1m35s + +``` +duration "95" +``` + +## durationRound + +Rounds a given duration to the most significant unit. Strings and `time.Duration` +gets parsed as a duration, while a `time.Time` is calculated as the duration since. + +This return 2h + +``` +durationRound "2h10m5s" +``` + +This returns 3mo + +``` +durationRound "2400h10m5s" +``` + +## unixEpoch + +Returns the seconds since the unix epoch for a `time.Time`. + +``` +now | unixEpoch +``` + +## dateModify, mustDateModify + +The `dateModify` takes a modification and a date and returns the timestamp. + +Subtract an hour and thirty minutes from the current time: + +``` +now | date_modify "-1.5h" +``` + +If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. + +## htmlDate + +The `htmlDate` function formats a date for inserting into an HTML date picker +input field. + +``` +now | htmlDate +``` + +## htmlDateInZone + +Same as htmlDate, but with a timezone. + +``` +htmlDateInZone (now) "UTC" +``` + +## toDate, mustToDate + +`toDate` converts a string to a date. The first argument is the date layout and +the second the date string. If the string can't be convert it returns the zero +value. +`mustToDate` will return an error in case the string cannot be converted. + +This is useful when you want to convert a string date to another format +(using pipe). The example below converts "2017-12-31" to "31/12/2017". + +``` +toDate "2006-01-02" "2017-12-31" | date "02/01/2006" +``` diff --git a/docs/sprig/defaults.md b/docs/sprig/defaults.md new file mode 100644 index 00000000..b8af1455 --- /dev/null +++ b/docs/sprig/defaults.md @@ -0,0 +1,169 @@ +# Default Functions + +Sprig provides tools for setting default values for templates. + +## default + +To set a simple default value, use `default`: + +``` +default "foo" .Bar +``` + +In the above, if `.Bar` evaluates to a non-empty value, it will be used. But if +it is empty, `foo` will be returned instead. + +The definition of "empty" depends on type: + +- Numeric: 0 +- String: "" +- Lists: `[]` +- Dicts: `{}` +- Boolean: `false` +- And always `nil` (aka null) + +For structs, there is no definition of empty, so a struct will never return the +default. + +## empty + +The `empty` function returns `true` if the given value is considered empty, and +`false` otherwise. The empty values are listed in the `default` section. + +``` +empty .Foo +``` + +Note that in Go template conditionals, emptiness is calculated for you. Thus, +you rarely need `if empty .Foo`. Instead, just use `if .Foo`. + +## coalesce + +The `coalesce` function takes a list of values and returns the first non-empty +one. + +``` +coalesce 0 1 2 +``` + +The above returns `1`. + +This function is useful for scanning through multiple variables or values: + +``` +coalesce .name .parent.name "Matt" +``` + +The above will first check to see if `.name` is empty. If it is not, it will return +that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. +Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. + +## all + +The `all` function takes a list of values and returns true if all values are non-empty. + +``` +all 0 1 2 +``` + +The above returns `false`. + +This function is useful for evaluating multiple conditions of variables or values: + +``` +all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Method "POST") +``` + +The above will check http.Request is POST with tls 1.3 and http/2. + +## any + +The `any` function takes a list of values and returns true if any value is non-empty. + +``` +any 0 1 2 +``` + +The above returns `true`. + +This function is useful for evaluating multiple conditions of variables or values: + +``` +any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method "OPTIONS") +``` + +The above will check http.Request method is one of GET/POST/OPTIONS. + +## fromJSON, mustFromJSON + +`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. +`mustFromJSON` will return an error in case the JSON is invalid. + +``` +fromJSON "{\"foo\": 55}" +``` + +## toJSON, mustToJSON + +The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. +`mustToJSON` will return an error in case the item cannot be encoded in JSON. + +``` +toJSON .Item +``` + +The above returns JSON string representation of `.Item`. + +## toPrettyJSON, mustToPrettyJSON + +The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. + +``` +toPrettyJSON .Item +``` + +The above returns indented JSON string representation of `.Item`. + +## toRawJSON, mustToRawJSON + +The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. + +``` +toRawJSON .Item +``` + +The above returns unescaped JSON string representation of `.Item`. + +## ternary + +The `ternary` function takes two values, and a test value. If the test value is +true, the first value will be returned. If the test value is empty, the second +value will be returned. This is similar to the c ternary operator. + +### true test value + +``` +ternary "foo" "bar" true +``` + +or + +``` +true | ternary "foo" "bar" +``` + +The above returns `"foo"`. + +### false test value + +``` +ternary "foo" "bar" false +``` + +or + +``` +false | ternary "foo" "bar" +``` + +The above returns `"bar"`. diff --git a/docs/sprig/dicts.md b/docs/sprig/dicts.md new file mode 100644 index 00000000..5a4490d5 --- /dev/null +++ b/docs/sprig/dicts.md @@ -0,0 +1,172 @@ +# Dictionaries and Dict Functions + +Sprig provides a key/value storage type called a `dict` (short for "dictionary", +as in Python). A `dict` is an _unorder_ type. + +The key to a dictionary **must be a string**. However, the value can be any +type, even another `dict` or `list`. + +Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will +modify the contents of a dictionary. + +## dict + +Creating dictionaries is done by calling the `dict` function and passing it a +list of pairs. + +The following creates a dictionary with three items: + +``` +$myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" +``` + +## get + +Given a map and a key, get the value from the map. + +``` +get $myDict "name1" +``` + +The above returns `"value1"` + +Note that if the key is not found, this operation will simply return `""`. No error +will be generated. + +## set + +Use `set` to add a new key/value pair to a dictionary. + +``` +$_ := set $myDict "name4" "value4" +``` + +Note that `set` _returns the dictionary_ (a requirement of Go template functions), +so you may need to trap the value as done above with the `$_` assignment. + +## unset + +Given a map and a key, delete the key from the map. + +``` +$_ := unset $myDict "name4" +``` + +As with `set`, this returns the dictionary. + +Note that if the key is not found, this operation will simply return. No error +will be generated. + +## hasKey + +The `hasKey` function returns `true` if the given dict contains the given key. + +``` +hasKey $myDict "name1" +``` + +If the key is not found, this returns `false`. + +## pluck + +The `pluck` function makes it possible to give one key and multiple maps, and +get a list of all of the matches: + +``` +pluck "name1" $myDict $myOtherDict +``` + +The above will return a `list` containing every found value (`[value1 otherValue1]`). + +If the give key is _not found_ in a map, that map will not have an item in the +list (and the length of the returned list will be less than the number of dicts +in the call to `pluck`. + +If the key is _found_ but the value is an empty value, that value will be +inserted. + +A common idiom in Sprig templates is to uses `pluck... | first` to get the first +matching key out of a collection of dictionaries. + +## dig + +The `dig` function traverses a nested set of dicts, selecting keys from a list +of values. It returns a default value if any of the keys are not found at the +associated dict. + +``` +dig "user" "role" "humanName" "guest" $dict +``` + +Given a dict structured like +``` +{ + user: { + role: { + humanName: "curator" + } + } +} +``` + +the above would return `"curator"`. If the dict lacked even a `user` field, +the result would be `"guest"`. + +Dig can be very useful in cases where you'd like to avoid guard clauses, +especially since Go's template package's `and` doesn't shortcut. For instance +`and a.maybeNil a.maybeNil.iNeedThis` will always evaluate +`a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) + +`dig` accepts its dict argument last in order to support pipelining. + +## keys + +The `keys` function will return a `list` of all of the keys in one or more `dict` +types. Since a dictionary is _unordered_, the keys will not be in a predictable order. +They can be sorted with `sortAlpha`. + +``` +keys $myDict | sortAlpha +``` + +When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` +function along with `sortAlpha` to get a unqiue, sorted list of keys. + +``` +keys $myDict $myOtherDict | uniq | sortAlpha +``` + +## pick + +The `pick` function selects just the given keys out of a dictionary, creating a +new `dict`. + +``` +$new := pick $myDict "name1" "name2" +``` + +The above returns `{name1: value1, name2: value2}` + +## omit + +The `omit` function is similar to `pick`, except it returns a new `dict` with all +the keys that _do not_ match the given keys. + +``` +$new := omit $myDict "name1" "name3" +``` + +The above returns `{name2: value2}` + +## values + +The `values` function is similar to `keys`, except it returns a new `list` with +all the values of the source `dict` (only one dictionary is supported). + +``` +$vals := values $myDict +``` + +The above returns `list["value1", "value2", "value 3"]`. Note that the `values` +function gives no guarantees about the result ordering- if you care about this, +then use `sortAlpha`. diff --git a/docs/sprig/encoding.md b/docs/sprig/encoding.md new file mode 100644 index 00000000..1c7a36f8 --- /dev/null +++ b/docs/sprig/encoding.md @@ -0,0 +1,6 @@ +# Encoding Functions + +Sprig has the following encoding and decoding functions: + +- `b64enc`/`b64dec`: Encode or decode with Base64 +- `b32enc`/`b32dec`: Encode or decode with Base32 diff --git a/docs/sprig/flow_control.md b/docs/sprig/flow_control.md new file mode 100644 index 00000000..6414640a --- /dev/null +++ b/docs/sprig/flow_control.md @@ -0,0 +1,11 @@ +# Flow Control Functions + +## fail + +Unconditionally returns an empty `string` and an `error` with the specified +text. This is useful in scenarios where other conditionals have determined that +template rendering should fail. + +``` +fail "Please accept the end user license agreement" +``` diff --git a/docs/sprig/integer_slice.md b/docs/sprig/integer_slice.md new file mode 100644 index 00000000..ab4bef6d --- /dev/null +++ b/docs/sprig/integer_slice.md @@ -0,0 +1,41 @@ +# Integer List Functions + +## until + +The `until` function builds a range of integers. + +``` +until 5 +``` + +The above generates the list `[0, 1, 2, 3, 4]`. + +This is useful for looping with `range $i, $e := until 5`. + +## untilStep + +Like `until`, `untilStep` generates a list of counting integers. But it allows +you to define a start, stop, and step: + +``` +untilStep 3 6 2 +``` + +The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal +or greater than 6. This is similar to Python's `range` function. + +## seq + +Works like the bash `seq` command. +* 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. +* 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. +* 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. + +``` +seq 5 => 1 2 3 4 5 +seq -3 => 1 0 -1 -2 -3 +seq 0 2 => 0 1 2 +seq 2 -2 => 2 1 0 -1 -2 +seq 0 2 10 => 0 2 4 6 8 10 +seq 0 -2 -5 => 0 -2 -4 +``` diff --git a/docs/sprig/lists.md b/docs/sprig/lists.md new file mode 100644 index 00000000..ed8c52b3 --- /dev/null +++ b/docs/sprig/lists.md @@ -0,0 +1,188 @@ +# Lists and List Functions + +Sprig provides a simple `list` type that can contain arbitrary sequential lists +of data. This is similar to arrays or slices, but lists are designed to be used +as immutable data types. + +Create a list of integers: + +``` +$myList := list 1 2 3 4 5 +``` + +The above creates a list of `[1 2 3 4 5]`. + +## first, mustFirst + +To get the head item on a list, use `first`. + +`first $myList` returns `1` + +`first` panics if there is a problem while `mustFirst` returns an error to the +template engine if there is a problem. + +## rest, mustRest + +To get the tail of the list (everything but the first item), use `rest`. + +`rest $myList` returns `[2 3 4 5]` + +`rest` panics if there is a problem while `mustRest` returns an error to the +template engine if there is a problem. + +## last, mustLast + +To get the last item on a list, use `last`: + +`last $myList` returns `5`. This is roughly analogous to reversing a list and +then calling `first`. + +`last` panics if there is a problem while `mustLast` returns an error to the +template engine if there is a problem. + +## initial, mustInitial + +This compliments `last` by returning all _but_ the last element. +`initial $myList` returns `[1 2 3 4]`. + +`initial` panics if there is a problem while `mustInitial` returns an error to the +template engine if there is a problem. + +## append, mustAppend + +Append a new item to an existing list, creating a new list. + +``` +$new = append $myList 6 +``` + +The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. + +`append` panics if there is a problem while `mustAppend` returns an error to the +template engine if there is a problem. + +## prepend, mustPrepend + +Push an element onto the front of a list, creating a new list. + +``` +prepend $myList 0 +``` + +The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. + +`prepend` panics if there is a problem while `mustPrepend` returns an error to the +template engine if there is a problem. + +## concat + +Concatenate arbitrary number of lists into one. + +``` +concat $myList ( list 6 7 ) ( list 8 ) +``` + +The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. + +## reverse, mustReverse + +Produce a new list with the reversed elements of the given list. + +``` +reverse $myList +``` + +The above would generate the list `[5 4 3 2 1]`. + +`reverse` panics if there is a problem while `mustReverse` returns an error to the +template engine if there is a problem. + +## uniq, mustUniq + +Generate a list with all of the duplicates removed. + +``` +list 1 1 1 2 | uniq +``` + +The above would produce `[1 2]` + +`uniq` panics if there is a problem while `mustUniq` returns an error to the +template engine if there is a problem. + +## without, mustWithout + +The `without` function filters items out of a list. + +``` +without $myList 3 +``` + +The above would produce `[1 2 4 5]` + +Without can take more than one filter: + +``` +without $myList 1 3 5 +``` + +That would produce `[2 4]` + +`without` panics if there is a problem while `mustWithout` returns an error to the +template engine if there is a problem. + +## has, mustHas + +Test to see if a list has a particular element. + +``` +has 4 $myList +``` + +The above would return `true`, while `has "hello" $myList` would return false. + +`has` panics if there is a problem while `mustHas` returns an error to the +template engine if there is a problem. + +## compact, mustCompact + +Accepts a list and removes entries with empty values. + +``` +$list := list 1 "a" "foo" "" +$copy := compact $list +``` + +`compact` will return a new list with the empty (i.e., "") item removed. + +`compact` panics if there is a problem and `mustCompact` returns an error to the +template engine if there is a problem. + +## slice, mustSlice + +To get partial elements of a list, use `slice list [n] [m]`. It is +equivalent of `list[n:m]`. + +- `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. +- `slice $myList 3` returns `[4 5]`. It is same as `myList[3:]`. +- `slice $myList 1 3` returns `[2 3]`. It is same as `myList[1:3]`. +- `slice $myList 0 3` returns `[1 2 3]`. It is same as `myList[:3]`. + +`slice` panics if there is a problem while `mustSlice` returns an error to the +template engine if there is a problem. + +## chunk + +To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. + +``` +chunk 3 (list 1 2 3 4 5 6 7 8) +``` + +This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. + +## A Note on List Internals + +A list is implemented in Go as a `[]interface{}`. For Go developers embedding +Sprig, you may pass `[]interface{}` items into your template context and be +able to use all of the `list` functions on those items. diff --git a/docs/sprig/math.md b/docs/sprig/math.md new file mode 100644 index 00000000..b08d0a2f --- /dev/null +++ b/docs/sprig/math.md @@ -0,0 +1,78 @@ +# Integer Math Functions + +The following math functions operate on `int64` values. + +## add + +Sum numbers with `add`. Accepts two or more inputs. + +``` +add 1 2 3 +``` + +## add1 + +To increment by 1, use `add1` + +## sub + +To subtract, use `sub` + +## div + +Perform integer division with `div` + +## mod + +Modulo with `mod` + +## mul + +Multiply with `mul`. Accepts two or more inputs. + +``` +mul 1 2 3 +``` + +## max + +Return the largest of a series of integers: + +This will return `3`: + +``` +max 1 2 3 +``` + +## min + +Return the smallest of a series of integers. + +`min 1 2 3` will return `1` + +## floor + +Returns the greatest float value less than or equal to input value + +`floor 123.9999` will return `123.0` + +## ceil + +Returns the greatest float value greater than or equal to input value + +`ceil 123.001` will return `124.0` + +## round + +Returns a float value with the remainder rounded to the given number to digits after the decimal point. + +`round 123.555555 3` will return `123.556` + +## randInt +Returns a random integer value from min (inclusive) to max (exclusive). + +``` +randInt 12 30 +``` + +The above will produce a random number in the range [12,30]. diff --git a/docs/sprig/os.md b/docs/sprig/os.md new file mode 100644 index 00000000..e6120c03 --- /dev/null +++ b/docs/sprig/os.md @@ -0,0 +1,24 @@ +# OS Functions + +_WARNING:_ These functions can lead to information leakage if not used +appropriately. + +_WARNING:_ Some notable implementations of Sprig (such as +[Kubernetes Helm](http://helm.sh)) _do not provide these functions for security +reasons_. + +## env + +The `env` function reads an environment variable: + +``` +env "HOME" +``` + +## expandenv + +To substitute environment variables in a string, use `expandenv`: + +``` +expandenv "Your path is set to $PATH" +``` diff --git a/docs/sprig/paths.md b/docs/sprig/paths.md new file mode 100644 index 00000000..f847e357 --- /dev/null +++ b/docs/sprig/paths.md @@ -0,0 +1,114 @@ +# Path and Filepath Functions + +While Sprig does not grant access to the filesystem, it does provide functions +for working with strings that follow file path conventions. + +## Paths + +Paths separated by the slash character (`/`), processed by the `path` package. + +Examples: + +* The [Linux](https://en.wikipedia.org/wiki/Linux) and + [MacOS](https://en.wikipedia.org/wiki/MacOS) + [filesystems](https://en.wikipedia.org/wiki/File_system): + `/home/user/file`, `/etc/config`; +* The path component of + [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): + `https://example.com/some/content/`, `ftp://example.com/file/`. + +### base + +Return the last element of a path. + +``` +base "foo/bar/baz" +``` + +The above prints "baz". + +### dir + +Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` +returns `foo/bar`. + +### clean + +Clean up a path. + +``` +clean "foo/bar/../baz" +``` + +The above resolves the `..` and returns `foo/baz`. + +### ext + +Return the file extension. + +``` +ext "foo.bar" +``` + +The above returns `.bar`. + +### isAbs + +To check whether a path is absolute, use `isAbs`. + +## Filepaths + +Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package. + +These are the recommended functions to use when parsing paths of local filesystems, usually when dealing with local files, directories, etc. + +Examples: + +* Running on Linux or MacOS the filesystem path is separated by the slash character (`/`): + `/home/user/file`, `/etc/config`; +* Running on [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) + the filesystem path is separated by the backslash character (`\`): + `C:\Users\Username\`, `C:\Program Files\Application\`; + +### osBase + +Return the last element of a filepath. + +``` +osBase "/foo/bar/baz" +osBase "C:\\foo\\bar\\baz" +``` + +The above prints "baz" on Linux and Windows, respectively. + +### osDir + +Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` +returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` +returns `C:\\foo\\bar` on Windows. + +### osClean + +Clean up a path. + +``` +osClean "/foo/bar/../baz" +osClean "C:\\foo\\bar\\..\\baz" +``` + +The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. + +### osExt + +Return the file extension. + +``` +osExt "/foo.bar" +osExt "C:\\foo.bar" +``` + +The above returns `.bar` on Linux and Windows, respectively. + +### osIsAbs + +To check whether a file path is absolute, use `osIsAbs`. diff --git a/docs/sprig/reflection.md b/docs/sprig/reflection.md new file mode 100644 index 00000000..51e167aa --- /dev/null +++ b/docs/sprig/reflection.md @@ -0,0 +1,50 @@ +# Reflection Functions + +Sprig provides rudimentary reflection tools. These help advanced template +developers understand the underlying Go type information for a particular value. + +Go has several primitive _kinds_, like `string`, `slice`, `int64`, and `bool`. + +Go has an open _type_ system that allows developers to create their own types. + +Sprig provides a set of functions for each. + +## Kind Functions + +There are two Kind functions: `kindOf` returns the kind of an object. + +``` +kindOf "hello" +``` + +The above would return `string`. For simple tests (like in `if` blocks), the +`kindIs` function will let you verify that a value is a particular kind: + +``` +kindIs "int" 123 +``` + +The above will return `true` + +## Type Functions + +Types are slightly harder to work with, so there are three different functions: + +- `typeOf` returns the underlying type of a value: `typeOf $foo` +- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` +- `typeIsLike` works as `typeIs`, except that it also dereferences pointers. + +**Note:** None of these can test whether or not something implements a given +interface, since doing so would require compiling the interface in ahead of time. + +## deepEqual + +`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) + +Works for non-primitive types as well (compared to the built-in `eq`). + +``` +deepEqual (list 1 2 3) (list 1 2 3) +``` + +The above will return `true` diff --git a/docs/sprig/semver.md b/docs/sprig/semver.md new file mode 100644 index 00000000..f049613d --- /dev/null +++ b/docs/sprig/semver.md @@ -0,0 +1,151 @@ +# Semantic Version Functions + +Some version schemes are easily parseable and comparable. Sprig provides functions +for working with [SemVer 2](http://semver.org) versions. + +## semver + +The `semver` function parses a string into a Semantic Version: + +``` +$version := semver "1.2.3-alpha.1+123" +``` + +_If the parser fails, it will cause template execution to halt with an error._ + +At this point, `$version` is a pointer to a `Version` object with the following +properties: + +- `$version.Major`: The major number (`1` above) +- `$version.Minor`: The minor number (`2` above) +- `$version.Patch`: The patch number (`3` above) +- `$version.Prerelease`: The prerelease (`alpha.1` above) +- `$version.Metadata`: The build metadata (`123` above) +- `$version.Original`: The original version as a string + +Additionally, you can compare a `Version` to another `version` using the `Compare` +function: + +``` +semver "1.4.3" | (semver "1.2.3").Compare +``` + +The above will return `-1`. + +The return values are: + +- `-1` if the given semver is greater than the semver whose `Compare` method was called +- `1` if the version who's `Compare` function was called is greater. +- `0` if they are the same version + +(Note that in SemVer, the `Metadata` field is not compared during version +comparison operations.) + +## semverCompare + +A more robust comparison function is provided as `semverCompare`. It returns `true` if +the constraint matches, or `false` if it does not match. This version supports version ranges: + +- `semverCompare "1.2.3" "1.2.3"` checks for an exact match +- `semverCompare "^1.2.0" "1.2.3"` checks that the major and minor versions match, and that the patch + number of the second version is _greater than or equal to_ the first parameter. + +The SemVer functions use the [Masterminds semver library](https://github.com/Masterminds/semver), +from the creators of Sprig. + +## Basic Comparisons + +There are two elements to the comparisons. First, a comparison string is a list +of space or comma separated AND comparisons. These are then separated by || (OR) +comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a +comparison that's greater than or equal to 1.2 and less than 3.0.0 or is +greater than or equal to 4.2.3. + +The basic comparisons are: + +- `=`: equal (aliased to no operator) +- `!=`: not equal +- `>`: greater than +- `<`: less than +- `>=`: greater than or equal to +- `<=`: less than or equal to + +_Note, according to the Semantic Version specification pre-releases may not be +API compliant with their release counterpart. It says,_ + +## Working With Prerelease Versions + +Pre-releases, for those not familiar with them, are used for software releases +prior to stable or generally available releases. Examples of prereleases include +development, alpha, beta, and release candidate releases. A prerelease may be +a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the +order of precedence, prereleases come before their associated releases. In this +example `1.2.3-beta.1 < 1.2.3`. + +According to the Semantic Version specification prereleases may not be +API compliant with their release counterpart. It says, + +> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. + +SemVer comparisons using constraints without a prerelease comparator will skip +prerelease versions. For example, `>=1.2.3` will skip prereleases when looking +at a list of releases while `>=1.2.3-0` will evaluate and find prereleases. + +The reason for the `0` as a pre-release version in the example comparison is +because pre-releases can only contain ASCII alphanumerics and hyphens (along with +`.` separators), per the spec. Sorting happens in ASCII sort order, again per the +spec. The lowest character is a `0` in ASCII sort order +(see an [ASCII Table](http://www.asciitable.com/)) + +Understanding ASCII sort ordering is important because A-Z comes before a-z. That +means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case +sensitivity doesn't apply here. This is due to ASCII sort ordering which is what +the spec specifies. + +## Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + +- `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` +- `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +## Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the patch level comparison (see tilde below). For example, + +- `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +- `>= 1.2.x` is equivalent to `>= 1.2.0` +- `<= 2.x` is equivalent to `< 3` +- `*` is equivalent to `>= 0.0.0` + +## Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + +- `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` +- `~1` is equivalent to `>= 1, < 2` +- `~2.3` is equivalent to `>= 2.3, < 2.4` +- `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +- `~1.x` is equivalent to `>= 1, < 2` + +## Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + +- `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` +- `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` +- `^2.3` is equivalent to `>= 2.3, < 3` +- `^2.x` is equivalent to `>= 2.0.0, < 3` +- `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` +- `^0.2` is equivalent to `>=0.2.0 <0.3.0` +- `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` +- `^0.0` is equivalent to `>=0.0.0 <0.1.0` +- `^0` is equivalent to `>=0.0.0 <1.0.0` diff --git a/docs/sprig/string_slice.md b/docs/sprig/string_slice.md new file mode 100644 index 00000000..96c0c83b --- /dev/null +++ b/docs/sprig/string_slice.md @@ -0,0 +1,72 @@ +# String List Functions + +These function operate on or generate slices of strings. In Go, a slice is a +growable array. In Sprig, it's a special case of a `list`. + +## join + +Join a list of strings into a single string, with the given separator. + +``` +list "hello" "world" | join "_" +``` + +The above will produce `hello_world` + +`join` will try to convert non-strings to a string value: + +``` +list 1 2 3 | join "+" +``` + +The above will produce `1+2+3` + +## splitList and split + +Split a string into a list of strings: + +``` +splitList "$" "foo$bar$baz" +``` + +The above will return `[foo bar baz]` + +The older `split` function splits a string into a `dict`. It is designed to make +it easy to use template dot notation for accessing members: + +``` +$a := split "$" "foo$bar$baz" +``` + +The above produces a map with index keys. `{_0: foo, _1: bar, _2: baz}` + +``` +$a._0 +``` + +The above produces `foo` + +## splitn + +`splitn` function splits a string into a `dict` with `n` keys. It is designed to make +it easy to use template dot notation for accessing members: + +``` +$a := splitn "$" 2 "foo$bar$baz" +``` + +The above produces a map with index keys. `{_0: foo, _1: bar$baz}` + +``` +$a._0 +``` + +The above produces `foo` + +## sortAlpha + +The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) +order. + +It does _not_ sort in place, but returns a sorted copy of the list, in keeping +with the immutability of lists. diff --git a/docs/sprig/strings.md b/docs/sprig/strings.md new file mode 100644 index 00000000..784392f1 --- /dev/null +++ b/docs/sprig/strings.md @@ -0,0 +1,309 @@ +# String Functions + +Sprig has a number of string manipulation functions. + +## trim + +The `trim` function removes space from either side of a string: + +``` +trim " hello " +``` + +The above produces `hello` + +## trimAll + +Remove given characters from the front or back of a string: + +``` +trimAll "$" "$5.00" +``` + +The above returns `5.00` (as a string). + +## trimSuffix + +Trim just the suffix from a string: + +``` +trimSuffix "-" "hello-" +``` + +The above returns `hello` + +## trimPrefix + +Trim just the prefix from a string: + +``` +trimPrefix "-" "-hello" +``` + +The above returns `hello` + +## upper + +Convert the entire string to uppercase: + +``` +upper "hello" +``` + +The above returns `HELLO` + +## lower + +Convert the entire string to lowercase: + +``` +lower "HELLO" +``` + +The above returns `hello` + +## title + +Convert to title case: + +``` +title "hello world" +``` + +The above returns `Hello World` + +## repeat + +Repeat a string multiple times: + +``` +repeat 3 "hello" +``` + +The above returns `hellohellohello` + +## substr + +Get a substring from a string. It takes three parameters: + +- start (int) +- end (int) +- string (string) + +``` +substr 0 5 "hello world" +``` + +The above returns `hello` + +## trunc + +Truncate a string (and add no suffix) + +``` +trunc 5 "hello world" +``` + +The above produces `hello`. + +``` +trunc -5 "hello world" +``` + +The above produces `world`. + +## contains + +Test to see if one string is contained inside of another: + +``` +contains "cat" "catch" +``` + +The above returns `true` because `catch` contains `cat`. + +## hasPrefix and hasSuffix + +The `hasPrefix` and `hasSuffix` functions test whether a string has a given +prefix or suffix: + +``` +hasPrefix "cat" "catch" +``` + +The above returns `true` because `catch` has the prefix `cat`. + +## quote and squote + +These functions wrap a string in double quotes (`quote`) or single quotes +(`squote`). + +## cat + +The `cat` function concatenates multiple strings together into one, separating +them with spaces: + +``` +cat "hello" "beautiful" "world" +``` + +The above produces `hello beautiful world` + +## indent + +The `indent` function indents every line in a given string to the specified +indent width. This is useful when aligning multi-line strings: + +``` +indent 4 $lots_of_text +``` + +The above will indent every line of text by 4 space characters. + +## nindent + +The `nindent` function is the same as the indent function, but prepends a new +line to the beginning of the string. + +``` +nindent 4 $lots_of_text +``` + +The above will indent every line of text by 4 space characters and add a new +line to the beginning. + +## replace + +Perform simple string replacement. + +It takes three arguments: + +- string to replace +- string to replace with +- source string + +``` +"I Am Henry VIII" | replace " " "-" +``` + +The above will produce `I-Am-Henry-VIII` + +## plural + +Pluralize a string. + +``` +len $fish | plural "one anchovy" "many anchovies" +``` + +In the above, if the length of the string is 1, the first argument will be +printed (`one anchovy`). Otherwise, the second argument will be printed +(`many anchovies`). + +The arguments are: + +- singular string +- plural string +- length integer + +NOTE: Sprig does not currently support languages with more complex pluralization +rules. And `0` is considered a plural because the English language treats it +as such (`zero anchovies`). The Sprig developers are working on a solution for +better internationalization. + +## regexMatch, mustRegexMatch + +Returns true if the input string contains any match of the regular expression. + +``` +regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" +``` + +The above produces `true` + +`regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the +template engine if there is a problem. + +## regexFindAll, mustRegexFindAll + +Returns a slice of all matches of the regular expression in the input string. +The last parameter n determines the number of substrings to return, where -1 means return all matches + +``` +regexFindAll "[2,4,6,8]" "123456789" -1 +``` + +The above produces `[2 4 6 8]` + +`regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the +template engine if there is a problem. + +## regexFind, mustRegexFind + +Return the first (left most) match of the regular expression in the input string + +``` +regexFind "[a-zA-Z][1-9]" "abcd1234" +``` + +The above produces `d1` + +`regexFind` panics if there is a problem and `mustRegexFind` returns an error to the +template engine if there is a problem. + +## regexReplaceAll, mustRegexReplaceAll + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. +Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch + +``` +regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W" +``` + +The above produces `-W-xxW-` + +`regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the +template engine if there is a problem. + +## regexReplaceAllLiteral, mustRegexReplaceAllLiteral + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement +The replacement string is substituted directly, without using Expand + +``` +regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}" +``` + +The above produces `-${1}-${1}-` + +`regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the +template engine if there is a problem. + +## regexSplit, mustRegexSplit + +Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches + +``` +regexSplit "z+" "pizza" -1 +``` + +The above produces `[pi a]` + +`regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the +template engine if there is a problem. + +## regexQuoteMeta + +Returns a string that escapes all regular expression metacharacters inside the argument text; +the returned string is a regular expression matching the literal text. + +``` +regexQuoteMeta "1.2.3" +``` + +The above produces `1\.2\.3` + +## See Also... + +The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains +functions for working with an array of strings. diff --git a/docs/sprig/url.md b/docs/sprig/url.md new file mode 100644 index 00000000..21d54a29 --- /dev/null +++ b/docs/sprig/url.md @@ -0,0 +1,33 @@ +# URL Functions + +## urlParse +Parses string for URL and produces dict with URL parts + +``` +urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" +``` + +The above returns a dict, containing URL object: +```yaml +scheme: 'http' +host: 'server.com:8080' +path: '/api' +query: 'list=false' +opaque: nil +fragment: 'anchor' +userinfo: 'admin:secret' +``` + +For more info, check https://golang.org/pkg/net/url/#URL + +## urlJoin +Joins map (produced by `urlParse`) to produce URL string + +``` +urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") +``` + +The above returns the following string: +``` +proto://host:80/path?query#fragment +``` diff --git a/docs/sprig/uuid.md b/docs/sprig/uuid.md new file mode 100644 index 00000000..1b57a330 --- /dev/null +++ b/docs/sprig/uuid.md @@ -0,0 +1,9 @@ +# UUID Functions + +Sprig can generate UUID v4 universally unique IDs. + +``` +uuidv4 +``` + +The above returns a new UUID of the v4 (randomly generated) type. diff --git a/mkdocs.yml b/mkdocs.yml index ef746518..8006eac4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,7 @@ nav: - "Integrations + projects": integrations.md - "Release notes": releases.md - "Emojis 🥳 🎉": emojis.md + - "Template Functions": sprig.md - "Troubleshooting": troubleshooting.md - "Known issues": known-issues.md - "Deprecation notices": deprecations.md diff --git a/server/server.go b/server/server.go index bfa7eb6b..94461fbb 100644 --- a/server/server.go +++ b/server/server.go @@ -34,6 +34,7 @@ import ( "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/util" + "heckel.io/ntfy/v2/util/sprig" ) // Server is the main server, providing the UI and API for ntfy @@ -1132,7 +1133,11 @@ func replaceTemplate(tpl string, source string) (string, error) { if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON } - t, err := template.New("").Parse(tpl) + sprigFuncs := sprig.FuncMap() + // remove unsafe functions + delete(sprigFuncs, "env") + delete(sprigFuncs, "expandenv") + t, err := template.New("").Funcs(sprigFuncs).Parse(tpl) if err != nil { return "", errHTTPBadRequestTemplateInvalid } diff --git a/server/server_test.go b/server/server_test.go index e09f67a2..4fa059b6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3024,6 +3024,51 @@ template ""}}`, } } +func TestServer_MessageTemplate_SprigFunctions(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + bodies := []string{ + `{"foo":"bar","nested":{"title":"here"}}`, + `{"topic":"ntfy-test"}`, + `{"topic":"another-topic"}`, + } + templates := []string{ + `{{.foo | upper}} is {{.nested.title | repeat 3}}`, + `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, + `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, + } + targets := []string{ + `BAR is hereherehere`, + `Topic: test`, + `Topic: another-topic`, + } + for i, body := range bodies { + template := templates[i] + target := targets[i] + t.Run(template, func(t *testing.T) { + response := request(t, s, "PUT", `/mytopic`, body, map[string]string{ + "Template": "yes", + "Message": template, + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, target, m.Message) + }) + } +} + +func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ env "PATH" }}`, + "X-Template": "1", + }) + + require.Equal(t, 400, response.Code) + require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/util/sprig/LICENSE.txt b/util/sprig/LICENSE.txt new file mode 100644 index 00000000..f311b1ea --- /dev/null +++ b/util/sprig/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (C) 2013-2020 Masterminds + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/util/sprig/crypto.go b/util/sprig/crypto.go new file mode 100644 index 00000000..4d027781 --- /dev/null +++ b/util/sprig/crypto.go @@ -0,0 +1,37 @@ +package sprig + +import ( + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "hash/adler32" + + "github.com/google/uuid" +) + +func sha512sum(input string) string { + hash := sha512.Sum512([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +func sha256sum(input string) string { + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +func sha1sum(input string) string { + hash := sha1.Sum([]byte(input)) + return hex.EncodeToString(hash[:]) +} + +func adler32sum(input string) string { + hash := adler32.Checksum([]byte(input)) + return fmt.Sprintf("%d", hash) +} + +// uuidv4 provides a safe and secure UUID v4 implementation +func uuidv4() string { + return uuid.New().String() +} diff --git a/util/sprig/crypto_test.go b/util/sprig/crypto_test.go new file mode 100644 index 00000000..bad809a5 --- /dev/null +++ b/util/sprig/crypto_test.go @@ -0,0 +1,54 @@ +package sprig + +import ( + "testing" +) + +func TestSha512Sum(t *testing.T) { + tpl := `{{"abc" | sha512sum}}` + if err := runt(tpl, "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"); err != nil { + t.Error(err) + } +} + +func TestSha256Sum(t *testing.T) { + tpl := `{{"abc" | sha256sum}}` + if err := runt(tpl, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); err != nil { + t.Error(err) + } +} + +func TestSha1Sum(t *testing.T) { + tpl := `{{"abc" | sha1sum}}` + if err := runt(tpl, "a9993e364706816aba3e25717850c26c9cd0d89d"); err != nil { + t.Error(err) + } +} + +func TestAdler32Sum(t *testing.T) { + tpl := `{{"abc" | adler32sum}}` + if err := runt(tpl, "38600999"); err != nil { + t.Error(err) + } +} + +func TestUUIDGeneration(t *testing.T) { + tpl := `{{uuidv4}}` + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + + if len(out) != 36 { + t.Error("Expected UUID of length 36") + } + + out2, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + + if out == out2 { + t.Error("Expected subsequent UUID generations to be different") + } +} diff --git a/util/sprig/date.go b/util/sprig/date.go new file mode 100644 index 00000000..ed022dda --- /dev/null +++ b/util/sprig/date.go @@ -0,0 +1,152 @@ +package sprig + +import ( + "strconv" + "time" +) + +// Given a format and a date, format the date string. +// +// Date can be a `time.Time` or an `int, int32, int64`. +// In the later case, it is treated as seconds since UNIX +// epoch. +func date(fmt string, date interface{}) string { + return dateInZone(fmt, date, "Local") +} + +func htmlDate(date interface{}) string { + return dateInZone("2006-01-02", date, "Local") +} + +func htmlDateInZone(date interface{}, zone string) string { + return dateInZone("2006-01-02", date, zone) +} + +func dateInZone(fmt string, date interface{}, zone string) string { + var t time.Time + switch date := date.(type) { + default: + t = time.Now() + case time.Time: + t = date + case *time.Time: + t = *date + case int64: + t = time.Unix(date, 0) + case int: + t = time.Unix(int64(date), 0) + case int32: + t = time.Unix(int64(date), 0) + } + + loc, err := time.LoadLocation(zone) + if err != nil { + loc, _ = time.LoadLocation("UTC") + } + + return t.In(loc).Format(fmt) +} + +func dateModify(fmt string, date time.Time) time.Time { + d, err := time.ParseDuration(fmt) + if err != nil { + return date + } + return date.Add(d) +} + +func mustDateModify(fmt string, date time.Time) (time.Time, error) { + d, err := time.ParseDuration(fmt) + if err != nil { + return time.Time{}, err + } + return date.Add(d), nil +} + +func dateAgo(date interface{}) string { + var t time.Time + + switch date := date.(type) { + default: + t = time.Now() + case time.Time: + t = date + case int64: + t = time.Unix(date, 0) + case int: + t = time.Unix(int64(date), 0) + } + // Drop resolution to seconds + duration := time.Since(t).Round(time.Second) + return duration.String() +} + +func duration(sec interface{}) string { + var n int64 + switch value := sec.(type) { + default: + n = 0 + case string: + n, _ = strconv.ParseInt(value, 10, 64) + case int64: + n = value + } + return (time.Duration(n) * time.Second).String() +} + +func durationRound(duration interface{}) string { + var d time.Duration + switch duration := duration.(type) { + default: + d = 0 + case string: + d, _ = time.ParseDuration(duration) + case int64: + d = time.Duration(duration) + case time.Time: + d = time.Since(duration) + } + + u := uint64(d) + neg := d < 0 + if neg { + u = -u + } + + var ( + year = uint64(time.Hour) * 24 * 365 + month = uint64(time.Hour) * 24 * 30 + day = uint64(time.Hour) * 24 + hour = uint64(time.Hour) + minute = uint64(time.Minute) + second = uint64(time.Second) + ) + switch { + case u > year: + return strconv.FormatUint(u/year, 10) + "y" + case u > month: + return strconv.FormatUint(u/month, 10) + "mo" + case u > day: + return strconv.FormatUint(u/day, 10) + "d" + case u > hour: + return strconv.FormatUint(u/hour, 10) + "h" + case u > minute: + return strconv.FormatUint(u/minute, 10) + "m" + case u > second: + return strconv.FormatUint(u/second, 10) + "s" + } + return "0s" +} + +func toDate(fmt, str string) time.Time { + t, _ := time.ParseInLocation(fmt, str, time.Local) + return t +} + +func mustToDate(fmt, str string) (time.Time, error) { + return time.ParseInLocation(fmt, str, time.Local) +} + +func unixEpoch(date time.Time) string { + return strconv.FormatInt(date.Unix(), 10) +} diff --git a/util/sprig/date_test.go b/util/sprig/date_test.go new file mode 100644 index 00000000..be7ec9d9 --- /dev/null +++ b/util/sprig/date_test.go @@ -0,0 +1,120 @@ +package sprig + +import ( + "testing" + "time" +) + +func TestHtmlDate(t *testing.T) { + t.Skip() + tpl := `{{ htmlDate 0}}` + if err := runt(tpl, "1970-01-01"); err != nil { + t.Error(err) + } +} + +func TestAgo(t *testing.T) { + tpl := "{{ ago .Time }}" + if err := runtv(tpl, "2m5s", map[string]interface{}{"Time": time.Now().Add(-125 * time.Second)}); err != nil { + t.Error(err) + } + + if err := runtv(tpl, "2h34m17s", map[string]interface{}{"Time": time.Now().Add(-(2*3600 + 34*60 + 17) * time.Second)}); err != nil { + t.Error(err) + } + + if err := runtv(tpl, "-5s", map[string]interface{}{"Time": time.Now().Add(5 * time.Second)}); err != nil { + t.Error(err) + } +} + +func TestToDate(t *testing.T) { + tpl := `{{toDate "2006-01-02" "2017-12-31" | date "02/01/2006"}}` + if err := runt(tpl, "31/12/2017"); err != nil { + t.Error(err) + } +} + +func TestUnixEpoch(t *testing.T) { + tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT") + if err != nil { + t.Error(err) + } + tpl := `{{unixEpoch .Time}}` + + if err = runtv(tpl, "1560458379", map[string]interface{}{"Time": tm}); err != nil { + t.Error(err) + } +} + +func TestDateInZone(t *testing.T) { + tm, err := time.Parse("02 Jan 06 15:04:05 MST", "13 Jun 19 20:39:39 GMT") + if err != nil { + t.Error(err) + } + tpl := `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "UTC" }}` + + // Test time.Time input + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { + t.Error(err) + } + + // Test pointer to time.Time input + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": &tm}); err != nil { + t.Error(err) + } + + // Test no time input. This should be close enough to time.Now() we can test + loc, _ := time.LoadLocation("UTC") + if err = runtv(tpl, time.Now().In(loc).Format("02 Jan 06 15:04 -0700"), map[string]interface{}{"Time": ""}); err != nil { + t.Error(err) + } + + // Test unix timestamp as int64 + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int64(1560458379)}); err != nil { + t.Error(err) + } + + // Test unix timestamp as int32 + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int32(1560458379)}); err != nil { + t.Error(err) + } + + // Test unix timestamp as int + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int(1560458379)}); err != nil { + t.Error(err) + } + + // Test case of invalid timezone + tpl = `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "foobar" }}` + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { + t.Error(err) + } +} + +func TestDuration(t *testing.T) { + tpl := "{{ duration .Secs }}" + if err := runtv(tpl, "1m1s", map[string]interface{}{"Secs": "61"}); err != nil { + t.Error(err) + } + if err := runtv(tpl, "1h0m0s", map[string]interface{}{"Secs": "3600"}); err != nil { + t.Error(err) + } + // 1d2h3m4s but go is opinionated + if err := runtv(tpl, "26h3m4s", map[string]interface{}{"Secs": "93784"}); err != nil { + t.Error(err) + } +} + +func TestDurationRound(t *testing.T) { + tpl := "{{ durationRound .Time }}" + if err := runtv(tpl, "2h", map[string]interface{}{"Time": "2h5s"}); err != nil { + t.Error(err) + } + if err := runtv(tpl, "1d", map[string]interface{}{"Time": "24h5s"}); err != nil { + t.Error(err) + } + if err := runtv(tpl, "3mo", map[string]interface{}{"Time": "2400h5s"}); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go new file mode 100644 index 00000000..201b7e24 --- /dev/null +++ b/util/sprig/defaults.go @@ -0,0 +1,163 @@ +package sprig + +import ( + "bytes" + "encoding/json" + "math/rand" + "reflect" + "strings" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// dfault checks whether `given` is set, and returns default if not set. +// +// This returns `d` if `given` appears not to be set, and `given` otherwise. +// +// For numeric types 0 is unset. +// For strings, maps, arrays, and slices, len() = 0 is considered unset. +// For bool, false is unset. +// Structs are never considered unset. +// +// For everything else, including pointers, a nil value is unset. +func dfault(d interface{}, given ...interface{}) interface{} { + + if empty(given) || empty(given[0]) { + return d + } + return given[0] +} + +// empty returns true if the given value has the zero value for its type. +func empty(given interface{}) bool { + g := reflect.ValueOf(given) + if !g.IsValid() { + return true + } + + // Basically adapted from text/template.isTrue + switch g.Kind() { + default: + return g.IsNil() + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return g.Len() == 0 + case reflect.Bool: + return !g.Bool() + case reflect.Complex64, reflect.Complex128: + return g.Complex() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return g.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return g.Uint() == 0 + case reflect.Float32, reflect.Float64: + return g.Float() == 0 + case reflect.Struct: + return false + } +} + +// coalesce returns the first non-empty value. +func coalesce(v ...interface{}) interface{} { + for _, val := range v { + if !empty(val) { + return val + } + } + return nil +} + +// all returns true if empty(x) is false for all values x in the list. +// If the list is empty, return true. +func all(v ...interface{}) bool { + for _, val := range v { + if empty(val) { + return false + } + } + return true +} + +// any returns true if empty(x) is false for any x in the list. +// If the list is empty, return false. +func any(v ...interface{}) bool { + for _, val := range v { + if !empty(val) { + return true + } + } + return false +} + +// fromJSON decodes JSON into a structured value, ignoring errors. +func fromJSON(v string) interface{} { + output, _ := mustFromJSON(v) + return output +} + +// mustFromJSON decodes JSON into a structured value, returning errors. +func mustFromJSON(v string) (interface{}, error) { + var output interface{} + err := json.Unmarshal([]byte(v), &output) + return output, err +} + +// toJSON encodes an item into a JSON string +func toJSON(v interface{}) string { + output, _ := json.Marshal(v) + return string(output) +} + +func mustToJSON(v interface{}) (string, error) { + output, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(output), nil +} + +// toPrettyJSON encodes an item into a pretty (indented) JSON string +func toPrettyJSON(v interface{}) string { + output, _ := json.MarshalIndent(v, "", " ") + return string(output) +} + +func mustToPrettyJSON(v interface{}) (string, error) { + output, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "", err + } + return string(output), nil +} + +// toRawJSON encodes an item into a JSON string with no escaping of HTML characters. +func toRawJSON(v interface{}) string { + output, err := mustToRawJSON(v) + if err != nil { + panic(err) + } + return string(output) +} + +// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters. +func mustToRawJSON(v interface{}) (string, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err := enc.Encode(&v) + if err != nil { + return "", err + } + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +// ternary returns the first value if the last value is true, otherwise returns the second value. +func ternary(vt interface{}, vf interface{}, v bool) interface{} { + if v { + return vt + } + + return vf +} diff --git a/util/sprig/defaults_test.go b/util/sprig/defaults_test.go new file mode 100644 index 00000000..eb7e35b4 --- /dev/null +++ b/util/sprig/defaults_test.go @@ -0,0 +1,196 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefault(t *testing.T) { + tpl := `{{"" | default "foo"}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + tpl = `{{default "foo" 234}}` + if err := runt(tpl, "234"); err != nil { + t.Error(err) + } + tpl = `{{default "foo" 2.34}}` + if err := runt(tpl, "2.34"); err != nil { + t.Error(err) + } + + tpl = `{{ .Nothing | default "123" }}` + if err := runt(tpl, "123"); err != nil { + t.Error(err) + } + tpl = `{{ default "123" }}` + if err := runt(tpl, "123"); err != nil { + t.Error(err) + } +} + +func TestEmpty(t *testing.T) { + tpl := `{{if empty 1}}1{{else}}0{{end}}` + if err := runt(tpl, "0"); err != nil { + t.Error(err) + } + + tpl = `{{if empty 0}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + tpl = `{{if empty ""}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + tpl = `{{if empty 0.0}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + tpl = `{{if empty false}}1{{else}}0{{end}}` + if err := runt(tpl, "1"); err != nil { + t.Error(err) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl = `{{if empty .top.NoSuchThing}}1{{else}}0{{end}}` + if err := runtv(tpl, "1", dict); err != nil { + t.Error(err) + } + tpl = `{{if empty .bottom.NoSuchThing}}1{{else}}0{{end}}` + if err := runtv(tpl, "1", dict); err != nil { + t.Error(err) + } +} + +func TestCoalesce(t *testing.T) { + tests := map[string]string{ + `{{ coalesce 1 }}`: "1", + `{{ coalesce "" 0 nil 2 }}`: "2", + `{{ $two := 2 }}{{ coalesce "" 0 nil $two }}`: "2", + `{{ $two := 2 }}{{ coalesce "" $two 0 0 0 }}`: "2", + `{{ $two := 2 }}{{ coalesce "" $two 3 4 5 }}`: "2", + `{{ coalesce }}`: "", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl := `{{ coalesce .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` + if err := runtv(tpl, "airplane", dict); err != nil { + t.Error(err) + } +} + +func TestAll(t *testing.T) { + tests := map[string]string{ + `{{ all 1 }}`: "true", + `{{ all "" 0 nil 2 }}`: "false", + `{{ $two := 2 }}{{ all "" 0 nil $two }}`: "false", + `{{ $two := 2 }}{{ all "" $two 0 0 0 }}`: "false", + `{{ $two := 2 }}{{ all "" $two 3 4 5 }}`: "false", + `{{ all }}`: "true", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl := `{{ all .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` + if err := runtv(tpl, "false", dict); err != nil { + t.Error(err) + } +} + +func TestAny(t *testing.T) { + tests := map[string]string{ + `{{ any 1 }}`: "true", + `{{ any "" 0 nil 2 }}`: "true", + `{{ $two := 2 }}{{ any "" 0 nil $two }}`: "true", + `{{ $two := 2 }}{{ any "" $two 3 4 5 }}`: "true", + `{{ $zero := 0 }}{{ any "" $zero 0 0 0 }}`: "false", + `{{ any }}`: "false", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } + + dict := map[string]interface{}{"top": map[string]interface{}{}} + tpl := `{{ any .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` + if err := runtv(tpl, "true", dict); err != nil { + t.Error(err) + } +} + +func TestFromJSON(t *testing.T) { + dict := map[string]interface{}{"Input": `{"foo": 55}`} + + tpl := `{{.Input | fromJSON}}` + expected := `map[foo:55]` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } + + tpl = `{{(.Input | fromJSON).foo}}` + expected = `55` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestToJSON(t *testing.T) { + dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} + + tpl := `{{.Top | toJSON}}` + expected := `{"bool":true,"number":42,"string":"test"}` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestToPrettyJSON(t *testing.T) { + dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} + tpl := `{{.Top | toPrettyJSON}}` + expected := `{ + "bool": true, + "number": 42, + "string": "test" +}` + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestToRawJSON(t *testing.T) { + dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42, "html": ""}} + tpl := `{{.Top | toRawJSON}}` + expected := `{"bool":true,"html":"","number":42,"string":"test"}` + + if err := runtv(tpl, expected, dict); err != nil { + t.Error(err) + } +} + +func TestTernary(t *testing.T) { + tpl := `{{true | ternary "foo" "bar"}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + + tpl = `{{ternary "foo" "bar" true}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + + tpl = `{{false | ternary "foo" "bar"}}` + if err := runt(tpl, "bar"); err != nil { + t.Error(err) + } + + tpl = `{{ternary "foo" "bar" false}}` + if err := runt(tpl, "bar"); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/dict.go b/util/sprig/dict.go new file mode 100644 index 00000000..fd2dd711 --- /dev/null +++ b/util/sprig/dict.go @@ -0,0 +1,118 @@ +package sprig + +func get(d map[string]interface{}, key string) interface{} { + if val, ok := d[key]; ok { + return val + } + return "" +} + +func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} { + d[key] = value + return d +} + +func unset(d map[string]interface{}, key string) map[string]interface{} { + delete(d, key) + return d +} + +func hasKey(d map[string]interface{}, key string) bool { + _, ok := d[key] + return ok +} + +func pluck(key string, d ...map[string]interface{}) []interface{} { + res := []interface{}{} + for _, dict := range d { + if val, ok := dict[key]; ok { + res = append(res, val) + } + } + return res +} + +func keys(dicts ...map[string]interface{}) []string { + k := []string{} + for _, dict := range dicts { + for key := range dict { + k = append(k, key) + } + } + return k +} + +func pick(dict map[string]interface{}, keys ...string) map[string]interface{} { + res := map[string]interface{}{} + for _, k := range keys { + if v, ok := dict[k]; ok { + res[k] = v + } + } + return res +} + +func omit(dict map[string]interface{}, keys ...string) map[string]interface{} { + res := map[string]interface{}{} + + omit := make(map[string]bool, len(keys)) + for _, k := range keys { + omit[k] = true + } + + for k, v := range dict { + if _, ok := omit[k]; !ok { + res[k] = v + } + } + return res +} + +func dict(v ...interface{}) map[string]interface{} { + dict := map[string]interface{}{} + lenv := len(v) + for i := 0; i < lenv; i += 2 { + key := strval(v[i]) + if i+1 >= lenv { + dict[key] = "" + continue + } + dict[key] = v[i+1] + } + return dict +} + +func values(dict map[string]interface{}) []interface{} { + values := []interface{}{} + for _, value := range dict { + values = append(values, value) + } + + return values +} + +func dig(ps ...interface{}) (interface{}, error) { + if len(ps) < 3 { + panic("dig needs at least three arguments") + } + dict := ps[len(ps)-1].(map[string]interface{}) + def := ps[len(ps)-2] + ks := make([]string, len(ps)-2) + for i := 0; i < len(ks); i++ { + ks[i] = ps[i].(string) + } + + return digFromDict(dict, def, ks) +} + +func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) { + k, ns := ks[0], ks[1:] + step, has := dict[k] + if !has { + return d, nil + } + if len(ns) == 0 { + return step, nil + } + return digFromDict(step.(map[string]interface{}), d, ns) +} diff --git a/util/sprig/dict_test.go b/util/sprig/dict_test.go new file mode 100644 index 00000000..0b293140 --- /dev/null +++ b/util/sprig/dict_test.go @@ -0,0 +1,166 @@ +package sprig + +import ( + "strings" + "testing" +) + +func TestDict(t *testing.T) { + tpl := `{{$d := dict 1 2 "three" "four" 5}}{{range $k, $v := $d}}{{$k}}{{$v}}{{end}}` + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + if len(out) != 12 { + t.Errorf("Expected length 12, got %d", len(out)) + } + // dict does not guarantee ordering because it is backed by a map. + if !strings.Contains(out, "12") { + t.Error("Expected grouping 12") + } + if !strings.Contains(out, "threefour") { + t.Error("Expected grouping threefour") + } + if !strings.Contains(out, "5") { + t.Error("Expected 5") + } + tpl = `{{$t := dict "I" "shot" "the" "albatross"}}{{$t.the}} {{$t.I}}` + if err := runt(tpl, "albatross shot"); err != nil { + t.Error(err) + } +} + +func TestUnset(t *testing.T) { + tpl := `{{- $d := dict "one" 1 "two" 222222 -}} + {{- $_ := unset $d "two" -}} + {{- range $k, $v := $d}}{{$k}}{{$v}}{{- end -}} + ` + + expect := "one1" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} +func TestHasKey(t *testing.T) { + tpl := `{{- $d := dict "one" 1 "two" 222222 -}} + {{- if hasKey $d "one" -}}1{{- end -}} + ` + + expect := "1" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} + +func TestPluck(t *testing.T) { + tpl := ` + {{- $d := dict "one" 1 "two" 222222 -}} + {{- $d2 := dict "one" 1 "two" 33333 -}} + {{- $d3 := dict "one" 1 -}} + {{- $d4 := dict "one" 1 "two" 4444 -}} + {{- pluck "two" $d $d2 $d3 $d4 -}} + ` + + expect := "[222222 33333 4444]" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} + +func TestKeys(t *testing.T) { + tests := map[string]string{ + `{{ dict "foo" 1 "bar" 2 | keys | sortAlpha }}`: "[bar foo]", + `{{ dict | keys }}`: "[]", + `{{ keys (dict "foo" 1) (dict "bar" 2) (dict "bar" 3) | uniq | sortAlpha }}`: "[bar foo]", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestPick(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" | len -}}`: "1", + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" -}}`: "map[two:222222]", + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" | len -}}`: "2", + `{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "one" "two" "three" | len -}}`: "2", + `{{- $d := dict }}{{ pick $d "two" | len -}}`: "0", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} +func TestOmit(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" | len -}}`: "1", + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" -}}`: "map[two:222222]", + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "one" "two" | len -}}`: "0", + `{{- $d := dict "one" 1 "two" 222222 }}{{ omit $d "two" "three" | len -}}`: "1", + `{{- $d := dict }}{{ omit $d "two" | len -}}`: "0", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestGet(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "one" 1 }}{{ get $d "one" -}}`: "1", + `{{- $d := dict "one" 1 "two" "2" }}{{ get $d "two" -}}`: "2", + `{{- $d := dict }}{{ get $d "two" -}}`: "", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestSet(t *testing.T) { + tpl := `{{- $d := dict "one" 1 "two" 222222 -}} + {{- $_ := set $d "two" 2 -}} + {{- $_ := set $d "three" 3 -}} + {{- if hasKey $d "one" -}}{{$d.one}}{{- end -}} + {{- if hasKey $d "two" -}}{{$d.two}}{{- end -}} + {{- if hasKey $d "three" -}}{{$d.three}}{{- end -}} + ` + + expect := "123" + if err := runt(tpl, expect); err != nil { + t.Error(err) + } +} + +func TestValues(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "a" 1 "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "1,2", + `{{- $d := dict "a" "first" "b" 2 }}{{ values $d | sortAlpha | join "," }}`: "2,first", + } + + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} + +func TestDig(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "c" "" $d }}`: "1", + `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "z" "2" $d }}`: "2", + `{{ dict "a" 1 | dig "a" "" }}`: "1", + `{{ dict "a" 1 | dig "z" "2" }}`: "2", + } + + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} diff --git a/util/sprig/doc.go b/util/sprig/doc.go new file mode 100644 index 00000000..91031d6d --- /dev/null +++ b/util/sprig/doc.go @@ -0,0 +1,19 @@ +/* +Package sprig provides template functions for Go. + +This package contains a number of utility functions for working with data +inside of Go `html/template` and `text/template` files. + +To add these functions, use the `template.Funcs()` method: + + t := template.New("foo").Funcs(sprig.FuncMap()) + +Note that you should add the function map before you parse any template files. + + In several cases, Sprig reverses the order of arguments from the way they + appear in the standard library. This is to make it easier to pipe + arguments into functions. + +See http://masterminds.github.io/sprig/ for more detailed documentation on each of the available functions. +*/ +package sprig diff --git a/util/sprig/example_test.go b/util/sprig/example_test.go new file mode 100644 index 00000000..2d7696bf --- /dev/null +++ b/util/sprig/example_test.go @@ -0,0 +1,25 @@ +package sprig + +import ( + "fmt" + "os" + "text/template" +) + +func Example() { + // Set up variables and template. + vars := map[string]interface{}{"Name": " John Jacob Jingleheimer Schmidt "} + tpl := `Hello {{.Name | trim | lower}}` + + // Get the Sprig function map. + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + + err := t.Execute(os.Stdout, vars) + if err != nil { + fmt.Printf("Error during template execution: %s", err) + return + } + // Output: + // Hello john jacob jingleheimer schmidt +} diff --git a/util/sprig/flow_control_test.go b/util/sprig/flow_control_test.go new file mode 100644 index 00000000..d4e5ebf0 --- /dev/null +++ b/util/sprig/flow_control_test.go @@ -0,0 +1,16 @@ +package sprig + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFail(t *testing.T) { + const msg = "This is an error!" + tpl := fmt.Sprintf(`{{fail "%s"}}`, msg) + _, err := runRaw(tpl, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), msg) +} diff --git a/util/sprig/functions.go b/util/sprig/functions.go new file mode 100644 index 00000000..8549e99c --- /dev/null +++ b/util/sprig/functions.go @@ -0,0 +1,302 @@ +package sprig + +import ( + "errors" + "html/template" + "math/rand" + "path" + "path/filepath" + "reflect" + "strconv" + "strings" + ttemplate "text/template" + "time" +) + +// FuncMap produces the function map. +// +// Use this to pass the functions into the template engine: +// +// tpl := template.New("foo").Funcs(sprig.FuncMap())) +func FuncMap() template.FuncMap { + return HTMLFuncMap() +} + +// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. +func HermeticTxtFuncMap() ttemplate.FuncMap { + r := TxtFuncMap() + for _, name := range nonhermeticFunctions { + delete(r, name) + } + return r +} + +// HermeticHTMLFuncMap returns an 'html/template'.Funcmap with only repeatable functions. +func HermeticHTMLFuncMap() template.FuncMap { + r := HTMLFuncMap() + for _, name := range nonhermeticFunctions { + delete(r, name) + } + return r +} + +// TxtFuncMap returns a 'text/template'.FuncMap +func TxtFuncMap() ttemplate.FuncMap { + return ttemplate.FuncMap(GenericFuncMap()) +} + +// HTMLFuncMap returns an 'html/template'.Funcmap +func HTMLFuncMap() template.FuncMap { + return template.FuncMap(GenericFuncMap()) +} + +// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. +func GenericFuncMap() map[string]interface{} { + gfm := make(map[string]interface{}, len(genericMap)) + for k, v := range genericMap { + gfm[k] = v + } + return gfm +} + +// These functions are not guaranteed to evaluate to the same result for given input, because they +// refer to the environment or global state. +var nonhermeticFunctions = []string{ + // Date functions + "date", + "date_in_zone", + "date_modify", + "now", + "htmlDate", + "htmlDateInZone", + "dateInZone", + "dateModify", + + // Strings + "randAlphaNum", + "randAlpha", + "randAscii", + "randNumeric", + "randBytes", + "uuidv4", +} + +var genericMap = map[string]interface{}{ + "hello": func() string { return "Hello!" }, + + // Date functions + "ago": dateAgo, + "date": date, + "date_in_zone": dateInZone, + "date_modify": dateModify, + "dateInZone": dateInZone, + "dateModify": dateModify, + "duration": duration, + "durationRound": durationRound, + "htmlDate": htmlDate, + "htmlDateInZone": htmlDateInZone, + "must_date_modify": mustDateModify, + "mustDateModify": mustDateModify, + "mustToDate": mustToDate, + "now": time.Now, + "toDate": toDate, + "unixEpoch": unixEpoch, + + // Strings + "trunc": trunc, + "trim": strings.TrimSpace, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": strings.Title, + "substr": substring, + // Switch order so that "foo" | repeat 5 + "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, + // Deprecated: Use trimAll. + "trimall": func(a, b string) string { return strings.Trim(b, a) }, + // Switch order so that "$foo" | trimall "$" + "trimAll": func(a, b string) string { return strings.Trim(b, a) }, + "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, + "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, + // Switch order so that "foobar" | contains "foo" + "contains": func(substr string, str string) bool { return strings.Contains(str, substr) }, + "hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) }, + "hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) }, + "quote": quote, + "squote": squote, + "cat": cat, + "indent": indent, + "nindent": nindent, + "replace": replace, + "plural": plural, + "sha1sum": sha1sum, + "sha256sum": sha256sum, + "sha512sum": sha512sum, + "adler32sum": adler32sum, + "toString": strval, + + // Wrap Atoi to stop errors. + "atoi": func(a string) int { i, _ := strconv.Atoi(a); return i }, + "seq": seq, + "toDecimal": toDecimal, + + //"gt": func(a, b int) bool {return a > b}, + //"gte": func(a, b int) bool {return a >= b}, + //"lt": func(a, b int) bool {return a < b}, + //"lte": func(a, b int) bool {return a <= b}, + + // split "/" foo/bar returns map[int]string{0: foo, 1: bar} + "split": split, + "splitList": func(sep, orig string) []string { return strings.Split(orig, sep) }, + // splitn "/" foo/bar/fuu returns map[int]string{0: foo, 1: bar/fuu} + "splitn": splitn, + "toStrings": strslice, + + "until": until, + "untilStep": untilStep, + + // VERY basic arithmetic. + "add1": func(i interface{}) int64 { return toInt64(i) + 1 }, + "add": func(i ...interface{}) int64 { + var a int64 = 0 + for _, b := range i { + a += toInt64(b) + } + return a + }, + "sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) }, + "div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) }, + "mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) }, + "mul": func(a interface{}, v ...interface{}) int64 { + val := toInt64(a) + for _, b := range v { + val = val * toInt64(b) + } + return val + }, + "randInt": func(min, max int) int { return rand.Intn(max-min) + min }, + "biggest": max, + "max": max, + "min": min, + "maxf": maxf, + "minf": minf, + "ceil": ceil, + "floor": floor, + "round": round, + + // string slices. Note that we reverse the order b/c that's better + // for template processing. + "join": join, + "sortAlpha": sortAlpha, + + // Defaults + "default": dfault, + "empty": empty, + "coalesce": coalesce, + "all": all, + "any": any, + "compact": compact, + "mustCompact": mustCompact, + "fromJSON": fromJSON, + "toJSON": toJSON, + "toPrettyJSON": toPrettyJSON, + "toRawJSON": toRawJSON, + "mustFromJSON": mustFromJSON, + "mustToJSON": mustToJSON, + "mustToPrettyJSON": mustToPrettyJSON, + "mustToRawJSON": mustToRawJSON, + "ternary": ternary, + + // Reflection + "typeOf": typeOf, + "typeIs": typeIs, + "typeIsLike": typeIsLike, + "kindOf": kindOf, + "kindIs": kindIs, + "deepEqual": reflect.DeepEqual, + + // Paths: + "base": path.Base, + "dir": path.Dir, + "clean": path.Clean, + "ext": path.Ext, + "isAbs": path.IsAbs, + + // Filepaths: + "osBase": filepath.Base, + "osClean": filepath.Clean, + "osDir": filepath.Dir, + "osExt": filepath.Ext, + "osIsAbs": filepath.IsAbs, + + // Encoding: + "b64enc": base64encode, + "b64dec": base64decode, + "b32enc": base32encode, + "b32dec": base32decode, + + // Data Structures: + "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. + "list": list, + "dict": dict, + "get": get, + "set": set, + "unset": unset, + "hasKey": hasKey, + "pluck": pluck, + "keys": keys, + "pick": pick, + "omit": omit, + "values": values, + + "append": push, "push": push, + "mustAppend": mustPush, "mustPush": mustPush, + "prepend": prepend, + "mustPrepend": mustPrepend, + "first": first, + "mustFirst": mustFirst, + "rest": rest, + "mustRest": mustRest, + "last": last, + "mustLast": mustLast, + "initial": initial, + "mustInitial": mustInitial, + "reverse": reverse, + "mustReverse": mustReverse, + "uniq": uniq, + "mustUniq": mustUniq, + "without": without, + "mustWithout": mustWithout, + "has": has, + "mustHas": mustHas, + "slice": slice, + "mustSlice": mustSlice, + "concat": concat, + "dig": dig, + "chunk": chunk, + "mustChunk": mustChunk, + + // UUIDs: + "uuidv4": uuidv4, + + // Flow Control: + "fail": func(msg string) (string, error) { return "", errors.New(msg) }, + + // Regex + "regexMatch": regexMatch, + "mustRegexMatch": mustRegexMatch, + "regexFindAll": regexFindAll, + "mustRegexFindAll": mustRegexFindAll, + "regexFind": regexFind, + "mustRegexFind": mustRegexFind, + "regexReplaceAll": regexReplaceAll, + "mustRegexReplaceAll": mustRegexReplaceAll, + "regexReplaceAllLiteral": regexReplaceAllLiteral, + "mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral, + "regexSplit": regexSplit, + "mustRegexSplit": mustRegexSplit, + "regexQuoteMeta": regexQuoteMeta, + + // URLs: + "urlParse": urlParse, + "urlJoin": urlJoin, +} diff --git a/util/sprig/functions_linux_test.go b/util/sprig/functions_linux_test.go new file mode 100644 index 00000000..cfbf253a --- /dev/null +++ b/util/sprig/functions_linux_test.go @@ -0,0 +1,28 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOsBase(t *testing.T) { + assert.NoError(t, runt(`{{ osBase "foo/bar" }}`, "bar")) +} + +func TestOsDir(t *testing.T) { + assert.NoError(t, runt(`{{ osDir "foo/bar/baz" }}`, "foo/bar")) +} + +func TestOsIsAbs(t *testing.T) { + assert.NoError(t, runt(`{{ osIsAbs "/foo" }}`, "true")) + assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) +} + +func TestOsClean(t *testing.T) { + assert.NoError(t, runt(`{{ osClean "/foo/../foo/../bar" }}`, "/bar")) +} + +func TestOsExt(t *testing.T) { + assert.NoError(t, runt(`{{ osExt "/foo/bar/baz.txt" }}`, ".txt")) +} diff --git a/util/sprig/functions_test.go b/util/sprig/functions_test.go new file mode 100644 index 00000000..b7bc01f4 --- /dev/null +++ b/util/sprig/functions_test.go @@ -0,0 +1,70 @@ +package sprig + +import ( + "bytes" + "fmt" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestBase(t *testing.T) { + assert.NoError(t, runt(`{{ base "foo/bar" }}`, "bar")) +} + +func TestDir(t *testing.T) { + assert.NoError(t, runt(`{{ dir "foo/bar/baz" }}`, "foo/bar")) +} + +func TestIsAbs(t *testing.T) { + assert.NoError(t, runt(`{{ isAbs "/foo" }}`, "true")) + assert.NoError(t, runt(`{{ isAbs "foo" }}`, "false")) +} + +func TestClean(t *testing.T) { + assert.NoError(t, runt(`{{ clean "/foo/../foo/../bar" }}`, "/bar")) +} + +func TestExt(t *testing.T) { + assert.NoError(t, runt(`{{ ext "/foo/bar/baz.txt" }}`, ".txt")) +} + +func TestRegex(t *testing.T) { + assert.NoError(t, runt(`{{ regexQuoteMeta "1.2.3" }}`, "1\\.2\\.3")) + assert.NoError(t, runt(`{{ regexQuoteMeta "pretzel" }}`, "pretzel")) +} + +// runt runs a template and checks that the output exactly matches the expected string. +func runt(tpl, expect string) error { + return runtv(tpl, expect, map[string]string{}) +} + +// runtv takes a template, and expected return, and values for substitution. +// +// It runs the template and verifies that the output is an exact match. +func runtv(tpl, expect string, vars interface{}) error { + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + var b bytes.Buffer + err := t.Execute(&b, vars) + if err != nil { + return err + } + if expect != b.String() { + return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) + } + return nil +} + +// runRaw runs a template with the given variables and returns the result. +func runRaw(tpl string, vars interface{}) (string, error) { + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + var b bytes.Buffer + err := t.Execute(&b, vars) + if err != nil { + return "", err + } + return b.String(), nil +} diff --git a/util/sprig/functions_windows_test.go b/util/sprig/functions_windows_test.go new file mode 100644 index 00000000..9d8bd0e5 --- /dev/null +++ b/util/sprig/functions_windows_test.go @@ -0,0 +1,28 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOsBase(t *testing.T) { + assert.NoError(t, runt(`{{ osBase "C:\\foo\\bar" }}`, "bar")) +} + +func TestOsDir(t *testing.T) { + assert.NoError(t, runt(`{{ osDir "C:\\foo\\bar\\baz" }}`, "C:\\foo\\bar")) +} + +func TestOsIsAbs(t *testing.T) { + assert.NoError(t, runt(`{{ osIsAbs "C:\\foo" }}`, "true")) + assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) +} + +func TestOsClean(t *testing.T) { + assert.NoError(t, runt(`{{ osClean "C:\\foo\\..\\foo\\..\\bar" }}`, "C:\\bar")) +} + +func TestOsExt(t *testing.T) { + assert.NoError(t, runt(`{{ osExt "C:\\foo\\bar\\baz.txt" }}`, ".txt")) +} diff --git a/util/sprig/list.go b/util/sprig/list.go new file mode 100644 index 00000000..ca0fbb78 --- /dev/null +++ b/util/sprig/list.go @@ -0,0 +1,464 @@ +package sprig + +import ( + "fmt" + "math" + "reflect" + "sort" +) + +// Reflection is used in these functions so that slices and arrays of strings, +// ints, and other types not implementing []interface{} can be worked with. +// For example, this is useful if you need to work on the output of regexs. + +func list(v ...interface{}) []interface{} { + return v +} + +func push(list interface{}, v interface{}) []interface{} { + l, err := mustPush(list, v) + if err != nil { + panic(err) + } + + return l +} + +func mustPush(list interface{}, v interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + nl := make([]interface{}, l) + for i := 0; i < l; i++ { + nl[i] = l2.Index(i).Interface() + } + + return append(nl, v), nil + + default: + return nil, fmt.Errorf("Cannot push on type %s", tp) + } +} + +func prepend(list interface{}, v interface{}) []interface{} { + l, err := mustPrepend(list, v) + if err != nil { + panic(err) + } + + return l +} + +func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { + //return append([]interface{}{v}, list...) + + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + nl := make([]interface{}, l) + for i := 0; i < l; i++ { + nl[i] = l2.Index(i).Interface() + } + + return append([]interface{}{v}, nl...), nil + + default: + return nil, fmt.Errorf("Cannot prepend on type %s", tp) + } +} + +func chunk(size int, list interface{}) [][]interface{} { + l, err := mustChunk(size, list) + if err != nil { + panic(err) + } + + return l +} + +func mustChunk(size int, list interface{}) ([][]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + + cs := int(math.Floor(float64(l-1)/float64(size)) + 1) + nl := make([][]interface{}, cs) + + for i := 0; i < cs; i++ { + clen := size + if i == cs-1 { + clen = int(math.Floor(math.Mod(float64(l), float64(size)))) + if clen == 0 { + clen = size + } + } + + nl[i] = make([]interface{}, clen) + + for j := 0; j < clen; j++ { + ix := i*size + j + nl[i][j] = l2.Index(ix).Interface() + } + } + + return nl, nil + + default: + return nil, fmt.Errorf("Cannot chunk type %s", tp) + } +} + +func last(list interface{}) interface{} { + l, err := mustLast(list) + if err != nil { + panic(err) + } + + return l +} + +func mustLast(list interface{}) (interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + return l2.Index(l - 1).Interface(), nil + default: + return nil, fmt.Errorf("Cannot find last on type %s", tp) + } +} + +func first(list interface{}) interface{} { + l, err := mustFirst(list) + if err != nil { + panic(err) + } + + return l +} + +func mustFirst(list interface{}) (interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + return l2.Index(0).Interface(), nil + default: + return nil, fmt.Errorf("Cannot find first on type %s", tp) + } +} + +func rest(list interface{}) []interface{} { + l, err := mustRest(list) + if err != nil { + panic(err) + } + + return l +} + +func mustRest(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + nl := make([]interface{}, l-1) + for i := 1; i < l; i++ { + nl[i-1] = l2.Index(i).Interface() + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot find rest on type %s", tp) + } +} + +func initial(list interface{}) []interface{} { + l, err := mustInitial(list) + if err != nil { + panic(err) + } + + return l +} + +func mustInitial(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + nl := make([]interface{}, l-1) + for i := 0; i < l-1; i++ { + nl[i] = l2.Index(i).Interface() + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot find initial on type %s", tp) + } +} + +func sortAlpha(list interface{}) []string { + k := reflect.Indirect(reflect.ValueOf(list)).Kind() + switch k { + case reflect.Slice, reflect.Array: + a := strslice(list) + s := sort.StringSlice(a) + s.Sort() + return s + } + return []string{strval(list)} +} + +func reverse(v interface{}) []interface{} { + l, err := mustReverse(v) + if err != nil { + panic(err) + } + + return l +} + +func mustReverse(v interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(v).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(v) + + l := l2.Len() + // We do not sort in place because the incoming array should not be altered. + nl := make([]interface{}, l) + for i := 0; i < l; i++ { + nl[l-i-1] = l2.Index(i).Interface() + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot find reverse on type %s", tp) + } +} + +func compact(list interface{}) []interface{} { + l, err := mustCompact(list) + if err != nil { + panic(err) + } + + return l +} + +func mustCompact(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + nl := []interface{}{} + var item interface{} + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if !empty(item) { + nl = append(nl, item) + } + } + + return nl, nil + default: + return nil, fmt.Errorf("Cannot compact on type %s", tp) + } +} + +func uniq(list interface{}) []interface{} { + l, err := mustUniq(list) + if err != nil { + panic(err) + } + + return l +} + +func mustUniq(list interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + dest := []interface{}{} + var item interface{} + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if !inList(dest, item) { + dest = append(dest, item) + } + } + + return dest, nil + default: + return nil, fmt.Errorf("Cannot find uniq on type %s", tp) + } +} + +func inList(haystack []interface{}, needle interface{}) bool { + for _, h := range haystack { + if reflect.DeepEqual(needle, h) { + return true + } + } + return false +} + +func without(list interface{}, omit ...interface{}) []interface{} { + l, err := mustWithout(list, omit...) + if err != nil { + panic(err) + } + + return l +} + +func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + res := []interface{}{} + var item interface{} + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if !inList(omit, item) { + res = append(res, item) + } + } + + return res, nil + default: + return nil, fmt.Errorf("Cannot find without on type %s", tp) + } +} + +func has(needle interface{}, haystack interface{}) bool { + l, err := mustHas(needle, haystack) + if err != nil { + panic(err) + } + + return l +} + +func mustHas(needle interface{}, haystack interface{}) (bool, error) { + if haystack == nil { + return false, nil + } + tp := reflect.TypeOf(haystack).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(haystack) + var item interface{} + l := l2.Len() + for i := 0; i < l; i++ { + item = l2.Index(i).Interface() + if reflect.DeepEqual(needle, item) { + return true, nil + } + } + + return false, nil + default: + return false, fmt.Errorf("Cannot find has on type %s", tp) + } +} + +// $list := [1, 2, 3, 4, 5] +// slice $list -> list[0:5] = list[:] +// slice $list 0 3 -> list[0:3] = list[:3] +// slice $list 3 5 -> list[3:5] +// slice $list 3 -> list[3:5] = list[3:] +func slice(list interface{}, indices ...interface{}) interface{} { + l, err := mustSlice(list, indices...) + if err != nil { + panic(err) + } + + return l +} + +func mustSlice(list interface{}, indices ...interface{}) (interface{}, error) { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil, nil + } + + var start, end int + if len(indices) > 0 { + start = toInt(indices[0]) + } + if len(indices) < 2 { + end = l + } else { + end = toInt(indices[1]) + } + + return l2.Slice(start, end).Interface(), nil + default: + return nil, fmt.Errorf("list should be type of slice or array but %s", tp) + } +} + +func concat(lists ...interface{}) interface{} { + var res []interface{} + for _, list := range lists { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + for i := 0; i < l2.Len(); i++ { + res = append(res, l2.Index(i).Interface()) + } + default: + panic(fmt.Sprintf("Cannot concat type %s as list", tp)) + } + } + return res +} diff --git a/util/sprig/list_test.go b/util/sprig/list_test.go new file mode 100644 index 00000000..ec4c4c14 --- /dev/null +++ b/util/sprig/list_test.go @@ -0,0 +1,364 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTuple(t *testing.T) { + tpl := `{{$t := tuple 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}` + if err := runt(tpl, "foo1a"); err != nil { + t.Error(err) + } +} + +func TestList(t *testing.T) { + tpl := `{{$t := list 1 "a" "foo"}}{{index $t 2}}{{index $t 0 }}{{index $t 1}}` + if err := runt(tpl, "foo1a"); err != nil { + t.Error(err) + } +} + +func TestPush(t *testing.T) { + // Named `append` in the function map + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ append $t 4 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ append $t 5 | join "-" }}`: "1-2-3-4-5", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ append $t "qux" | join "-" }}`: "foo-bar-baz-qux", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustPush(t *testing.T) { + // Named `append` in the function map + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ mustAppend $t 4 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ mustAppend $t 5 | join "-" }}`: "1-2-3-4-5", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPush $t "qux" | join "-" }}`: "foo-bar-baz-qux", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestChunk(t *testing.T) { + tests := map[string]string{ + `{{ tuple 1 2 3 4 5 6 7 | chunk 3 | len }}`: "3", + `{{ tuple | chunk 3 | len }}`: "0", + `{{ range ( tuple 1 2 3 4 5 6 7 8 9 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|", + `{{ range ( tuple 1 2 3 4 5 6 7 8 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|", + `{{ range ( tuple 1 2 | chunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustChunk(t *testing.T) { + tests := map[string]string{ + `{{ tuple 1 2 3 4 5 6 7 | mustChunk 3 | len }}`: "3", + `{{ tuple | mustChunk 3 | len }}`: "0", + `{{ range ( tuple 1 2 3 4 5 6 7 8 9 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8-9|", + `{{ range ( tuple 1 2 3 4 5 6 7 8 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2-3|4-5-6|7-8|", + `{{ range ( tuple 1 2 | mustChunk 3 ) }}{{. | join "-"}}|{{end}}`: "1-2|", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestPrepend(t *testing.T) { + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ prepend $t 0 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ prepend $t 0 | join "-" }}`: "0-1-2-3-4", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ prepend $t "qux" | join "-" }}`: "qux-foo-bar-baz", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustPrepend(t *testing.T) { + tests := map[string]string{ + `{{ $t := tuple 1 2 3 }}{{ mustPrepend $t 0 | len }}`: "4", + `{{ $t := tuple 1 2 3 4 }}{{ mustPrepend $t 0 | join "-" }}`: "0-1-2-3-4", + `{{ $t := regexSplit "/" "foo/bar/baz" -1 }}{{ mustPrepend $t "qux" | join "-" }}`: "qux-foo-bar-baz", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestFirst(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | first }}`: "1", + `{{ list | first }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | first }}`: "foo", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustFirst(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustFirst }}`: "1", + `{{ list | mustFirst }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | mustFirst }}`: "foo", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestLast(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | last }}`: "3", + `{{ list | last }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | last }}`: "bar", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustLast(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustLast }}`: "3", + `{{ list | mustLast }}`: "", + `{{ regexSplit "/src/" "foo/src/bar" -1 | mustLast }}`: "bar", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestInitial(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | initial | len }}`: "2", + `{{ list 1 2 3 | initial | last }}`: "2", + `{{ list 1 2 3 | initial | first }}`: "1", + `{{ list | initial }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | initial }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustInitial(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustInitial | len }}`: "2", + `{{ list 1 2 3 | mustInitial | last }}`: "2", + `{{ list 1 2 3 | mustInitial | first }}`: "1", + `{{ list | mustInitial }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | mustInitial }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestRest(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | rest | len }}`: "2", + `{{ list 1 2 3 | rest | last }}`: "3", + `{{ list 1 2 3 | rest | first }}`: "2", + `{{ list | rest }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | rest }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustRest(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustRest | len }}`: "2", + `{{ list 1 2 3 | mustRest | last }}`: "3", + `{{ list 1 2 3 | mustRest | first }}`: "2", + `{{ list | mustRest }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | mustRest }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestReverse(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | reverse | first }}`: "3", + `{{ list 1 2 3 | reverse | rest | first }}`: "2", + `{{ list 1 2 3 | reverse | last }}`: "1", + `{{ list 1 2 3 4 | reverse }}`: "[4 3 2 1]", + `{{ list 1 | reverse }}`: "[1]", + `{{ list | reverse }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | reverse }}`: "[baz bar foo]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustReverse(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustReverse | first }}`: "3", + `{{ list 1 2 3 | mustReverse | rest | first }}`: "2", + `{{ list 1 2 3 | mustReverse | last }}`: "1", + `{{ list 1 2 3 4 | mustReverse }}`: "[4 3 2 1]", + `{{ list 1 | mustReverse }}`: "[1]", + `{{ list | mustReverse }}`: "[]", + `{{ regexSplit "/" "foo/bar/baz" -1 | mustReverse }}`: "[baz bar foo]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestCompact(t *testing.T) { + tests := map[string]string{ + `{{ list 1 0 "" "hello" | compact }}`: `[1 hello]`, + `{{ list "" "" | compact }}`: `[]`, + `{{ list | compact }}`: `[]`, + `{{ regexSplit "/" "foo//bar" -1 | compact }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustCompact(t *testing.T) { + tests := map[string]string{ + `{{ list 1 0 "" "hello" | mustCompact }}`: `[1 hello]`, + `{{ list "" "" | mustCompact }}`: `[]`, + `{{ list | mustCompact }}`: `[]`, + `{{ regexSplit "/" "foo//bar" -1 | mustCompact }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestUniq(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 4 | uniq }}`: `[1 2 3 4]`, + `{{ list "a" "b" "c" "d" | uniq }}`: `[a b c d]`, + `{{ list 1 1 1 1 2 2 2 2 | uniq }}`: `[1 2]`, + `{{ list "foo" 1 1 1 1 "foo" "foo" | uniq }}`: `[foo 1]`, + `{{ list | uniq }}`: `[]`, + `{{ regexSplit "/" "foo/foo/bar" -1 | uniq }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustUniq(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 4 | mustUniq }}`: `[1 2 3 4]`, + `{{ list "a" "b" "c" "d" | mustUniq }}`: `[a b c d]`, + `{{ list 1 1 1 1 2 2 2 2 | mustUniq }}`: `[1 2]`, + `{{ list "foo" 1 1 1 1 "foo" "foo" | mustUniq }}`: `[foo 1]`, + `{{ list | mustUniq }}`: `[]`, + `{{ regexSplit "/" "foo/foo/bar" -1 | mustUniq }}`: "[foo bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestWithout(t *testing.T) { + tests := map[string]string{ + `{{ without (list 1 2 3 4) 1 }}`: `[2 3 4]`, + `{{ without (list "a" "b" "c" "d") "a" }}`: `[b c d]`, + `{{ without (list 1 1 1 1 2) 1 }}`: `[2]`, + `{{ without (list) 1 }}`: `[]`, + `{{ without (list 1 2 3) }}`: `[1 2 3]`, + `{{ without list }}`: `[]`, + `{{ without (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustWithout(t *testing.T) { + tests := map[string]string{ + `{{ mustWithout (list 1 2 3 4) 1 }}`: `[2 3 4]`, + `{{ mustWithout (list "a" "b" "c" "d") "a" }}`: `[b c d]`, + `{{ mustWithout (list 1 1 1 1 2) 1 }}`: `[2]`, + `{{ mustWithout (list) 1 }}`: `[]`, + `{{ mustWithout (list 1 2 3) }}`: `[1 2 3]`, + `{{ mustWithout list }}`: `[]`, + `{{ mustWithout (regexSplit "/" "foo/bar/baz" -1 ) "foo" }}`: "[bar baz]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestHas(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | has 1 }}`: `true`, + `{{ list 1 2 3 | has 4 }}`: `false`, + `{{ regexSplit "/" "foo/bar/baz" -1 | has "bar" }}`: `true`, + `{{ has "bar" nil }}`: `false`, + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustHas(t *testing.T) { + tests := map[string]string{ + `{{ list 1 2 3 | mustHas 1 }}`: `true`, + `{{ list 1 2 3 | mustHas 4 }}`: `false`, + `{{ regexSplit "/" "foo/bar/baz" -1 | mustHas "bar" }}`: `true`, + `{{ mustHas "bar" nil }}`: `false`, + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestSlice(t *testing.T) { + tests := map[string]string{ + `{{ slice (list 1 2 3) }}`: "[1 2 3]", + `{{ slice (list 1 2 3) 0 1 }}`: "[1]", + `{{ slice (list 1 2 3) 1 3 }}`: "[2 3]", + `{{ slice (list 1 2 3) 1 }}`: "[2 3]", + `{{ slice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestMustSlice(t *testing.T) { + tests := map[string]string{ + `{{ mustSlice (list 1 2 3) }}`: "[1 2 3]", + `{{ mustSlice (list 1 2 3) 0 1 }}`: "[1]", + `{{ mustSlice (list 1 2 3) 1 3 }}`: "[2 3]", + `{{ mustSlice (list 1 2 3) 1 }}`: "[2 3]", + `{{ mustSlice (regexSplit "/" "foo/bar/baz" -1) 1 2 }}`: "[bar]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} + +func TestConcat(t *testing.T) { + tests := map[string]string{ + `{{ concat (list 1 2 3) }}`: "[1 2 3]", + `{{ concat (list 1 2 3) (list 4 5) }}`: "[1 2 3 4 5]", + `{{ concat (list 1 2 3) (list 4 5) (list) }}`: "[1 2 3 4 5]", + `{{ concat (list 1 2 3) (list 4 5) (list nil) }}`: "[1 2 3 4 5 ]", + `{{ concat (list 1 2 3) (list 4 5) (list ( list "foo" ) ) }}`: "[1 2 3 4 5 [foo]]", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go new file mode 100644 index 00000000..0b23cd21 --- /dev/null +++ b/util/sprig/numeric.go @@ -0,0 +1,228 @@ +package sprig + +import ( + "fmt" + "math" + "reflect" + "strconv" + "strings" +) + +// toFloat64 converts 64-bit floats +func toFloat64(v interface{}) float64 { + if str, ok := v.(string); ok { + iv, err := strconv.ParseFloat(str, 64) + if err != nil { + return 0 + } + return iv + } + + val := reflect.Indirect(reflect.ValueOf(v)) + switch val.Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return float64(val.Int()) + case reflect.Uint8, reflect.Uint16, reflect.Uint32: + return float64(val.Uint()) + case reflect.Uint, reflect.Uint64: + return float64(val.Uint()) + case reflect.Float32, reflect.Float64: + return val.Float() + case reflect.Bool: + if val.Bool() { + return 1 + } + return 0 + default: + return 0 + } +} + +func toInt(v interface{}) int { + // It's not optimal. But I don't want duplicate toInt64 code. + return int(toInt64(v)) +} + +// toInt64 converts integer types to 64-bit integers +func toInt64(v interface{}) int64 { + if str, ok := v.(string); ok { + iv, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return 0 + } + return iv + } + + val := reflect.Indirect(reflect.ValueOf(v)) + switch val.Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return val.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32: + return int64(val.Uint()) + case reflect.Uint, reflect.Uint64: + tv := val.Uint() + if tv <= math.MaxInt64 { + return int64(tv) + } + // TODO: What is the sensible thing to do here? + return math.MaxInt64 + case reflect.Float32, reflect.Float64: + return int64(val.Float()) + case reflect.Bool: + if val.Bool() { + return 1 + } + return 0 + default: + return 0 + } +} + +func max(a interface{}, i ...interface{}) int64 { + aa := toInt64(a) + for _, b := range i { + bb := toInt64(b) + if bb > aa { + aa = bb + } + } + return aa +} + +func maxf(a interface{}, i ...interface{}) float64 { + aa := toFloat64(a) + for _, b := range i { + bb := toFloat64(b) + aa = math.Max(aa, bb) + } + return aa +} + +func min(a interface{}, i ...interface{}) int64 { + aa := toInt64(a) + for _, b := range i { + bb := toInt64(b) + if bb < aa { + aa = bb + } + } + return aa +} + +func minf(a interface{}, i ...interface{}) float64 { + aa := toFloat64(a) + for _, b := range i { + bb := toFloat64(b) + aa = math.Min(aa, bb) + } + return aa +} + +func until(count int) []int { + step := 1 + if count < 0 { + step = -1 + } + return untilStep(0, count, step) +} + +func untilStep(start, stop, step int) []int { + v := []int{} + + if stop < start { + if step >= 0 { + return v + } + for i := start; i > stop; i += step { + v = append(v, i) + } + return v + } + + if step <= 0 { + return v + } + for i := start; i < stop; i += step { + v = append(v, i) + } + return v +} + +func floor(a interface{}) float64 { + aa := toFloat64(a) + return math.Floor(aa) +} + +func ceil(a interface{}) float64 { + aa := toFloat64(a) + return math.Ceil(aa) +} + +func round(a interface{}, p int, rOpt ...float64) float64 { + roundOn := .5 + if len(rOpt) > 0 { + roundOn = rOpt[0] + } + val := toFloat64(a) + places := toFloat64(p) + + var round float64 + pow := math.Pow(10, places) + digit := pow * val + _, div := math.Modf(digit) + if div >= roundOn { + round = math.Ceil(digit) + } else { + round = math.Floor(digit) + } + return round / pow +} + +// converts unix octal to decimal +func toDecimal(v interface{}) int64 { + result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) + if err != nil { + return 0 + } + return result +} + +func seq(params ...int) string { + increment := 1 + switch len(params) { + case 0: + return "" + case 1: + start := 1 + end := params[0] + if end < start { + increment = -1 + } + return intArrayToString(untilStep(start, end+increment, increment), " ") + case 3: + start := params[0] + end := params[2] + step := params[1] + if end < start { + increment = -1 + if step > 0 { + return "" + } + } + return intArrayToString(untilStep(start, end+increment, step), " ") + case 2: + start := params[0] + end := params[1] + step := 1 + if end < start { + step = -1 + } + return intArrayToString(untilStep(start, end+step, step), " ") + default: + return "" + } +} + +func intArrayToString(slice []int, delimeter string) string { + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") +} diff --git a/util/sprig/numeric_test.go b/util/sprig/numeric_test.go new file mode 100644 index 00000000..94e8a6d4 --- /dev/null +++ b/util/sprig/numeric_test.go @@ -0,0 +1,307 @@ +package sprig + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "strconv" + "testing" +) + +func TestUntil(t *testing.T) { + tests := map[string]string{ + `{{range $i, $e := until 5}}{{$i}}{{$e}}{{end}}`: "0011223344", + `{{range $i, $e := until -5}}{{$i}}{{$e}} {{end}}`: "00 1-1 2-2 3-3 4-4 ", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} +func TestUntilStep(t *testing.T) { + tests := map[string]string{ + `{{range $i, $e := untilStep 0 5 1}}{{$i}}{{$e}}{{end}}`: "0011223344", + `{{range $i, $e := untilStep 3 6 1}}{{$i}}{{$e}}{{end}}`: "031425", + `{{range $i, $e := untilStep 0 -10 -2}}{{$i}}{{$e}} {{end}}`: "00 1-2 2-4 3-6 4-8 ", + `{{range $i, $e := untilStep 3 0 1}}{{$i}}{{$e}}{{end}}`: "", + `{{range $i, $e := untilStep 3 99 0}}{{$i}}{{$e}}{{end}}`: "", + `{{range $i, $e := untilStep 3 99 -1}}{{$i}}{{$e}}{{end}}`: "", + `{{range $i, $e := untilStep 3 0 0}}{{$i}}{{$e}}{{end}}`: "", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } + +} +func TestBiggest(t *testing.T) { + tpl := `{{ biggest 1 2 3 345 5 6 7}}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } + + tpl = `{{ max 345}}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} +func TestMaxf(t *testing.T) { + tpl := `{{ maxf 1 2 3 345.7 5 6 7}}` + if err := runt(tpl, `345.7`); err != nil { + t.Error(err) + } + + tpl = `{{ max 345 }}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} +func TestMin(t *testing.T) { + tpl := `{{ min 1 2 3 345 5 6 7}}` + if err := runt(tpl, `1`); err != nil { + t.Error(err) + } + + tpl = `{{ min 345}}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} + +func TestMinf(t *testing.T) { + tpl := `{{ minf 1.4 2 3 345.6 5 6 7}}` + if err := runt(tpl, `1.4`); err != nil { + t.Error(err) + } + + tpl = `{{ minf 345 }}` + if err := runt(tpl, `345`); err != nil { + t.Error(err) + } +} + +func TestToFloat64(t *testing.T) { + target := float64(102) + if target != toFloat64(int8(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int32(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int16(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(int64(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64("102") { + t.Errorf("Expected 102") + } + if 0 != toFloat64("frankie") { + t.Errorf("Expected 0") + } + if target != toFloat64(uint16(102)) { + t.Errorf("Expected 102") + } + if target != toFloat64(uint64(102)) { + t.Errorf("Expected 102") + } + if 102.1234 != toFloat64(float64(102.1234)) { + t.Errorf("Expected 102.1234") + } + if 1 != toFloat64(true) { + t.Errorf("Expected 102") + } +} +func TestToInt64(t *testing.T) { + target := int64(102) + if target != toInt64(int8(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int32(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int16(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(int64(102)) { + t.Errorf("Expected 102") + } + if target != toInt64("102") { + t.Errorf("Expected 102") + } + if 0 != toInt64("frankie") { + t.Errorf("Expected 0") + } + if target != toInt64(uint16(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(uint64(102)) { + t.Errorf("Expected 102") + } + if target != toInt64(float64(102.1234)) { + t.Errorf("Expected 102") + } + if 1 != toInt64(true) { + t.Errorf("Expected 102") + } +} + +func TestToInt(t *testing.T) { + target := int(102) + if target != toInt(int8(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int32(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int16(102)) { + t.Errorf("Expected 102") + } + if target != toInt(int64(102)) { + t.Errorf("Expected 102") + } + if target != toInt("102") { + t.Errorf("Expected 102") + } + if 0 != toInt("frankie") { + t.Errorf("Expected 0") + } + if target != toInt(uint16(102)) { + t.Errorf("Expected 102") + } + if target != toInt(uint64(102)) { + t.Errorf("Expected 102") + } + if target != toInt(float64(102.1234)) { + t.Errorf("Expected 102") + } + if 1 != toInt(true) { + t.Errorf("Expected 102") + } +} + +func TestToDecimal(t *testing.T) { + tests := map[interface{}]int64{ + "777": 511, + 777: 511, + 770: 504, + 755: 493, + } + + for input, expectedResult := range tests { + result := toDecimal(input) + if result != expectedResult { + t.Errorf("Expected %v but got %v", expectedResult, result) + } + } +} + +func TestAdd1(t *testing.T) { + tpl := `{{ 3 | add1 }}` + if err := runt(tpl, `4`); err != nil { + t.Error(err) + } +} + +func TestAdd(t *testing.T) { + tpl := `{{ 3 | add 1 2}}` + if err := runt(tpl, `6`); err != nil { + t.Error(err) + } +} + +func TestDiv(t *testing.T) { + tpl := `{{ 4 | div 5 }}` + if err := runt(tpl, `1`); err != nil { + t.Error(err) + } +} + +func TestMul(t *testing.T) { + tpl := `{{ 1 | mul "2" 3 "4"}}` + if err := runt(tpl, `24`); err != nil { + t.Error(err) + } +} + +func TestSub(t *testing.T) { + tpl := `{{ 3 | sub 14 }}` + if err := runt(tpl, `11`); err != nil { + t.Error(err) + } +} + +func TestCeil(t *testing.T) { + assert.Equal(t, 123.0, ceil(123)) + assert.Equal(t, 123.0, ceil("123")) + assert.Equal(t, 124.0, ceil(123.01)) + assert.Equal(t, 124.0, ceil("123.01")) +} + +func TestFloor(t *testing.T) { + assert.Equal(t, 123.0, floor(123)) + assert.Equal(t, 123.0, floor("123")) + assert.Equal(t, 123.0, floor(123.9999)) + assert.Equal(t, 123.0, floor("123.9999")) +} + +func TestRound(t *testing.T) { + assert.Equal(t, 123.556, round(123.5555, 3)) + assert.Equal(t, 123.556, round("123.55555", 3)) + assert.Equal(t, 124.0, round(123.500001, 0)) + assert.Equal(t, 123.0, round(123.49999999, 0)) + assert.Equal(t, 123.23, round(123.2329999, 2, .3)) + assert.Equal(t, 123.24, round(123.233, 2, .3)) +} + +func TestRandomInt(t *testing.T) { + var tests = []struct { + min int + max int + }{ + {10, 11}, + {10, 13}, + {0, 1}, + {5, 50}, + } + for _, v := range tests { + x, _ := runRaw(fmt.Sprintf(`{{ randInt %d %d }}`, v.min, v.max), nil) + r, err := strconv.Atoi(x) + assert.NoError(t, err) + assert.True(t, func(min, max, r int) bool { + return r >= v.min && r < v.max + }(v.min, v.max, r)) + } +} + +func TestSeq(t *testing.T) { + tests := map[string]string{ + `{{seq 0 1 3}}`: "0 1 2 3", + `{{seq 0 3 10}}`: "0 3 6 9", + `{{seq 3 3 2}}`: "", + `{{seq 3 -3 2}}`: "3", + `{{seq}}`: "", + `{{seq 0 4}}`: "0 1 2 3 4", + `{{seq 5}}`: "1 2 3 4 5", + `{{seq -5}}`: "1 0 -1 -2 -3 -4 -5", + `{{seq 0}}`: "1 0", + `{{seq 0 1 2 3}}`: "", + `{{seq 0 -4}}`: "0 -1 -2 -3 -4", + } + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} diff --git a/util/sprig/reflect.go b/util/sprig/reflect.go new file mode 100644 index 00000000..8a65c132 --- /dev/null +++ b/util/sprig/reflect.go @@ -0,0 +1,28 @@ +package sprig + +import ( + "fmt" + "reflect" +) + +// typeIs returns true if the src is the type named in target. +func typeIs(target string, src interface{}) bool { + return target == typeOf(src) +} + +func typeIsLike(target string, src interface{}) bool { + t := typeOf(src) + return target == t || "*"+target == t +} + +func typeOf(src interface{}) string { + return fmt.Sprintf("%T", src) +} + +func kindIs(target string, src interface{}) bool { + return target == kindOf(src) +} + +func kindOf(src interface{}) string { + return reflect.ValueOf(src).Kind().String() +} diff --git a/util/sprig/reflect_test.go b/util/sprig/reflect_test.go new file mode 100644 index 00000000..f102907e --- /dev/null +++ b/util/sprig/reflect_test.go @@ -0,0 +1,73 @@ +package sprig + +import ( + "testing" +) + +type fixtureTO struct { + Name, Value string +} + +func TestTypeOf(t *testing.T) { + f := &fixtureTO{"hello", "world"} + tpl := `{{typeOf .}}` + if err := runtv(tpl, "*sprig.fixtureTO", f); err != nil { + t.Error(err) + } +} + +func TestKindOf(t *testing.T) { + tpl := `{{kindOf .}}` + + f := fixtureTO{"hello", "world"} + if err := runtv(tpl, "struct", f); err != nil { + t.Error(err) + } + + f2 := []string{"hello"} + if err := runtv(tpl, "slice", f2); err != nil { + t.Error(err) + } + + var f3 *fixtureTO + if err := runtv(tpl, "ptr", f3); err != nil { + t.Error(err) + } +} + +func TestTypeIs(t *testing.T) { + f := &fixtureTO{"hello", "world"} + tpl := `{{if typeIs "*sprig.fixtureTO" .}}t{{else}}f{{end}}` + if err := runtv(tpl, "t", f); err != nil { + t.Error(err) + } + + f2 := "hello" + if err := runtv(tpl, "f", f2); err != nil { + t.Error(err) + } +} +func TestTypeIsLike(t *testing.T) { + f := "foo" + tpl := `{{if typeIsLike "string" .}}t{{else}}f{{end}}` + if err := runtv(tpl, "t", f); err != nil { + t.Error(err) + } + + // Now make a pointer. Should still match. + f2 := &f + if err := runtv(tpl, "t", f2); err != nil { + t.Error(err) + } +} +func TestKindIs(t *testing.T) { + f := &fixtureTO{"hello", "world"} + tpl := `{{if kindIs "ptr" .}}t{{else}}f{{end}}` + if err := runtv(tpl, "t", f); err != nil { + t.Error(err) + } + f2 := "hello" + if err := runtv(tpl, "f", f2); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/regex.go b/util/sprig/regex.go new file mode 100644 index 00000000..fab55101 --- /dev/null +++ b/util/sprig/regex.go @@ -0,0 +1,83 @@ +package sprig + +import ( + "regexp" +) + +func regexMatch(regex string, s string) bool { + match, _ := regexp.MatchString(regex, s) + return match +} + +func mustRegexMatch(regex string, s string) (bool, error) { + return regexp.MatchString(regex, s) +} + +func regexFindAll(regex string, s string, n int) []string { + r := regexp.MustCompile(regex) + return r.FindAllString(s, n) +} + +func mustRegexFindAll(regex string, s string, n int) ([]string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return []string{}, err + } + return r.FindAllString(s, n), nil +} + +func regexFind(regex string, s string) string { + r := regexp.MustCompile(regex) + return r.FindString(s) +} + +func mustRegexFind(regex string, s string) (string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return "", err + } + return r.FindString(s), nil +} + +func regexReplaceAll(regex string, s string, repl string) string { + r := regexp.MustCompile(regex) + return r.ReplaceAllString(s, repl) +} + +func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return "", err + } + return r.ReplaceAllString(s, repl), nil +} + +func regexReplaceAllLiteral(regex string, s string, repl string) string { + r := regexp.MustCompile(regex) + return r.ReplaceAllLiteralString(s, repl) +} + +func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return "", err + } + return r.ReplaceAllLiteralString(s, repl), nil +} + +func regexSplit(regex string, s string, n int) []string { + r := regexp.MustCompile(regex) + return r.Split(s, n) +} + +func mustRegexSplit(regex string, s string, n int) ([]string, error) { + r, err := regexp.Compile(regex) + if err != nil { + return []string{}, err + } + return r.Split(s, n), nil +} + +func regexQuoteMeta(s string) string { + return regexp.QuoteMeta(s) +} diff --git a/util/sprig/regex_test.go b/util/sprig/regex_test.go new file mode 100644 index 00000000..60aafc29 --- /dev/null +++ b/util/sprig/regex_test.go @@ -0,0 +1,203 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRegexMatch(t *testing.T) { + regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + + assert.True(t, regexMatch(regex, "test@acme.com")) + assert.True(t, regexMatch(regex, "Test@Acme.Com")) + assert.False(t, regexMatch(regex, "test")) + assert.False(t, regexMatch(regex, "test.com")) + assert.False(t, regexMatch(regex, "test@acme")) +} + +func TestMustRegexMatch(t *testing.T) { + regex := "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + + o, err := mustRegexMatch(regex, "test@acme.com") + assert.True(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "Test@Acme.Com") + assert.True(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "test") + assert.False(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "test.com") + assert.False(t, o) + assert.Nil(t, err) + + o, err = mustRegexMatch(regex, "test@acme") + assert.False(t, o) + assert.Nil(t, err) +} + +func TestRegexFindAll(t *testing.T) { + regex := "a{2}" + assert.Equal(t, 1, len(regexFindAll(regex, "aa", -1))) + assert.Equal(t, 1, len(regexFindAll(regex, "aaaaaaaa", 1))) + assert.Equal(t, 2, len(regexFindAll(regex, "aaaa", -1))) + assert.Equal(t, 0, len(regexFindAll(regex, "none", -1))) +} + +func TestMustRegexFindAll(t *testing.T) { + type args struct { + regex, s string + n int + } + cases := []struct { + expected int + args args + }{ + {1, args{"a{2}", "aa", -1}}, + {1, args{"a{2}", "aaaaaaaa", 1}}, + {2, args{"a{2}", "aaaa", -1}}, + {0, args{"a{2}", "none", -1}}, + } + + for _, c := range cases { + res, err := mustRegexFindAll(c.args.regex, c.args.s, c.args.n) + if err != nil { + t.Errorf("regexFindAll test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, len(res), "case %#v", c.args) + } +} + +func TestRegexFindl(t *testing.T) { + regex := "fo.?" + assert.Equal(t, "foo", regexFind(regex, "foorbar")) + assert.Equal(t, "foo", regexFind(regex, "foo foe fome")) + assert.Equal(t, "", regexFind(regex, "none")) +} + +func TestMustRegexFindl(t *testing.T) { + type args struct{ regex, s string } + cases := []struct { + expected string + args args + }{ + {"foo", args{"fo.?", "foorbar"}}, + {"foo", args{"fo.?", "foo foe fome"}}, + {"", args{"fo.?", "none"}}, + } + + for _, c := range cases { + res, err := mustRegexFind(c.args.regex, c.args.s) + if err != nil { + t.Errorf("regexFind test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, res, "case %#v", c.args) + } +} + +func TestRegexReplaceAll(t *testing.T) { + regex := "a(x*)b" + assert.Equal(t, "-T-T-", regexReplaceAll(regex, "-ab-axxb-", "T")) + assert.Equal(t, "--xx-", regexReplaceAll(regex, "-ab-axxb-", "$1")) + assert.Equal(t, "---", regexReplaceAll(regex, "-ab-axxb-", "$1W")) + assert.Equal(t, "-W-xxW-", regexReplaceAll(regex, "-ab-axxb-", "${1}W")) +} + +func TestMustRegexReplaceAll(t *testing.T) { + type args struct{ regex, s, repl string } + cases := []struct { + expected string + args args + }{ + {"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}}, + {"--xx-", args{"a(x*)b", "-ab-axxb-", "$1"}}, + {"---", args{"a(x*)b", "-ab-axxb-", "$1W"}}, + {"-W-xxW-", args{"a(x*)b", "-ab-axxb-", "${1}W"}}, + } + + for _, c := range cases { + res, err := mustRegexReplaceAll(c.args.regex, c.args.s, c.args.repl) + if err != nil { + t.Errorf("regexReplaceAll test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, res, "case %#v", c.args) + } +} + +func TestRegexReplaceAllLiteral(t *testing.T) { + regex := "a(x*)b" + assert.Equal(t, "-T-T-", regexReplaceAllLiteral(regex, "-ab-axxb-", "T")) + assert.Equal(t, "-$1-$1-", regexReplaceAllLiteral(regex, "-ab-axxb-", "$1")) + assert.Equal(t, "-${1}-${1}-", regexReplaceAllLiteral(regex, "-ab-axxb-", "${1}")) +} + +func TestMustRegexReplaceAllLiteral(t *testing.T) { + type args struct{ regex, s, repl string } + cases := []struct { + expected string + args args + }{ + {"-T-T-", args{"a(x*)b", "-ab-axxb-", "T"}}, + {"-$1-$1-", args{"a(x*)b", "-ab-axxb-", "$1"}}, + {"-${1}-${1}-", args{"a(x*)b", "-ab-axxb-", "${1}"}}, + } + + for _, c := range cases { + res, err := mustRegexReplaceAllLiteral(c.args.regex, c.args.s, c.args.repl) + if err != nil { + t.Errorf("regexReplaceAllLiteral test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, res, "case %#v", c.args) + } +} + +func TestRegexSplit(t *testing.T) { + regex := "a" + assert.Equal(t, 4, len(regexSplit(regex, "banana", -1))) + assert.Equal(t, 0, len(regexSplit(regex, "banana", 0))) + assert.Equal(t, 1, len(regexSplit(regex, "banana", 1))) + assert.Equal(t, 2, len(regexSplit(regex, "banana", 2))) + + regex = "z+" + assert.Equal(t, 2, len(regexSplit(regex, "pizza", -1))) + assert.Equal(t, 0, len(regexSplit(regex, "pizza", 0))) + assert.Equal(t, 1, len(regexSplit(regex, "pizza", 1))) + assert.Equal(t, 2, len(regexSplit(regex, "pizza", 2))) +} + +func TestMustRegexSplit(t *testing.T) { + type args struct { + regex, s string + n int + } + cases := []struct { + expected int + args args + }{ + {4, args{"a", "banana", -1}}, + {0, args{"a", "banana", 0}}, + {1, args{"a", "banana", 1}}, + {2, args{"a", "banana", 2}}, + {2, args{"z+", "pizza", -1}}, + {0, args{"z+", "pizza", 0}}, + {1, args{"z+", "pizza", 1}}, + {2, args{"z+", "pizza", 2}}, + } + + for _, c := range cases { + res, err := mustRegexSplit(c.args.regex, c.args.s, c.args.n) + if err != nil { + t.Errorf("regexSplit test case %v failed with err %s", c, err) + } + assert.Equal(t, c.expected, len(res), "case %#v", c.args) + } +} + +func TestRegexQuoteMeta(t *testing.T) { + assert.Equal(t, "1\\.2\\.3", regexQuoteMeta("1.2.3")) + assert.Equal(t, "pretzel", regexQuoteMeta("pretzel")) +} diff --git a/util/sprig/strings.go b/util/sprig/strings.go new file mode 100644 index 00000000..3c62d6b6 --- /dev/null +++ b/util/sprig/strings.go @@ -0,0 +1,189 @@ +package sprig + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" +) + +func base64encode(v string) string { + return base64.StdEncoding.EncodeToString([]byte(v)) +} + +func base64decode(v string) string { + data, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return err.Error() + } + return string(data) +} + +func base32encode(v string) string { + return base32.StdEncoding.EncodeToString([]byte(v)) +} + +func base32decode(v string) string { + data, err := base32.StdEncoding.DecodeString(v) + if err != nil { + return err.Error() + } + return string(data) +} + +func quote(str ...interface{}) string { + out := make([]string, 0, len(str)) + for _, s := range str { + if s != nil { + out = append(out, fmt.Sprintf("%q", strval(s))) + } + } + return strings.Join(out, " ") +} + +func squote(str ...interface{}) string { + out := make([]string, 0, len(str)) + for _, s := range str { + if s != nil { + out = append(out, fmt.Sprintf("'%v'", s)) + } + } + return strings.Join(out, " ") +} + +func cat(v ...interface{}) string { + v = removeNilElements(v) + r := strings.TrimSpace(strings.Repeat("%v ", len(v))) + return fmt.Sprintf(r, v...) +} + +func indent(spaces int, v string) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.Replace(v, "\n", "\n"+pad, -1) +} + +func nindent(spaces int, v string) string { + return "\n" + indent(spaces, v) +} + +func replace(old, new, src string) string { + return strings.Replace(src, old, new, -1) +} + +func plural(one, many string, count int) string { + if count == 1 { + return one + } + return many +} + +func strslice(v interface{}) []string { + switch v := v.(type) { + case []string: + return v + case []interface{}: + b := make([]string, 0, len(v)) + for _, s := range v { + if s != nil { + b = append(b, strval(s)) + } + } + return b + default: + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Array, reflect.Slice: + l := val.Len() + b := make([]string, 0, l) + for i := 0; i < l; i++ { + value := val.Index(i).Interface() + if value != nil { + b = append(b, strval(value)) + } + } + return b + default: + if v == nil { + return []string{} + } + + return []string{strval(v)} + } + } +} + +func removeNilElements(v []interface{}) []interface{} { + newSlice := make([]interface{}, 0, len(v)) + for _, i := range v { + if i != nil { + newSlice = append(newSlice, i) + } + } + return newSlice +} + +func strval(v interface{}) string { + switch v := v.(type) { + case string: + return v + case []byte: + return string(v) + case error: + return v.Error() + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} + +func trunc(c int, s string) string { + if c < 0 && len(s)+c > 0 { + return s[len(s)+c:] + } + if c >= 0 && len(s) > c { + return s[:c] + } + return s +} + +func join(sep string, v interface{}) string { + return strings.Join(strslice(v), sep) +} + +func split(sep, orig string) map[string]string { + parts := strings.Split(orig, sep) + res := make(map[string]string, len(parts)) + for i, v := range parts { + res["_"+strconv.Itoa(i)] = v + } + return res +} + +func splitn(sep string, n int, orig string) map[string]string { + parts := strings.SplitN(orig, sep, n) + res := make(map[string]string, len(parts)) + for i, v := range parts { + res["_"+strconv.Itoa(i)] = v + } + return res +} + +// substring creates a substring of the given string. +// +// If start is < 0, this calls string[:end]. +// +// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] +// +// Otherwise, this calls string[start, end]. +func substring(start, end int, s string) string { + if start < 0 { + return s[:end] + } + if end < 0 || end > len(s) { + return s[start:] + } + return s[start:end] +} diff --git a/util/sprig/strings_test.go b/util/sprig/strings_test.go new file mode 100644 index 00000000..38c96c4e --- /dev/null +++ b/util/sprig/strings_test.go @@ -0,0 +1,233 @@ +package sprig + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubstr(t *testing.T) { + tpl := `{{"fooo" | substr 0 3 }}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestSubstr_shorterString(t *testing.T) { + tpl := `{{"foo" | substr 0 10 }}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestTrunc(t *testing.T) { + tpl := `{{ "foooooo" | trunc 3 }}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } + tpl = `{{ "baaaaaar" | trunc -3 }}` + if err := runt(tpl, "aar"); err != nil { + t.Error(err) + } + tpl = `{{ "baaaaaar" | trunc -999 }}` + if err := runt(tpl, "baaaaaar"); err != nil { + t.Error(err) + } + tpl = `{{ "baaaaaz" | trunc 0 }}` + if err := runt(tpl, ""); err != nil { + t.Error(err) + } +} + +func TestQuote(t *testing.T) { + tpl := `{{quote "a" "b" "c"}}` + if err := runt(tpl, `"a" "b" "c"`); err != nil { + t.Error(err) + } + tpl = `{{quote "\"a\"" "b" "c"}}` + if err := runt(tpl, `"\"a\"" "b" "c"`); err != nil { + t.Error(err) + } + tpl = `{{quote 1 2 3 }}` + if err := runt(tpl, `"1" "2" "3"`); err != nil { + t.Error(err) + } + tpl = `{{ .value | quote }}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, ``, values); err != nil { + t.Error(err) + } +} +func TestSquote(t *testing.T) { + tpl := `{{squote "a" "b" "c"}}` + if err := runt(tpl, `'a' 'b' 'c'`); err != nil { + t.Error(err) + } + tpl = `{{squote 1 2 3 }}` + if err := runt(tpl, `'1' '2' '3'`); err != nil { + t.Error(err) + } + tpl = `{{ .value | squote }}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, ``, values); err != nil { + t.Error(err) + } +} + +func TestContains(t *testing.T) { + // Mainly, we're just verifying the paramater order swap. + tests := []string{ + `{{if contains "cat" "fair catch"}}1{{end}}`, + `{{if hasPrefix "cat" "catch"}}1{{end}}`, + `{{if hasSuffix "cat" "ducat"}}1{{end}}`, + } + for _, tt := range tests { + if err := runt(tt, "1"); err != nil { + t.Error(err) + } + } +} + +func TestTrim(t *testing.T) { + tests := []string{ + `{{trim " 5.00 "}}`, + `{{trimAll "$" "$5.00$"}}`, + `{{trimPrefix "$" "$5.00"}}`, + `{{trimSuffix "$" "5.00$"}}`, + } + for _, tt := range tests { + if err := runt(tt, "5.00"); err != nil { + t.Error(err) + } + } +} + +func TestSplit(t *testing.T) { + tpl := `{{$v := "foo$bar$baz" | split "$"}}{{$v._0}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestSplitn(t *testing.T) { + tpl := `{{$v := "foo$bar$baz" | splitn "$" 2}}{{$v._0}}` + if err := runt(tpl, "foo"); err != nil { + t.Error(err) + } +} + +func TestToString(t *testing.T) { + tpl := `{{ toString 1 | kindOf }}` + assert.NoError(t, runt(tpl, "string")) +} + +func TestToStrings(t *testing.T) { + tpl := `{{ $s := list 1 2 3 | toStrings }}{{ index $s 1 | kindOf }}` + assert.NoError(t, runt(tpl, "string")) + tpl = `{{ list 1 .value 2 | toStrings }}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, `[1 2]`, values); err != nil { + t.Error(err) + } +} + +func TestJoin(t *testing.T) { + assert.NoError(t, runt(`{{ tuple "a" "b" "c" | join "-" }}`, "a-b-c")) + assert.NoError(t, runt(`{{ tuple 1 2 3 | join "-" }}`, "1-2-3")) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]interface{}{"V": []string{"a", "b", "c"}})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]interface{}{"V": "abc"})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]interface{}{"V": []int{1, 2, 3}})) + assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]interface{}{"value": []interface{}{"1", nil, "2"}})) +} + +func TestSortAlpha(t *testing.T) { + // Named `append` in the function map + tests := map[string]string{ + `{{ list "c" "a" "b" | sortAlpha | join "" }}`: "abc", + `{{ list 2 1 4 3 | sortAlpha | join "" }}`: "1234", + } + for tpl, expect := range tests { + assert.NoError(t, runt(tpl, expect)) + } +} +func TestBase64EncodeDecode(t *testing.T) { + magicWord := "coffee" + expect := base64.StdEncoding.EncodeToString([]byte(magicWord)) + + if expect == magicWord { + t.Fatal("Encoder doesn't work.") + } + + tpl := `{{b64enc "coffee"}}` + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + tpl = fmt.Sprintf("{{b64dec %q}}", expect) + if err := runt(tpl, magicWord); err != nil { + t.Error(err) + } +} +func TestBase32EncodeDecode(t *testing.T) { + magicWord := "coffee" + expect := base32.StdEncoding.EncodeToString([]byte(magicWord)) + + if expect == magicWord { + t.Fatal("Encoder doesn't work.") + } + + tpl := `{{b32enc "coffee"}}` + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + tpl = fmt.Sprintf("{{b32dec %q}}", expect) + if err := runt(tpl, magicWord); err != nil { + t.Error(err) + } +} + +func TestCat(t *testing.T) { + tpl := `{{$b := "b"}}{{"c" | cat "a" $b}}` + if err := runt(tpl, "a b c"); err != nil { + t.Error(err) + } + tpl = `{{ .value | cat "a" "b"}}` + values := map[string]interface{}{"value": nil} + if err := runtv(tpl, "a b", values); err != nil { + t.Error(err) + } +} + +func TestIndent(t *testing.T) { + tpl := `{{indent 4 "a\nb\nc"}}` + if err := runt(tpl, " a\n b\n c"); err != nil { + t.Error(err) + } +} + +func TestNindent(t *testing.T) { + tpl := `{{nindent 4 "a\nb\nc"}}` + if err := runt(tpl, "\n a\n b\n c"); err != nil { + t.Error(err) + } +} + +func TestReplace(t *testing.T) { + tpl := `{{"I Am Henry VIII" | replace " " "-"}}` + if err := runt(tpl, "I-Am-Henry-VIII"); err != nil { + t.Error(err) + } +} + +func TestPlural(t *testing.T) { + tpl := `{{$num := len "two"}}{{$num}} {{$num | plural "1 char" "chars"}}` + if err := runt(tpl, "3 chars"); err != nil { + t.Error(err) + } + tpl = `{{len "t" | plural "cheese" "%d chars"}}` + if err := runt(tpl, "cheese"); err != nil { + t.Error(err) + } +} diff --git a/util/sprig/url.go b/util/sprig/url.go new file mode 100644 index 00000000..b8e120e1 --- /dev/null +++ b/util/sprig/url.go @@ -0,0 +1,66 @@ +package sprig + +import ( + "fmt" + "net/url" + "reflect" +) + +func dictGetOrEmpty(dict map[string]interface{}, key string) string { + value, ok := dict[key] + if !ok { + return "" + } + tp := reflect.TypeOf(value).Kind() + if tp != reflect.String { + panic(fmt.Sprintf("unable to parse %s key, must be of type string, but %s found", key, tp.String())) + } + return reflect.ValueOf(value).String() +} + +// parses given URL to return dict object +func urlParse(v string) map[string]interface{} { + dict := map[string]interface{}{} + parsedURL, err := url.Parse(v) + if err != nil { + panic(fmt.Sprintf("unable to parse url: %s", err)) + } + dict["scheme"] = parsedURL.Scheme + dict["host"] = parsedURL.Host + dict["hostname"] = parsedURL.Hostname() + dict["path"] = parsedURL.Path + dict["query"] = parsedURL.RawQuery + dict["opaque"] = parsedURL.Opaque + dict["fragment"] = parsedURL.Fragment + if parsedURL.User != nil { + dict["userinfo"] = parsedURL.User.String() + } else { + dict["userinfo"] = "" + } + + return dict +} + +// join given dict to URL string +func urlJoin(d map[string]interface{}) string { + resURL := url.URL{ + Scheme: dictGetOrEmpty(d, "scheme"), + Host: dictGetOrEmpty(d, "host"), + Path: dictGetOrEmpty(d, "path"), + RawQuery: dictGetOrEmpty(d, "query"), + Opaque: dictGetOrEmpty(d, "opaque"), + Fragment: dictGetOrEmpty(d, "fragment"), + } + userinfo := dictGetOrEmpty(d, "userinfo") + var user *url.Userinfo + if userinfo != "" { + tempURL, err := url.Parse(fmt.Sprintf("proto://%s@host", userinfo)) + if err != nil { + panic(fmt.Sprintf("unable to parse userinfo in dict: %s", err)) + } + user = tempURL.User + } + + resURL.User = user + return resURL.String() +} diff --git a/util/sprig/url_test.go b/util/sprig/url_test.go new file mode 100644 index 00000000..f9c00b17 --- /dev/null +++ b/util/sprig/url_test.go @@ -0,0 +1,87 @@ +package sprig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var urlTests = map[string]map[string]interface{}{ + "proto://auth@host:80/path?query#fragment": { + "fragment": "fragment", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/path", + "query": "query", + "scheme": "proto", + "userinfo": "auth", + }, + "proto://host:80/path": { + "fragment": "", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/path", + "query": "", + "scheme": "proto", + "userinfo": "", + }, + "something": { + "fragment": "", + "host": "", + "hostname": "", + "opaque": "", + "path": "something", + "query": "", + "scheme": "", + "userinfo": "", + }, + "proto://user:passwor%20d@host:80/path": { + "fragment": "", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/path", + "query": "", + "scheme": "proto", + "userinfo": "user:passwor%20d", + }, + "proto://host:80/pa%20th?key=val%20ue": { + "fragment": "", + "host": "host:80", + "hostname": "host", + "opaque": "", + "path": "/pa th", + "query": "key=val%20ue", + "scheme": "proto", + "userinfo": "", + }, +} + +func TestUrlParse(t *testing.T) { + // testing that function is exported and working properly + assert.NoError(t, runt( + `{{ index ( urlParse "proto://auth@host:80/path?query#fragment" ) "host" }}`, + "host:80")) + + // testing scenarios + for url, expected := range urlTests { + assert.EqualValues(t, expected, urlParse(url)) + } +} + +func TestUrlJoin(t *testing.T) { + tests := map[string]string{ + `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "proto") }}`: "proto://host:80/path?query#fragment", + `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "scheme" "proto" "userinfo" "ASDJKJSD") }}`: "proto://ASDJKJSD@host:80/path#fragment", + } + for tpl, expected := range tests { + assert.NoError(t, runt(tpl, expected)) + } + + for expected, urlMap := range urlTests { + assert.EqualValues(t, expected, urlJoin(urlMap)) + } + +} From 650f492d7dd2b3196474cc3ce334767ff63a367f Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Mon, 7 Jul 2025 22:47:41 -0600 Subject: [PATCH 165/378] make tests happy --- docs/releases.md | 2 +- docs/sprig/os.md | 24 ------ docs/sprig/semver.md | 151 ------------------------------------- go.mod | 4 +- server/server.go | 6 +- util/sprig/defaults.go | 6 -- util/sprig/functions.go | 4 +- util/sprig/list.go | 26 +++---- util/sprig/numeric_test.go | 14 ++-- 9 files changed, 27 insertions(+), 210 deletions(-) delete mode 100644 docs/sprig/os.md delete mode 100644 docs/sprig/semver.md diff --git a/docs/releases.md b/docs/releases.md index ed728fcb..5e18edab 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1440,7 +1440,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) * Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) -* You can now use [Slim-Sprig](https://github.com/go-task/slim-sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) +* You can now use a subset of [Sprig](https://github.com/Masterminds/sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) **Languages** diff --git a/docs/sprig/os.md b/docs/sprig/os.md deleted file mode 100644 index e6120c03..00000000 --- a/docs/sprig/os.md +++ /dev/null @@ -1,24 +0,0 @@ -# OS Functions - -_WARNING:_ These functions can lead to information leakage if not used -appropriately. - -_WARNING:_ Some notable implementations of Sprig (such as -[Kubernetes Helm](http://helm.sh)) _do not provide these functions for security -reasons_. - -## env - -The `env` function reads an environment variable: - -``` -env "HOME" -``` - -## expandenv - -To substitute environment variables in a string, use `expandenv`: - -``` -expandenv "Your path is set to $PATH" -``` diff --git a/docs/sprig/semver.md b/docs/sprig/semver.md deleted file mode 100644 index f049613d..00000000 --- a/docs/sprig/semver.md +++ /dev/null @@ -1,151 +0,0 @@ -# Semantic Version Functions - -Some version schemes are easily parseable and comparable. Sprig provides functions -for working with [SemVer 2](http://semver.org) versions. - -## semver - -The `semver` function parses a string into a Semantic Version: - -``` -$version := semver "1.2.3-alpha.1+123" -``` - -_If the parser fails, it will cause template execution to halt with an error._ - -At this point, `$version` is a pointer to a `Version` object with the following -properties: - -- `$version.Major`: The major number (`1` above) -- `$version.Minor`: The minor number (`2` above) -- `$version.Patch`: The patch number (`3` above) -- `$version.Prerelease`: The prerelease (`alpha.1` above) -- `$version.Metadata`: The build metadata (`123` above) -- `$version.Original`: The original version as a string - -Additionally, you can compare a `Version` to another `version` using the `Compare` -function: - -``` -semver "1.4.3" | (semver "1.2.3").Compare -``` - -The above will return `-1`. - -The return values are: - -- `-1` if the given semver is greater than the semver whose `Compare` method was called -- `1` if the version who's `Compare` function was called is greater. -- `0` if they are the same version - -(Note that in SemVer, the `Metadata` field is not compared during version -comparison operations.) - -## semverCompare - -A more robust comparison function is provided as `semverCompare`. It returns `true` if -the constraint matches, or `false` if it does not match. This version supports version ranges: - -- `semverCompare "1.2.3" "1.2.3"` checks for an exact match -- `semverCompare "^1.2.0" "1.2.3"` checks that the major and minor versions match, and that the patch - number of the second version is _greater than or equal to_ the first parameter. - -The SemVer functions use the [Masterminds semver library](https://github.com/Masterminds/semver), -from the creators of Sprig. - -## Basic Comparisons - -There are two elements to the comparisons. First, a comparison string is a list -of space or comma separated AND comparisons. These are then separated by || (OR) -comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a -comparison that's greater than or equal to 1.2 and less than 3.0.0 or is -greater than or equal to 4.2.3. - -The basic comparisons are: - -- `=`: equal (aliased to no operator) -- `!=`: not equal -- `>`: greater than -- `<`: less than -- `>=`: greater than or equal to -- `<=`: less than or equal to - -_Note, according to the Semantic Version specification pre-releases may not be -API compliant with their release counterpart. It says,_ - -## Working With Prerelease Versions - -Pre-releases, for those not familiar with them, are used for software releases -prior to stable or generally available releases. Examples of prereleases include -development, alpha, beta, and release candidate releases. A prerelease may be -a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the -order of precedence, prereleases come before their associated releases. In this -example `1.2.3-beta.1 < 1.2.3`. - -According to the Semantic Version specification prereleases may not be -API compliant with their release counterpart. It says, - -> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. - -SemVer comparisons using constraints without a prerelease comparator will skip -prerelease versions. For example, `>=1.2.3` will skip prereleases when looking -at a list of releases while `>=1.2.3-0` will evaluate and find prereleases. - -The reason for the `0` as a pre-release version in the example comparison is -because pre-releases can only contain ASCII alphanumerics and hyphens (along with -`.` separators), per the spec. Sorting happens in ASCII sort order, again per the -spec. The lowest character is a `0` in ASCII sort order -(see an [ASCII Table](http://www.asciitable.com/)) - -Understanding ASCII sort ordering is important because A-Z comes before a-z. That -means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case -sensitivity doesn't apply here. This is due to ASCII sort ordering which is what -the spec specifies. - -## Hyphen Range Comparisons - -There are multiple methods to handle ranges and the first is hyphens ranges. -These look like: - -- `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` -- `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` - -## Wildcards In Comparisons - -The `x`, `X`, and `*` characters can be used as a wildcard character. This works -for all comparison operators. When used on the `=` operator it falls -back to the patch level comparison (see tilde below). For example, - -- `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` -- `>= 1.2.x` is equivalent to `>= 1.2.0` -- `<= 2.x` is equivalent to `< 3` -- `*` is equivalent to `>= 0.0.0` - -## Tilde Range Comparisons (Patch) - -The tilde (`~`) comparison operator is for patch level ranges when a minor -version is specified and major level changes when the minor number is missing. -For example, - -- `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` -- `~1` is equivalent to `>= 1, < 2` -- `~2.3` is equivalent to `>= 2.3, < 2.4` -- `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` -- `~1.x` is equivalent to `>= 1, < 2` - -## Caret Range Comparisons (Major) - -The caret (`^`) comparison operator is for major level changes once a stable -(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts -as the API stability level. This is useful when comparisons of API versions as a -major change is API breaking. For example, - -- `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` -- `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` -- `^2.3` is equivalent to `>= 2.3, < 3` -- `^2.x` is equivalent to `>= 2.0.0, < 3` -- `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` -- `^0.2` is equivalent to `>=0.2.0 <0.3.0` -- `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` -- `^0.0` is equivalent to `>=0.0.0 <0.1.0` -- `^0` is equivalent to `>=0.0.0 <1.0.0` diff --git a/go.mod b/go.mod index 7bddeb07..88b88463 100644 --- a/go.mod +++ b/go.mod @@ -32,9 +32,11 @@ require github.com/pkg/errors v0.9.1 // indirect require ( firebase.google.com/go/v4 v4.16.1 github.com/SherClockHolmes/webpush-go v1.4.0 + github.com/google/uuid v1.6.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.22.0 github.com/stripe/stripe-go/v74 v74.30.0 + golang.org/x/text v0.26.0 ) require ( @@ -67,7 +69,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -93,7 +94,6 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect diff --git a/server/server.go b/server/server.go index 94461fbb..7e5fbb94 100644 --- a/server/server.go +++ b/server/server.go @@ -1133,11 +1133,7 @@ func replaceTemplate(tpl string, source string) (string, error) { if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON } - sprigFuncs := sprig.FuncMap() - // remove unsafe functions - delete(sprigFuncs, "env") - delete(sprigFuncs, "expandenv") - t, err := template.New("").Funcs(sprigFuncs).Parse(tpl) + t, err := template.New("").Funcs(sprig.FuncMap()).Parse(tpl) if err != nil { return "", errHTTPBadRequestTemplateInvalid } diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 201b7e24..6a828a2a 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -3,16 +3,10 @@ package sprig import ( "bytes" "encoding/json" - "math/rand" "reflect" "strings" - "time" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - // dfault checks whether `given` is set, and returns default if not set. // // This returns `d` if `given` appears not to be set, and `given` otherwise. diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 8549e99c..3ea46924 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -11,6 +11,8 @@ import ( "strings" ttemplate "text/template" "time" + + "golang.org/x/text/cases" ) // FuncMap produces the function map. @@ -107,7 +109,7 @@ var genericMap = map[string]interface{}{ "trim": strings.TrimSpace, "upper": strings.ToUpper, "lower": strings.ToLower, - "title": strings.Title, + "title": cases.Title, "substr": substring, // Switch order so that "foo" | repeat 5 "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, diff --git a/util/sprig/list.go b/util/sprig/list.go index ca0fbb78..f4e95dda 100644 --- a/util/sprig/list.go +++ b/util/sprig/list.go @@ -39,7 +39,7 @@ func mustPush(list interface{}, v interface{}) ([]interface{}, error) { return append(nl, v), nil default: - return nil, fmt.Errorf("Cannot push on type %s", tp) + return nil, fmt.Errorf("cannot push on type %s", tp) } } @@ -69,7 +69,7 @@ func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { return append([]interface{}{v}, nl...), nil default: - return nil, fmt.Errorf("Cannot prepend on type %s", tp) + return nil, fmt.Errorf("cannot prepend on type %s", tp) } } @@ -113,7 +113,7 @@ func mustChunk(size int, list interface{}) ([][]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot chunk type %s", tp) + return nil, fmt.Errorf("cannot chunk type %s", tp) } } @@ -139,7 +139,7 @@ func mustLast(list interface{}) (interface{}, error) { return l2.Index(l - 1).Interface(), nil default: - return nil, fmt.Errorf("Cannot find last on type %s", tp) + return nil, fmt.Errorf("cannot find last on type %s", tp) } } @@ -165,7 +165,7 @@ func mustFirst(list interface{}) (interface{}, error) { return l2.Index(0).Interface(), nil default: - return nil, fmt.Errorf("Cannot find first on type %s", tp) + return nil, fmt.Errorf("cannot find first on type %s", tp) } } @@ -196,7 +196,7 @@ func mustRest(list interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot find rest on type %s", tp) + return nil, fmt.Errorf("cannot find rest on type %s", tp) } } @@ -227,7 +227,7 @@ func mustInitial(list interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot find initial on type %s", tp) + return nil, fmt.Errorf("cannot find initial on type %s", tp) } } @@ -267,7 +267,7 @@ func mustReverse(v interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot find reverse on type %s", tp) + return nil, fmt.Errorf("cannot find reverse on type %s", tp) } } @@ -298,7 +298,7 @@ func mustCompact(list interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot compact on type %s", tp) + return nil, fmt.Errorf("cannot compact on type %s", tp) } } @@ -329,7 +329,7 @@ func mustUniq(list interface{}) ([]interface{}, error) { return dest, nil default: - return nil, fmt.Errorf("Cannot find uniq on type %s", tp) + return nil, fmt.Errorf("cannot find uniq on type %s", tp) } } @@ -369,7 +369,7 @@ func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { return res, nil default: - return nil, fmt.Errorf("Cannot find without on type %s", tp) + return nil, fmt.Errorf("cannot find without on type %s", tp) } } @@ -401,7 +401,7 @@ func mustHas(needle interface{}, haystack interface{}) (bool, error) { return false, nil default: - return false, fmt.Errorf("Cannot find has on type %s", tp) + return false, fmt.Errorf("cannot find has on type %s", tp) } } @@ -457,7 +457,7 @@ func concat(lists ...interface{}) interface{} { res = append(res, l2.Index(i).Interface()) } default: - panic(fmt.Sprintf("Cannot concat type %s as list", tp)) + panic(fmt.Sprintf("cannot concat type %s as list", tp)) } } return res diff --git a/util/sprig/numeric_test.go b/util/sprig/numeric_test.go index 94e8a6d4..573873d8 100644 --- a/util/sprig/numeric_test.go +++ b/util/sprig/numeric_test.go @@ -101,7 +101,7 @@ func TestToFloat64(t *testing.T) { if target != toFloat64("102") { t.Errorf("Expected 102") } - if 0 != toFloat64("frankie") { + if toFloat64("frankie") != 0 { t.Errorf("Expected 0") } if target != toFloat64(uint16(102)) { @@ -110,10 +110,10 @@ func TestToFloat64(t *testing.T) { if target != toFloat64(uint64(102)) { t.Errorf("Expected 102") } - if 102.1234 != toFloat64(float64(102.1234)) { + if toFloat64(float64(102.1234)) != 102.1234 { t.Errorf("Expected 102.1234") } - if 1 != toFloat64(true) { + if toFloat64(true) != 1 { t.Errorf("Expected 102") } } @@ -137,7 +137,7 @@ func TestToInt64(t *testing.T) { if target != toInt64("102") { t.Errorf("Expected 102") } - if 0 != toInt64("frankie") { + if toInt64("frankie") != 0 { t.Errorf("Expected 0") } if target != toInt64(uint16(102)) { @@ -149,7 +149,7 @@ func TestToInt64(t *testing.T) { if target != toInt64(float64(102.1234)) { t.Errorf("Expected 102") } - if 1 != toInt64(true) { + if toInt64(true) != 1 { t.Errorf("Expected 102") } } @@ -174,7 +174,7 @@ func TestToInt(t *testing.T) { if target != toInt("102") { t.Errorf("Expected 102") } - if 0 != toInt("frankie") { + if toInt("frankie") != 0 { t.Errorf("Expected 0") } if target != toInt(uint16(102)) { @@ -186,7 +186,7 @@ func TestToInt(t *testing.T) { if target != toInt(float64(102.1234)) { t.Errorf("Expected 102") } - if 1 != toInt(true) { + if toInt(true) != 1 { t.Errorf("Expected 102") } } From c0b5151baeee36cfeeb0fea1c3c83519d8acb490 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 10 Jul 2025 20:50:29 +0200 Subject: [PATCH 166/378] Predefined users --- .goreleaser.yml | 84 +++++++++++++++++++------------------------- Makefile | 2 +- cmd/serve.go | 32 ++++++++++++++--- cmd/user.go | 19 +++++++--- server/config.go | 3 +- server/server.go | 2 ++ user/manager.go | 28 ++++++++++++--- user/manager_test.go | 54 +++++++++++++++++++++++++--- 8 files changed, 157 insertions(+), 67 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index fa423a86..f0cf08f6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,76 +1,70 @@ +version: 2 before: hooks: - go mod download - go mod tidy builds: - - - id: ntfy_linux_amd64 + - id: ntfy_linux_amd64 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [amd64] - - - id: ntfy_linux_armv6 + goos: [ linux ] + goarch: [ amd64 ] + - id: ntfy_linux_armv6 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm] - goarm: [6] - - - id: ntfy_linux_armv7 + goos: [ linux ] + goarch: [ arm ] + goarm: [ 6 ] + - id: ntfy_linux_armv7 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm] - goarm: [7] - - - id: ntfy_linux_arm64 + goos: [ linux ] + goarch: [ arm ] + goarm: [ 7 ] + - id: ntfy_linux_arm64 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm64] - - - id: ntfy_windows_amd64 + goos: [ linux ] + goarch: [ arm64 ] + - id: ntfy_windows_amd64 binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [noserver] # don't include server files + tags: [ noserver ] # don't include server files ldflags: - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [windows] - goarch: [amd64] - - - id: ntfy_darwin_all + goos: [ windows ] + goarch: [ amd64 ] + - id: ntfy_darwin_all binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [noserver] # don't include server files + tags: [ noserver ] # don't include server files ldflags: - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [darwin] - goarch: [amd64, arm64] # will be combined to "universal binary" (see below) + goos: [ darwin ] + goarch: [ amd64, arm64 ] # will be combined to "universal binary" (see below) nfpms: - - - package_name: ntfy + - package_name: ntfy homepage: https://heckel.io/ntfy maintainer: Philipp C. Heckel description: Simple pub-sub notification service @@ -106,9 +100,8 @@ nfpms: preremove: "scripts/prerm.sh" postremove: "scripts/postrm.sh" archives: - - - id: ntfy_linux - builds: + - id: ntfy_linux + ids: - ntfy_linux_amd64 - ntfy_linux_armv6 - ntfy_linux_armv7 @@ -122,19 +115,17 @@ archives: - client/client.yml - client/ntfy-client.service - client/user/ntfy-client.service - - - id: ntfy_windows - builds: + - id: ntfy_windows + ids: - ntfy_windows_amd64 - format: zip + formats: [ zip ] wrap_in_directory: true files: - LICENSE - README.md - client/client.yml - - - id: ntfy_darwin - builds: + - id: ntfy_darwin + ids: - ntfy_darwin_all wrap_in_directory: true files: @@ -142,14 +133,13 @@ archives: - README.md - client/client.yml universal_binaries: - - - id: ntfy_darwin_all + - id: ntfy_darwin_all replace: true name_template: ntfy checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" changelog: sort: asc filters: diff --git a/Makefile b/Makefile index 4355423e..82ab53e2 100644 --- a/Makefile +++ b/Makefile @@ -220,7 +220,7 @@ cli-deps-static-sites: touch server/docs/index.html server/site/app.html cli-deps-all: - go install github.com/goreleaser/goreleaser@latest + go install github.com/goreleaser/goreleaser/v2@latest cli-deps-gcc-armv6-armv7: which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; } diff --git a/cmd/serve.go b/cmd/serve.go index 516356c5..abd9ac06 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -52,7 +52,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), - altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provisioned-users", Aliases: []string{"auth_provisioned_users"}, EnvVars: []string{"NTFY_AUTH_PROVISIONED_USERS"}, Usage: "pre-provisioned declarative users"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), @@ -158,7 +158,8 @@ func execServe(c *cli.Context) error { authFile := c.String("auth-file") authStartupQueries := c.String("auth-startup-queries") authDefaultAccess := c.String("auth-default-access") - authUsers := c.StringSlice("auth-users") + authProvisionedUsersRaw := c.StringSlice("auth-provisioned-users") + //authProvisionedAccessRaw := c.StringSlice("auth-provisioned-access") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -348,11 +349,33 @@ func execServe(c *cli.Context) error { webRoot = "/" + webRoot } - // Default auth permissions + // Convert default auth permission, read provisioned users authDefault, err := user.ParsePermission(authDefaultAccess) if err != nil { return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'") } + authProvisionedUsers := make([]*user.User, 0) + for _, userLine := range authProvisionedUsersRaw { + parts := strings.Split(userLine, ":") + if len(parts) != 3 { + return fmt.Errorf("invalid provisioned user %s, expected format: 'name:hash:role'", userLine) + } + username := strings.TrimSpace(parts[0]) + passwordHash := strings.TrimSpace(parts[1]) + role := user.Role(strings.TrimSpace(parts[2])) + if !user.AllowedUsername(username) { + return fmt.Errorf("invalid provisioned user %s, username invalid", userLine) + } else if passwordHash == "" { + return fmt.Errorf("invalid provisioned user %s, password hash cannot be empty", userLine) + } else if !user.AllowedRole(role) { + return fmt.Errorf("invalid provisioned user %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) + } + authProvisionedUsers = append(authProvisionedUsers, &user.User{ + Name: username, + Hash: passwordHash, + Role: role, + }) + } // Special case: Unset default if listenHTTP == "-" { @@ -408,7 +431,8 @@ func execServe(c *cli.Context) error { conf.AuthFile = authFile conf.AuthStartupQueries = authStartupQueries conf.AuthDefault = authDefault - conf.AuthUsers = nil // FIXME + conf.AuthProvisionedUsers = authProvisionedUsers + conf.AuthProvisionedAccess = nil // FIXME conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit diff --git a/cmd/user.go b/cmd/user.go index 9902dace..7519438c 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -224,7 +224,7 @@ func execUserDel(c *cli.Context) error { if err != nil { return err } - if _, err := manager.User(username); err == user.ErrUserNotFound { + if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } if err := manager.RemoveUser(username); err != nil { @@ -250,7 +250,7 @@ func execUserChangePass(c *cli.Context) error { if err != nil { return err } - if _, err := manager.User(username); err == user.ErrUserNotFound { + if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } if password == "" { @@ -278,7 +278,7 @@ func execUserChangeRole(c *cli.Context) error { if err != nil { return err } - if _, err := manager.User(username); err == user.ErrUserNotFound { + if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } if err := manager.ChangeRole(username, role); err != nil { @@ -302,7 +302,7 @@ func execUserChangeTier(c *cli.Context) error { if err != nil { return err } - if _, err := manager.User(username); err == user.ErrUserNotFound { + if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } if tier == tierReset { @@ -344,7 +344,16 @@ func createUserManager(c *cli.Context) (*user.Manager, error) { if err != nil { return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'") } - return user.NewManager(authFile, authStartupQueries, authDefault, user.DefaultUserPasswordBcryptCost, user.DefaultUserStatsQueueWriterInterval) + authConfig := &user.Config{ + Filename: authFile, + StartupQueries: authStartupQueries, + DefaultAccess: authDefault, + ProvisionedUsers: nil, //FIXME + ProvisionedAccess: nil, //FIXME + BcryptCost: user.DefaultUserPasswordBcryptCost, + QueueWriterInterval: user.DefaultUserStatsQueueWriterInterval, + } + return user.NewManager(authConfig) } func readPasswordAndConfirm(c *cli.Context) (string, error) { diff --git a/server/config.go b/server/config.go index 67554021..c163614f 100644 --- a/server/config.go +++ b/server/config.go @@ -93,7 +93,8 @@ type Config struct { AuthFile string AuthStartupQueries string AuthDefault user.Permission - AuthUsers []user.User + AuthProvisionedUsers []*user.User + AuthProvisionedAccess map[string][]*user.Grant AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration AttachmentCacheDir string diff --git a/server/server.go b/server/server.go index 10ad7d8e..cba9b181 100644 --- a/server/server.go +++ b/server/server.go @@ -193,6 +193,8 @@ func New(conf *Config) (*Server, error) { Filename: conf.AuthFile, StartupQueries: conf.AuthStartupQueries, DefaultAccess: conf.AuthDefault, + ProvisionedUsers: conf.AuthProvisionedUsers, + ProvisionedAccess: conf.AuthProvisionedAccess, BcryptCost: conf.AuthBcryptCost, QueueWriterInterval: conf.AuthStatsQueueWriterInterval, } diff --git a/user/manager.go b/user/manager.go index 04c3c878..8932f34a 100644 --- a/user/manager.go +++ b/user/manager.go @@ -449,13 +449,13 @@ type Manager struct { } type Config struct { - Filename string - StartupQueries string + Filename string // Database filename, e.g. "/var/lib/ntfy/user.db" + StartupQueries string // Queries to run on startup, e.g. to create initial users or tiers DefaultAccess Permission // Default permission if no ACL matches ProvisionedUsers []*User // Predefined users to create on startup ProvisionedAccess map[string][]*Grant // Predefined access grants to create on startup - BcryptCost int // Makes testing easier - QueueWriterInterval time.Duration + QueueWriterInterval time.Duration // Interval for the async queue writer to flush stats and token updates to the database + BcryptCost int // Cost of generated passwords; lowering makes testing faster } var _ Auther = (*Manager)(nil) @@ -469,7 +469,6 @@ func NewManager(config *Config) (*Manager, error) { if config.QueueWriterInterval.Seconds() <= 0 { config.QueueWriterInterval = DefaultUserStatsQueueWriterInterval } - // Open DB and run setup queries db, err := sql.Open("sqlite3", config.Filename) if err != nil { @@ -487,6 +486,9 @@ func NewManager(config *Config) (*Manager, error) { statsQueue: make(map[string]*Stats), tokenQueue: make(map[string]*TokenUpdate), } + if err := manager.provisionUsers(); err != nil { + return nil, err + } go manager.asyncQueueWriter(config.QueueWriterInterval) return manager, nil } @@ -1522,6 +1524,22 @@ func (a *Manager) Close() error { return a.db.Close() } +func (a *Manager) provisionUsers() error { + for _, user := range a.config.ProvisionedUsers { + if err := a.AddUser(user.Name, user.Hash, user.Role, true); err != nil && !errors.Is(err, ErrUserExists) { + return err + } + } + for username, grants := range a.config.ProvisionedAccess { + for _, grant := range grants { + if err := a.AllowAccess(username, grant.TopicPattern, grant.Allow); err != nil { + return err + } + } + } + return nil +} + // toSQLWildcard converts a wildcard string to a SQL wildcard string. It only allows '*' as wildcards, // and escapes '_', assuming '\' as escape character. func toSQLWildcard(s string) string { diff --git a/user/manager_test.go b/user/manager_test.go index 89f35e3c..b57c762c 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -731,7 +731,14 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { } func TestManager_EnqueueStats_ResetStats(t *testing.T) { - a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond) + conf := &Config{ + Filename: filepath.Join(t.TempDir(), "db"), + StartupQueries: "", + DefaultAccess: PermissionReadWrite, + BcryptCost: bcrypt.MinCost, + QueueWriterInterval: 1500 * time.Millisecond, + } + a, err := NewManager(conf) require.Nil(t, err) require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) @@ -773,7 +780,14 @@ func TestManager_EnqueueStats_ResetStats(t *testing.T) { } func TestManager_EnqueueTokenUpdate(t *testing.T) { - a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 500*time.Millisecond) + conf := &Config{ + Filename: filepath.Join(t.TempDir(), "db"), + StartupQueries: "", + DefaultAccess: PermissionReadWrite, + BcryptCost: bcrypt.MinCost, + QueueWriterInterval: 500 * time.Millisecond, + } + a, err := NewManager(conf) require.Nil(t, err) require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) @@ -806,7 +820,14 @@ func TestManager_EnqueueTokenUpdate(t *testing.T) { } func TestManager_ChangeSettings(t *testing.T) { - a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond) + conf := &Config{ + Filename: filepath.Join(t.TempDir(), "db"), + StartupQueries: "", + DefaultAccess: PermissionReadWrite, + BcryptCost: bcrypt.MinCost, + QueueWriterInterval: 1500 * time.Millisecond, + } + a, err := NewManager(conf) require.Nil(t, err) require.Nil(t, a.AddUser("ben", "ben", RoleUser, false)) @@ -1075,6 +1096,24 @@ func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) { require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionWrite)) } +func TestManager_WithProvisionedUsers(t *testing.T) { + f := filepath.Join(t.TempDir(), "user.db") + conf := &Config{ + Filename: f, + DefaultAccess: PermissionReadWrite, + ProvisionedUsers: []*User{ + {Name: "phil", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, + }, + } + a, err := NewManager(conf) + require.Nil(t, err) + users, err := a.Users() + require.Nil(t, err) + for _, u := range users { + fmt.Println(u.ID, u.Name, u.Role) + } +} + func TestToFromSQLWildcard(t *testing.T) { require.Equal(t, "up%", toSQLWildcard("up*")) require.Equal(t, "up\\_%", toSQLWildcard("up_*")) @@ -1336,7 +1375,14 @@ func newTestManager(t *testing.T, defaultAccess Permission) *Manager { } func newTestManagerFromFile(t *testing.T, filename, startupQueries string, defaultAccess Permission, bcryptCost int, statsWriterInterval time.Duration) *Manager { - a, err := NewManager(filename, startupQueries, defaultAccess, bcryptCost, statsWriterInterval) + conf := &Config{ + Filename: filename, + StartupQueries: startupQueries, + DefaultAccess: defaultAccess, + BcryptCost: bcryptCost, + QueueWriterInterval: statsWriterInterval, + } + a, err := NewManager(conf) require.Nil(t, err) return a } From 8d6f1eecdfd6f9bf54bb785e4f317cd9c7ee6c0c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 10 Jul 2025 21:06:39 +0200 Subject: [PATCH 167/378] Fix build --- .goreleaser.yml | 84 ++++++++++++++++++++++--------------------------- Makefile | 2 +- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index fa423a86..f0cf08f6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,76 +1,70 @@ +version: 2 before: hooks: - go mod download - go mod tidy builds: - - - id: ntfy_linux_amd64 + - id: ntfy_linux_amd64 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [amd64] - - - id: ntfy_linux_armv6 + goos: [ linux ] + goarch: [ amd64 ] + - id: ntfy_linux_armv6 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm] - goarm: [6] - - - id: ntfy_linux_armv7 + goos: [ linux ] + goarch: [ arm ] + goarm: [ 6 ] + - id: ntfy_linux_armv7 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm] - goarm: [7] - - - id: ntfy_linux_arm64 + goos: [ linux ] + goarch: [ arm ] + goarm: [ 7 ] + - id: ntfy_linux_arm64 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm64] - - - id: ntfy_windows_amd64 + goos: [ linux ] + goarch: [ arm64 ] + - id: ntfy_windows_amd64 binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [noserver] # don't include server files + tags: [ noserver ] # don't include server files ldflags: - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [windows] - goarch: [amd64] - - - id: ntfy_darwin_all + goos: [ windows ] + goarch: [ amd64 ] + - id: ntfy_darwin_all binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [noserver] # don't include server files + tags: [ noserver ] # don't include server files ldflags: - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [darwin] - goarch: [amd64, arm64] # will be combined to "universal binary" (see below) + goos: [ darwin ] + goarch: [ amd64, arm64 ] # will be combined to "universal binary" (see below) nfpms: - - - package_name: ntfy + - package_name: ntfy homepage: https://heckel.io/ntfy maintainer: Philipp C. Heckel description: Simple pub-sub notification service @@ -106,9 +100,8 @@ nfpms: preremove: "scripts/prerm.sh" postremove: "scripts/postrm.sh" archives: - - - id: ntfy_linux - builds: + - id: ntfy_linux + ids: - ntfy_linux_amd64 - ntfy_linux_armv6 - ntfy_linux_armv7 @@ -122,19 +115,17 @@ archives: - client/client.yml - client/ntfy-client.service - client/user/ntfy-client.service - - - id: ntfy_windows - builds: + - id: ntfy_windows + ids: - ntfy_windows_amd64 - format: zip + formats: [ zip ] wrap_in_directory: true files: - LICENSE - README.md - client/client.yml - - - id: ntfy_darwin - builds: + - id: ntfy_darwin + ids: - ntfy_darwin_all wrap_in_directory: true files: @@ -142,14 +133,13 @@ archives: - README.md - client/client.yml universal_binaries: - - - id: ntfy_darwin_all + - id: ntfy_darwin_all replace: true name_template: ntfy checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" changelog: sort: asc filters: diff --git a/Makefile b/Makefile index 4355423e..82ab53e2 100644 --- a/Makefile +++ b/Makefile @@ -220,7 +220,7 @@ cli-deps-static-sites: touch server/docs/index.html server/site/app.html cli-deps-all: - go install github.com/goreleaser/goreleaser@latest + go install github.com/goreleaser/goreleaser/v2@latest cli-deps-gcc-armv6-armv7: which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; } From 1ce08a18c03966f0b69a34d3afac4a99b0ab91c5 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 10 Jul 2025 21:17:58 +0200 Subject: [PATCH 168/378] Bump release notes --- Makefile | 2 +- docs/install.md | 60 ++++++++++++++++++++++++------------------------ docs/releases.md | 32 +++++++++++++++----------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 82ab53e2..575bb788 100644 --- a/Makefile +++ b/Makefile @@ -301,7 +301,7 @@ release: clean cli-deps release-checks docs web check goreleaser release --clean release-snapshot: clean cli-deps docs web check - goreleaser release --snapshot --skip-publish --clean + goreleaser release --snapshot --clean release-checks: $(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-)) diff --git a/docs/install.md b/docs/install.md index 42c868fc..b841e950 100644 --- a/docs/install.md +++ b/docs/install.md @@ -30,37 +30,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.tar.gz - tar zxvf ntfy_2.12.0_linux_amd64.tar.gz - sudo cp -a ntfy_2.12.0_linux_amd64/ntfy /usr/local/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_amd64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.tar.gz + tar zxvf ntfy_2.13.0_linux_amd64.tar.gz + sudo cp -a ntfy_2.13.0_linux_amd64/ntfy /usr/local/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.tar.gz - tar zxvf ntfy_2.12.0_linux_armv6.tar.gz - sudo cp -a ntfy_2.12.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.tar.gz + tar zxvf ntfy_2.13.0_linux_armv6.tar.gz + sudo cp -a ntfy_2.13.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.tar.gz - tar zxvf ntfy_2.12.0_linux_armv7.tar.gz - sudo cp -a ntfy_2.12.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.tar.gz + tar zxvf ntfy_2.13.0_linux_armv7.tar.gz + sudo cp -a ntfy_2.13.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.tar.gz - tar zxvf ntfy_2.12.0_linux_arm64.tar.gz - sudo cp -a ntfy_2.12.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.tar.gz + tar zxvf ntfy_2.13.0_linux_arm64.tar.gz + sudo cp -a ntfy_2.13.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -110,7 +110,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -118,7 +118,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -126,7 +126,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -134,7 +134,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -144,28 +144,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -195,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. -To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz), +To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_darwin_all.tar.gz), extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz > ntfy_2.12.0_darwin_all.tar.gz -tar zxvf ntfy_2.12.0_darwin_all.tar.gz -sudo cp -a ntfy_2.12.0_darwin_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_darwin_all.tar.gz > ntfy_2.13.0_darwin_all.tar.gz +tar zxvf ntfy_2.13.0_darwin_all.tar.gz +sudo cp -a ntfy_2.13.0_darwin_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_2.12.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_2.13.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -224,7 +224,7 @@ brew install ntfy ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_windows_amd64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). diff --git a/docs/releases.md b/docs/releases.md index 0877527e..484f3623 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,25 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). +### ntfy server v2.13.0 +Released July 10, 2025 + +This is a relatively small release, mainly to support IPv6 and to add more sophisticated +proxy header support. Quick reminder that if you like ntfy, **please consider sponsoring us** +via [GitHub Sponsors](https://github.com/sponsors/binwiederhier) and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app). +ntfy will always remain open source. + +**Features:** + +* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) +* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) +* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) + +**Languages** + +* Update new languages from Weblate. Thanks to all the contributors! +* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app + ### ntfy server v2.12.0 Released May 29, 2025 @@ -1433,19 +1452,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy server v2.13.0 (UNRELEASED) - -**Features:** - -* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) -* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) -* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) - -**Languages** - -* Update new languages from Weblate. Thanks to all the contributors! -* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app - ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** From fea0f301d2375a664ba52f50be7998efb4fa236e Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 11 Jul 2025 13:19:31 +0200 Subject: [PATCH 169/378] Sprig funcs --- .goreleaser.yml | 84 +++++++++++++++++++++--------------------------- Makefile | 4 +-- docs/install.md | 60 +++++++++++++++++----------------- docs/releases.md | 29 +++++++++++------ 4 files changed, 89 insertions(+), 88 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index fa423a86..f0cf08f6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,76 +1,70 @@ +version: 2 before: hooks: - go mod download - go mod tidy builds: - - - id: ntfy_linux_amd64 + - id: ntfy_linux_amd64 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [amd64] - - - id: ntfy_linux_armv6 + goos: [ linux ] + goarch: [ amd64 ] + - id: ntfy_linux_armv6 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm] - goarm: [6] - - - id: ntfy_linux_armv7 + goos: [ linux ] + goarch: [ arm ] + goarm: [ 6 ] + - id: ntfy_linux_armv7 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm] - goarm: [7] - - - id: ntfy_linux_arm64 + goos: [ linux ] + goarch: [ arm ] + goarm: [ 7 ] + - id: ntfy_linux_arm64 binary: ntfy env: - CGO_ENABLED=1 # required for go-sqlite3 - CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu - tags: [sqlite_omit_load_extension,osusergo,netgo] + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [linux] - goarch: [arm64] - - - id: ntfy_windows_amd64 + goos: [ linux ] + goarch: [ arm64 ] + - id: ntfy_windows_amd64 binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [noserver] # don't include server files + tags: [ noserver ] # don't include server files ldflags: - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [windows] - goarch: [amd64] - - - id: ntfy_darwin_all + goos: [ windows ] + goarch: [ amd64 ] + - id: ntfy_darwin_all binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [noserver] # don't include server files + tags: [ noserver ] # don't include server files ldflags: - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" - goos: [darwin] - goarch: [amd64, arm64] # will be combined to "universal binary" (see below) + goos: [ darwin ] + goarch: [ amd64, arm64 ] # will be combined to "universal binary" (see below) nfpms: - - - package_name: ntfy + - package_name: ntfy homepage: https://heckel.io/ntfy maintainer: Philipp C. Heckel description: Simple pub-sub notification service @@ -106,9 +100,8 @@ nfpms: preremove: "scripts/prerm.sh" postremove: "scripts/postrm.sh" archives: - - - id: ntfy_linux - builds: + - id: ntfy_linux + ids: - ntfy_linux_amd64 - ntfy_linux_armv6 - ntfy_linux_armv7 @@ -122,19 +115,17 @@ archives: - client/client.yml - client/ntfy-client.service - client/user/ntfy-client.service - - - id: ntfy_windows - builds: + - id: ntfy_windows + ids: - ntfy_windows_amd64 - format: zip + formats: [ zip ] wrap_in_directory: true files: - LICENSE - README.md - client/client.yml - - - id: ntfy_darwin - builds: + - id: ntfy_darwin + ids: - ntfy_darwin_all wrap_in_directory: true files: @@ -142,14 +133,13 @@ archives: - README.md - client/client.yml universal_binaries: - - - id: ntfy_darwin_all + - id: ntfy_darwin_all replace: true name_template: ntfy checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" changelog: sort: asc filters: diff --git a/Makefile b/Makefile index 4355423e..575bb788 100644 --- a/Makefile +++ b/Makefile @@ -220,7 +220,7 @@ cli-deps-static-sites: touch server/docs/index.html server/site/app.html cli-deps-all: - go install github.com/goreleaser/goreleaser@latest + go install github.com/goreleaser/goreleaser/v2@latest cli-deps-gcc-armv6-armv7: which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; } @@ -301,7 +301,7 @@ release: clean cli-deps release-checks docs web check goreleaser release --clean release-snapshot: clean cli-deps docs web check - goreleaser release --snapshot --skip-publish --clean + goreleaser release --snapshot --clean release-checks: $(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-)) diff --git a/docs/install.md b/docs/install.md index 42c868fc..b841e950 100644 --- a/docs/install.md +++ b/docs/install.md @@ -30,37 +30,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.tar.gz - tar zxvf ntfy_2.12.0_linux_amd64.tar.gz - sudo cp -a ntfy_2.12.0_linux_amd64/ntfy /usr/local/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_amd64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.tar.gz + tar zxvf ntfy_2.13.0_linux_amd64.tar.gz + sudo cp -a ntfy_2.13.0_linux_amd64/ntfy /usr/local/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.tar.gz - tar zxvf ntfy_2.12.0_linux_armv6.tar.gz - sudo cp -a ntfy_2.12.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.tar.gz + tar zxvf ntfy_2.13.0_linux_armv6.tar.gz + sudo cp -a ntfy_2.13.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.tar.gz - tar zxvf ntfy_2.12.0_linux_armv7.tar.gz - sudo cp -a ntfy_2.12.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.tar.gz + tar zxvf ntfy_2.13.0_linux_armv7.tar.gz + sudo cp -a ntfy_2.13.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.tar.gz - tar zxvf ntfy_2.12.0_linux_arm64.tar.gz - sudo cp -a ntfy_2.12.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.tar.gz + tar zxvf ntfy_2.13.0_linux_arm64.tar.gz + sudo cp -a ntfy_2.13.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -110,7 +110,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -118,7 +118,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -126,7 +126,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -134,7 +134,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -144,28 +144,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -195,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. -To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz), +To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_darwin_all.tar.gz), extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz > ntfy_2.12.0_darwin_all.tar.gz -tar zxvf ntfy_2.12.0_darwin_all.tar.gz -sudo cp -a ntfy_2.12.0_darwin_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_darwin_all.tar.gz > ntfy_2.13.0_darwin_all.tar.gz +tar zxvf ntfy_2.13.0_darwin_all.tar.gz +sudo cp -a ntfy_2.13.0_darwin_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_2.12.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_2.13.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -224,7 +224,7 @@ brew install ntfy ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_windows_amd64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). diff --git a/docs/releases.md b/docs/releases.md index 5e18edab..fe91f580 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,25 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). +### ntfy server v2.13.0 +Released July 10, 2025 + +This is a relatively small release, mainly to support IPv6 and to add more sophisticated +proxy header support. Quick reminder that if you like ntfy, **please consider sponsoring us** +via [GitHub Sponsors](https://github.com/sponsors/binwiederhier) and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app). +ntfy will always remain open source. + +**Features:** + +* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) +* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) +* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) + +**Languages** + +* Update new languages from Weblate. Thanks to all the contributors! +* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app + ### ntfy server v2.12.0 Released May 29, 2025 @@ -1433,20 +1452,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy server v2.13.0 (UNRELEASED) +### ntfy server v2.14.0 (UNRELEASED) **Features:** -* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) -* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) -* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) * You can now use a subset of [Sprig](https://github.com/Masterminds/sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) -**Languages** - -* Update new languages from Weblate. Thanks to all the contributors! -* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app - ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** From 2a468493f92a17e626a3b3a48d87ccbc72d3848b Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 13 Jul 2025 12:45:00 +0200 Subject: [PATCH 170/378] any --- util/sprig/date.go | 14 +++--- util/sprig/date_test.go | 34 ++++++------- util/sprig/defaults.go | 33 ++++++------- util/sprig/defaults_test.go | 16 +++--- util/sprig/dict.go | 39 +++++++-------- util/sprig/example_test.go | 2 +- util/sprig/functions.go | 74 +++++++-------------------- util/sprig/functions_test.go | 4 +- util/sprig/list.go | 96 ++++++++++++++++++------------------ util/sprig/numeric.go | 22 ++++----- util/sprig/numeric_test.go | 2 +- util/sprig/reflect.go | 10 ++-- util/sprig/strings.go | 18 +++---- util/sprig/strings_test.go | 16 +++--- util/sprig/url.go | 8 +-- util/sprig/url_test.go | 2 +- 16 files changed, 174 insertions(+), 216 deletions(-) diff --git a/util/sprig/date.go b/util/sprig/date.go index ed022dda..3fed04e9 100644 --- a/util/sprig/date.go +++ b/util/sprig/date.go @@ -10,19 +10,19 @@ import ( // Date can be a `time.Time` or an `int, int32, int64`. // In the later case, it is treated as seconds since UNIX // epoch. -func date(fmt string, date interface{}) string { +func date(fmt string, date any) string { return dateInZone(fmt, date, "Local") } -func htmlDate(date interface{}) string { +func htmlDate(date any) string { return dateInZone("2006-01-02", date, "Local") } -func htmlDateInZone(date interface{}, zone string) string { +func htmlDateInZone(date any, zone string) string { return dateInZone("2006-01-02", date, zone) } -func dateInZone(fmt string, date interface{}, zone string) string { +func dateInZone(fmt string, date any, zone string) string { var t time.Time switch date := date.(type) { default: @@ -63,7 +63,7 @@ func mustDateModify(fmt string, date time.Time) (time.Time, error) { return date.Add(d), nil } -func dateAgo(date interface{}) string { +func dateAgo(date any) string { var t time.Time switch date := date.(type) { @@ -81,7 +81,7 @@ func dateAgo(date interface{}) string { return duration.String() } -func duration(sec interface{}) string { +func duration(sec any) string { var n int64 switch value := sec.(type) { default: @@ -94,7 +94,7 @@ func duration(sec interface{}) string { return (time.Duration(n) * time.Second).String() } -func durationRound(duration interface{}) string { +func durationRound(duration any) string { var d time.Duration switch duration := duration.(type) { default: diff --git a/util/sprig/date_test.go b/util/sprig/date_test.go index be7ec9d9..3ebfa2be 100644 --- a/util/sprig/date_test.go +++ b/util/sprig/date_test.go @@ -15,15 +15,15 @@ func TestHtmlDate(t *testing.T) { func TestAgo(t *testing.T) { tpl := "{{ ago .Time }}" - if err := runtv(tpl, "2m5s", map[string]interface{}{"Time": time.Now().Add(-125 * time.Second)}); err != nil { + if err := runtv(tpl, "2m5s", map[string]any{"Time": time.Now().Add(-125 * time.Second)}); err != nil { t.Error(err) } - if err := runtv(tpl, "2h34m17s", map[string]interface{}{"Time": time.Now().Add(-(2*3600 + 34*60 + 17) * time.Second)}); err != nil { + if err := runtv(tpl, "2h34m17s", map[string]any{"Time": time.Now().Add(-(2*3600 + 34*60 + 17) * time.Second)}); err != nil { t.Error(err) } - if err := runtv(tpl, "-5s", map[string]interface{}{"Time": time.Now().Add(5 * time.Second)}); err != nil { + if err := runtv(tpl, "-5s", map[string]any{"Time": time.Now().Add(5 * time.Second)}); err != nil { t.Error(err) } } @@ -42,7 +42,7 @@ func TestUnixEpoch(t *testing.T) { } tpl := `{{unixEpoch .Time}}` - if err = runtv(tpl, "1560458379", map[string]interface{}{"Time": tm}); err != nil { + if err = runtv(tpl, "1560458379", map[string]any{"Time": tm}); err != nil { t.Error(err) } } @@ -55,66 +55,66 @@ func TestDateInZone(t *testing.T) { tpl := `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "UTC" }}` // Test time.Time input - if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": tm}); err != nil { t.Error(err) } // Test pointer to time.Time input - if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": &tm}); err != nil { + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": &tm}); err != nil { t.Error(err) } // Test no time input. This should be close enough to time.Now() we can test loc, _ := time.LoadLocation("UTC") - if err = runtv(tpl, time.Now().In(loc).Format("02 Jan 06 15:04 -0700"), map[string]interface{}{"Time": ""}); err != nil { + if err = runtv(tpl, time.Now().In(loc).Format("02 Jan 06 15:04 -0700"), map[string]any{"Time": ""}); err != nil { t.Error(err) } // Test unix timestamp as int64 - if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int64(1560458379)}); err != nil { + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": int64(1560458379)}); err != nil { t.Error(err) } // Test unix timestamp as int32 - if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int32(1560458379)}); err != nil { + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": int32(1560458379)}); err != nil { t.Error(err) } // Test unix timestamp as int - if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": int(1560458379)}); err != nil { + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": int(1560458379)}); err != nil { t.Error(err) } // Test case of invalid timezone tpl = `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "foobar" }}` - if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]interface{}{"Time": tm}); err != nil { + if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": tm}); err != nil { t.Error(err) } } func TestDuration(t *testing.T) { tpl := "{{ duration .Secs }}" - if err := runtv(tpl, "1m1s", map[string]interface{}{"Secs": "61"}); err != nil { + if err := runtv(tpl, "1m1s", map[string]any{"Secs": "61"}); err != nil { t.Error(err) } - if err := runtv(tpl, "1h0m0s", map[string]interface{}{"Secs": "3600"}); err != nil { + if err := runtv(tpl, "1h0m0s", map[string]any{"Secs": "3600"}); err != nil { t.Error(err) } // 1d2h3m4s but go is opinionated - if err := runtv(tpl, "26h3m4s", map[string]interface{}{"Secs": "93784"}); err != nil { + if err := runtv(tpl, "26h3m4s", map[string]any{"Secs": "93784"}); err != nil { t.Error(err) } } func TestDurationRound(t *testing.T) { tpl := "{{ durationRound .Time }}" - if err := runtv(tpl, "2h", map[string]interface{}{"Time": "2h5s"}); err != nil { + if err := runtv(tpl, "2h", map[string]any{"Time": "2h5s"}); err != nil { t.Error(err) } - if err := runtv(tpl, "1d", map[string]interface{}{"Time": "24h5s"}); err != nil { + if err := runtv(tpl, "1d", map[string]any{"Time": "24h5s"}); err != nil { t.Error(err) } - if err := runtv(tpl, "3mo", map[string]interface{}{"Time": "2400h5s"}); err != nil { + if err := runtv(tpl, "3mo", map[string]any{"Time": "2400h5s"}); err != nil { t.Error(err) } } diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 6a828a2a..7dcf7450 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -17,7 +17,7 @@ import ( // Structs are never considered unset. // // For everything else, including pointers, a nil value is unset. -func dfault(d interface{}, given ...interface{}) interface{} { +func dfault(d any, given ...any) any { if empty(given) || empty(given[0]) { return d @@ -26,7 +26,7 @@ func dfault(d interface{}, given ...interface{}) interface{} { } // empty returns true if the given value has the zero value for its type. -func empty(given interface{}) bool { +func empty(given any) bool { g := reflect.ValueOf(given) if !g.IsValid() { return true @@ -54,7 +54,7 @@ func empty(given interface{}) bool { } // coalesce returns the first non-empty value. -func coalesce(v ...interface{}) interface{} { +func coalesce(v ...any) any { for _, val := range v { if !empty(val) { return val @@ -65,7 +65,7 @@ func coalesce(v ...interface{}) interface{} { // all returns true if empty(x) is false for all values x in the list. // If the list is empty, return true. -func all(v ...interface{}) bool { +func all(v ...any) bool { for _, val := range v { if empty(val) { return false @@ -74,9 +74,9 @@ func all(v ...interface{}) bool { return true } -// any returns true if empty(x) is false for any x in the list. +// anyNonEmpty returns true if empty(x) is false for anyNonEmpty x in the list. // If the list is empty, return false. -func any(v ...interface{}) bool { +func anyNonEmpty(v ...any) bool { for _, val := range v { if !empty(val) { return true @@ -86,25 +86,25 @@ func any(v ...interface{}) bool { } // fromJSON decodes JSON into a structured value, ignoring errors. -func fromJSON(v string) interface{} { +func fromJSON(v string) any { output, _ := mustFromJSON(v) return output } // mustFromJSON decodes JSON into a structured value, returning errors. -func mustFromJSON(v string) (interface{}, error) { - var output interface{} +func mustFromJSON(v string) (any, error) { + var output any err := json.Unmarshal([]byte(v), &output) return output, err } // toJSON encodes an item into a JSON string -func toJSON(v interface{}) string { +func toJSON(v any) string { output, _ := json.Marshal(v) return string(output) } -func mustToJSON(v interface{}) (string, error) { +func mustToJSON(v any) (string, error) { output, err := json.Marshal(v) if err != nil { return "", err @@ -113,12 +113,12 @@ func mustToJSON(v interface{}) (string, error) { } // toPrettyJSON encodes an item into a pretty (indented) JSON string -func toPrettyJSON(v interface{}) string { +func toPrettyJSON(v any) string { output, _ := json.MarshalIndent(v, "", " ") return string(output) } -func mustToPrettyJSON(v interface{}) (string, error) { +func mustToPrettyJSON(v any) (string, error) { output, err := json.MarshalIndent(v, "", " ") if err != nil { return "", err @@ -127,7 +127,7 @@ func mustToPrettyJSON(v interface{}) (string, error) { } // toRawJSON encodes an item into a JSON string with no escaping of HTML characters. -func toRawJSON(v interface{}) string { +func toRawJSON(v any) string { output, err := mustToRawJSON(v) if err != nil { panic(err) @@ -136,7 +136,7 @@ func toRawJSON(v interface{}) string { } // mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters. -func mustToRawJSON(v interface{}) (string, error) { +func mustToRawJSON(v any) (string, error) { buf := new(bytes.Buffer) enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) @@ -148,10 +148,9 @@ func mustToRawJSON(v interface{}) (string, error) { } // ternary returns the first value if the last value is true, otherwise returns the second value. -func ternary(vt interface{}, vf interface{}, v bool) interface{} { +func ternary(vt any, vf any, v bool) any { if v { return vt } - return vf } diff --git a/util/sprig/defaults_test.go b/util/sprig/defaults_test.go index eb7e35b4..f67c9cd9 100644 --- a/util/sprig/defaults_test.go +++ b/util/sprig/defaults_test.go @@ -53,7 +53,7 @@ func TestEmpty(t *testing.T) { t.Error(err) } - dict := map[string]interface{}{"top": map[string]interface{}{}} + dict := map[string]any{"top": map[string]any{}} tpl = `{{if empty .top.NoSuchThing}}1{{else}}0{{end}}` if err := runtv(tpl, "1", dict); err != nil { t.Error(err) @@ -77,7 +77,7 @@ func TestCoalesce(t *testing.T) { assert.NoError(t, runt(tpl, expect)) } - dict := map[string]interface{}{"top": map[string]interface{}{}} + dict := map[string]any{"top": map[string]any{}} tpl := `{{ coalesce .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` if err := runtv(tpl, "airplane", dict); err != nil { t.Error(err) @@ -97,7 +97,7 @@ func TestAll(t *testing.T) { assert.NoError(t, runt(tpl, expect)) } - dict := map[string]interface{}{"top": map[string]interface{}{}} + dict := map[string]any{"top": map[string]any{}} tpl := `{{ all .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` if err := runtv(tpl, "false", dict); err != nil { t.Error(err) @@ -117,7 +117,7 @@ func TestAny(t *testing.T) { assert.NoError(t, runt(tpl, expect)) } - dict := map[string]interface{}{"top": map[string]interface{}{}} + dict := map[string]any{"top": map[string]any{}} tpl := `{{ any .top.NoSuchThing .bottom .bottom.dollar "airplane"}}` if err := runtv(tpl, "true", dict); err != nil { t.Error(err) @@ -125,7 +125,7 @@ func TestAny(t *testing.T) { } func TestFromJSON(t *testing.T) { - dict := map[string]interface{}{"Input": `{"foo": 55}`} + dict := map[string]any{"Input": `{"foo": 55}`} tpl := `{{.Input | fromJSON}}` expected := `map[foo:55]` @@ -141,7 +141,7 @@ func TestFromJSON(t *testing.T) { } func TestToJSON(t *testing.T) { - dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} + dict := map[string]any{"Top": map[string]any{"bool": true, "string": "test", "number": 42}} tpl := `{{.Top | toJSON}}` expected := `{"bool":true,"number":42,"string":"test"}` @@ -151,7 +151,7 @@ func TestToJSON(t *testing.T) { } func TestToPrettyJSON(t *testing.T) { - dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42}} + dict := map[string]any{"Top": map[string]any{"bool": true, "string": "test", "number": 42}} tpl := `{{.Top | toPrettyJSON}}` expected := `{ "bool": true, @@ -164,7 +164,7 @@ func TestToPrettyJSON(t *testing.T) { } func TestToRawJSON(t *testing.T) { - dict := map[string]interface{}{"Top": map[string]interface{}{"bool": true, "string": "test", "number": 42, "html": ""}} + dict := map[string]any{"Top": map[string]any{"bool": true, "string": "test", "number": 42, "html": ""}} tpl := `{{.Top | toRawJSON}}` expected := `{"bool":true,"html":"","number":42,"string":"test"}` diff --git a/util/sprig/dict.go b/util/sprig/dict.go index fd2dd711..97182a97 100644 --- a/util/sprig/dict.go +++ b/util/sprig/dict.go @@ -1,29 +1,29 @@ package sprig -func get(d map[string]interface{}, key string) interface{} { +func get(d map[string]any, key string) any { if val, ok := d[key]; ok { return val } return "" } -func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} { +func set(d map[string]any, key string, value any) map[string]any { d[key] = value return d } -func unset(d map[string]interface{}, key string) map[string]interface{} { +func unset(d map[string]any, key string) map[string]any { delete(d, key) return d } -func hasKey(d map[string]interface{}, key string) bool { +func hasKey(d map[string]any, key string) bool { _, ok := d[key] return ok } -func pluck(key string, d ...map[string]interface{}) []interface{} { - res := []interface{}{} +func pluck(key string, d ...map[string]any) []any { + var res []any for _, dict := range d { if val, ok := dict[key]; ok { res = append(res, val) @@ -32,7 +32,7 @@ func pluck(key string, d ...map[string]interface{}) []interface{} { return res } -func keys(dicts ...map[string]interface{}) []string { +func keys(dicts ...map[string]any) []string { k := []string{} for _, dict := range dicts { for key := range dict { @@ -42,8 +42,8 @@ func keys(dicts ...map[string]interface{}) []string { return k } -func pick(dict map[string]interface{}, keys ...string) map[string]interface{} { - res := map[string]interface{}{} +func pick(dict map[string]any, keys ...string) map[string]any { + res := map[string]any{} for _, k := range keys { if v, ok := dict[k]; ok { res[k] = v @@ -52,8 +52,8 @@ func pick(dict map[string]interface{}, keys ...string) map[string]interface{} { return res } -func omit(dict map[string]interface{}, keys ...string) map[string]interface{} { - res := map[string]interface{}{} +func omit(dict map[string]any, keys ...string) map[string]any { + res := map[string]any{} omit := make(map[string]bool, len(keys)) for _, k := range keys { @@ -68,8 +68,8 @@ func omit(dict map[string]interface{}, keys ...string) map[string]interface{} { return res } -func dict(v ...interface{}) map[string]interface{} { - dict := map[string]interface{}{} +func dict(v ...any) map[string]any { + dict := map[string]any{} lenv := len(v) for i := 0; i < lenv; i += 2 { key := strval(v[i]) @@ -82,20 +82,19 @@ func dict(v ...interface{}) map[string]interface{} { return dict } -func values(dict map[string]interface{}) []interface{} { - values := []interface{}{} +func values(dict map[string]any) []any { + var values []any for _, value := range dict { values = append(values, value) } - return values } -func dig(ps ...interface{}) (interface{}, error) { +func dig(ps ...any) (any, error) { if len(ps) < 3 { panic("dig needs at least three arguments") } - dict := ps[len(ps)-1].(map[string]interface{}) + dict := ps[len(ps)-1].(map[string]any) def := ps[len(ps)-2] ks := make([]string, len(ps)-2) for i := 0; i < len(ks); i++ { @@ -105,7 +104,7 @@ func dig(ps ...interface{}) (interface{}, error) { return digFromDict(dict, def, ks) } -func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) { +func digFromDict(dict map[string]any, d any, ks []string) (any, error) { k, ns := ks[0], ks[1:] step, has := dict[k] if !has { @@ -114,5 +113,5 @@ func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (inter if len(ns) == 0 { return step, nil } - return digFromDict(step.(map[string]interface{}), d, ns) + return digFromDict(step.(map[string]any), d, ns) } diff --git a/util/sprig/example_test.go b/util/sprig/example_test.go index 2d7696bf..2f1b74c8 100644 --- a/util/sprig/example_test.go +++ b/util/sprig/example_test.go @@ -8,7 +8,7 @@ import ( func Example() { // Set up variables and template. - vars := map[string]interface{}{"Name": " John Jacob Jingleheimer Schmidt "} + vars := map[string]any{"Name": " John Jacob Jingleheimer Schmidt "} tpl := `Hello {{.Name | trim | lower}}` // Get the Sprig function map. diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 3ea46924..68ef516d 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -24,68 +24,26 @@ func FuncMap() template.FuncMap { return HTMLFuncMap() } -// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. -func HermeticTxtFuncMap() ttemplate.FuncMap { - r := TxtFuncMap() - for _, name := range nonhermeticFunctions { - delete(r, name) - } - return r -} - -// HermeticHTMLFuncMap returns an 'html/template'.Funcmap with only repeatable functions. -func HermeticHTMLFuncMap() template.FuncMap { - r := HTMLFuncMap() - for _, name := range nonhermeticFunctions { - delete(r, name) - } - return r -} - // TxtFuncMap returns a 'text/template'.FuncMap func TxtFuncMap() ttemplate.FuncMap { - return ttemplate.FuncMap(GenericFuncMap()) + return GenericFuncMap() } // HTMLFuncMap returns an 'html/template'.Funcmap func HTMLFuncMap() template.FuncMap { - return template.FuncMap(GenericFuncMap()) + return GenericFuncMap() } -// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. -func GenericFuncMap() map[string]interface{} { - gfm := make(map[string]interface{}, len(genericMap)) +// GenericFuncMap returns a copy of the basic function map as a map[string]any. +func GenericFuncMap() map[string]any { + gfm := make(map[string]any, len(genericMap)) for k, v := range genericMap { gfm[k] = v } return gfm } -// These functions are not guaranteed to evaluate to the same result for given input, because they -// refer to the environment or global state. -var nonhermeticFunctions = []string{ - // Date functions - "date", - "date_in_zone", - "date_modify", - "now", - "htmlDate", - "htmlDateInZone", - "dateInZone", - "dateModify", - - // Strings - "randAlphaNum", - "randAlpha", - "randAscii", - "randNumeric", - "randBytes", - "uuidv4", -} - -var genericMap = map[string]interface{}{ - "hello": func() string { return "Hello!" }, - +var genericMap = map[string]any{ // Date functions "ago": dateAgo, "date": date, @@ -157,18 +115,18 @@ var genericMap = map[string]interface{}{ "untilStep": untilStep, // VERY basic arithmetic. - "add1": func(i interface{}) int64 { return toInt64(i) + 1 }, - "add": func(i ...interface{}) int64 { + "add1": func(i any) int64 { return toInt64(i) + 1 }, + "add": func(i ...any) int64 { var a int64 = 0 for _, b := range i { a += toInt64(b) } return a }, - "sub": func(a, b interface{}) int64 { return toInt64(a) - toInt64(b) }, - "div": func(a, b interface{}) int64 { return toInt64(a) / toInt64(b) }, - "mod": func(a, b interface{}) int64 { return toInt64(a) % toInt64(b) }, - "mul": func(a interface{}, v ...interface{}) int64 { + "sub": func(a, b any) int64 { return toInt64(a) - toInt64(b) }, + "div": func(a, b any) int64 { return toInt64(a) / toInt64(b) }, + "mod": func(a, b any) int64 { return toInt64(a) % toInt64(b) }, + "mul": func(a any, v ...any) int64 { val := toInt64(a) for _, b := range v { val = val * toInt64(b) @@ -195,7 +153,7 @@ var genericMap = map[string]interface{}{ "empty": empty, "coalesce": coalesce, "all": all, - "any": any, + "any": anyNonEmpty, "compact": compact, "mustCompact": mustCompact, "fromJSON": fromJSON, @@ -250,8 +208,10 @@ var genericMap = map[string]interface{}{ "omit": omit, "values": values, - "append": push, "push": push, - "mustAppend": mustPush, "mustPush": mustPush, + "append": push, + "push": push, + "mustAppend": mustPush, + "mustPush": mustPush, "prepend": prepend, "mustPrepend": mustPrepend, "first": first, diff --git a/util/sprig/functions_test.go b/util/sprig/functions_test.go index b7bc01f4..e5989b98 100644 --- a/util/sprig/functions_test.go +++ b/util/sprig/functions_test.go @@ -43,7 +43,7 @@ func runt(tpl, expect string) error { // runtv takes a template, and expected return, and values for substitution. // // It runs the template and verifies that the output is an exact match. -func runtv(tpl, expect string, vars interface{}) error { +func runtv(tpl, expect string, vars any) error { fmap := TxtFuncMap() t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) var b bytes.Buffer @@ -58,7 +58,7 @@ func runtv(tpl, expect string, vars interface{}) error { } // runRaw runs a template with the given variables and returns the result. -func runRaw(tpl string, vars interface{}) (string, error) { +func runRaw(tpl string, vars any) (string, error) { fmap := TxtFuncMap() t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) var b bytes.Buffer diff --git a/util/sprig/list.go b/util/sprig/list.go index f4e95dda..138ecfa5 100644 --- a/util/sprig/list.go +++ b/util/sprig/list.go @@ -8,14 +8,14 @@ import ( ) // Reflection is used in these functions so that slices and arrays of strings, -// ints, and other types not implementing []interface{} can be worked with. +// ints, and other types not implementing []any can be worked with. // For example, this is useful if you need to work on the output of regexs. -func list(v ...interface{}) []interface{} { +func list(v ...any) []any { return v } -func push(list interface{}, v interface{}) []interface{} { +func push(list any, v any) []any { l, err := mustPush(list, v) if err != nil { panic(err) @@ -24,14 +24,14 @@ func push(list interface{}, v interface{}) []interface{} { return l } -func mustPush(list interface{}, v interface{}) ([]interface{}, error) { +func mustPush(list any, v any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) l := l2.Len() - nl := make([]interface{}, l) + nl := make([]any, l) for i := 0; i < l; i++ { nl[i] = l2.Index(i).Interface() } @@ -43,7 +43,7 @@ func mustPush(list interface{}, v interface{}) ([]interface{}, error) { } } -func prepend(list interface{}, v interface{}) []interface{} { +func prepend(list any, v any) []any { l, err := mustPrepend(list, v) if err != nil { panic(err) @@ -52,8 +52,8 @@ func prepend(list interface{}, v interface{}) []interface{} { return l } -func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { - //return append([]interface{}{v}, list...) +func mustPrepend(list any, v any) ([]any, error) { + //return append([]any{v}, list...) tp := reflect.TypeOf(list).Kind() switch tp { @@ -61,19 +61,19 @@ func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { l2 := reflect.ValueOf(list) l := l2.Len() - nl := make([]interface{}, l) + nl := make([]any, l) for i := 0; i < l; i++ { nl[i] = l2.Index(i).Interface() } - return append([]interface{}{v}, nl...), nil + return append([]any{v}, nl...), nil default: return nil, fmt.Errorf("cannot prepend on type %s", tp) } } -func chunk(size int, list interface{}) [][]interface{} { +func chunk(size int, list any) [][]any { l, err := mustChunk(size, list) if err != nil { panic(err) @@ -82,7 +82,7 @@ func chunk(size int, list interface{}) [][]interface{} { return l } -func mustChunk(size int, list interface{}) ([][]interface{}, error) { +func mustChunk(size int, list any) ([][]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -91,7 +91,7 @@ func mustChunk(size int, list interface{}) ([][]interface{}, error) { l := l2.Len() cs := int(math.Floor(float64(l-1)/float64(size)) + 1) - nl := make([][]interface{}, cs) + nl := make([][]any, cs) for i := 0; i < cs; i++ { clen := size @@ -102,7 +102,7 @@ func mustChunk(size int, list interface{}) ([][]interface{}, error) { } } - nl[i] = make([]interface{}, clen) + nl[i] = make([]any, clen) for j := 0; j < clen; j++ { ix := i*size + j @@ -117,7 +117,7 @@ func mustChunk(size int, list interface{}) ([][]interface{}, error) { } } -func last(list interface{}) interface{} { +func last(list any) any { l, err := mustLast(list) if err != nil { panic(err) @@ -126,7 +126,7 @@ func last(list interface{}) interface{} { return l } -func mustLast(list interface{}) (interface{}, error) { +func mustLast(list any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -143,7 +143,7 @@ func mustLast(list interface{}) (interface{}, error) { } } -func first(list interface{}) interface{} { +func first(list any) any { l, err := mustFirst(list) if err != nil { panic(err) @@ -152,7 +152,7 @@ func first(list interface{}) interface{} { return l } -func mustFirst(list interface{}) (interface{}, error) { +func mustFirst(list any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -169,7 +169,7 @@ func mustFirst(list interface{}) (interface{}, error) { } } -func rest(list interface{}) []interface{} { +func rest(list any) []any { l, err := mustRest(list) if err != nil { panic(err) @@ -178,7 +178,7 @@ func rest(list interface{}) []interface{} { return l } -func mustRest(list interface{}) ([]interface{}, error) { +func mustRest(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -189,7 +189,7 @@ func mustRest(list interface{}) ([]interface{}, error) { return nil, nil } - nl := make([]interface{}, l-1) + nl := make([]any, l-1) for i := 1; i < l; i++ { nl[i-1] = l2.Index(i).Interface() } @@ -200,7 +200,7 @@ func mustRest(list interface{}) ([]interface{}, error) { } } -func initial(list interface{}) []interface{} { +func initial(list any) []any { l, err := mustInitial(list) if err != nil { panic(err) @@ -209,7 +209,7 @@ func initial(list interface{}) []interface{} { return l } -func mustInitial(list interface{}) ([]interface{}, error) { +func mustInitial(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -220,7 +220,7 @@ func mustInitial(list interface{}) ([]interface{}, error) { return nil, nil } - nl := make([]interface{}, l-1) + nl := make([]any, l-1) for i := 0; i < l-1; i++ { nl[i] = l2.Index(i).Interface() } @@ -231,7 +231,7 @@ func mustInitial(list interface{}) ([]interface{}, error) { } } -func sortAlpha(list interface{}) []string { +func sortAlpha(list any) []string { k := reflect.Indirect(reflect.ValueOf(list)).Kind() switch k { case reflect.Slice, reflect.Array: @@ -243,7 +243,7 @@ func sortAlpha(list interface{}) []string { return []string{strval(list)} } -func reverse(v interface{}) []interface{} { +func reverse(v any) []any { l, err := mustReverse(v) if err != nil { panic(err) @@ -252,7 +252,7 @@ func reverse(v interface{}) []interface{} { return l } -func mustReverse(v interface{}) ([]interface{}, error) { +func mustReverse(v any) ([]any, error) { tp := reflect.TypeOf(v).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -260,7 +260,7 @@ func mustReverse(v interface{}) ([]interface{}, error) { l := l2.Len() // We do not sort in place because the incoming array should not be altered. - nl := make([]interface{}, l) + nl := make([]any, l) for i := 0; i < l; i++ { nl[l-i-1] = l2.Index(i).Interface() } @@ -271,7 +271,7 @@ func mustReverse(v interface{}) ([]interface{}, error) { } } -func compact(list interface{}) []interface{} { +func compact(list any) []any { l, err := mustCompact(list) if err != nil { panic(err) @@ -280,15 +280,15 @@ func compact(list interface{}) []interface{} { return l } -func mustCompact(list interface{}) ([]interface{}, error) { +func mustCompact(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) l := l2.Len() - nl := []interface{}{} - var item interface{} + nl := []any{} + var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() if !empty(item) { @@ -302,7 +302,7 @@ func mustCompact(list interface{}) ([]interface{}, error) { } } -func uniq(list interface{}) []interface{} { +func uniq(list any) []any { l, err := mustUniq(list) if err != nil { panic(err) @@ -311,15 +311,15 @@ func uniq(list interface{}) []interface{} { return l } -func mustUniq(list interface{}) ([]interface{}, error) { +func mustUniq(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) l := l2.Len() - dest := []interface{}{} - var item interface{} + dest := []any{} + var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() if !inList(dest, item) { @@ -333,7 +333,7 @@ func mustUniq(list interface{}) ([]interface{}, error) { } } -func inList(haystack []interface{}, needle interface{}) bool { +func inList(haystack []any, needle any) bool { for _, h := range haystack { if reflect.DeepEqual(needle, h) { return true @@ -342,7 +342,7 @@ func inList(haystack []interface{}, needle interface{}) bool { return false } -func without(list interface{}, omit ...interface{}) []interface{} { +func without(list any, omit ...any) []any { l, err := mustWithout(list, omit...) if err != nil { panic(err) @@ -351,15 +351,15 @@ func without(list interface{}, omit ...interface{}) []interface{} { return l } -func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { +func mustWithout(list any, omit ...any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) l := l2.Len() - res := []interface{}{} - var item interface{} + res := []any{} + var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() if !inList(omit, item) { @@ -373,7 +373,7 @@ func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { } } -func has(needle interface{}, haystack interface{}) bool { +func has(needle any, haystack any) bool { l, err := mustHas(needle, haystack) if err != nil { panic(err) @@ -382,7 +382,7 @@ func has(needle interface{}, haystack interface{}) bool { return l } -func mustHas(needle interface{}, haystack interface{}) (bool, error) { +func mustHas(needle any, haystack any) (bool, error) { if haystack == nil { return false, nil } @@ -390,7 +390,7 @@ func mustHas(needle interface{}, haystack interface{}) (bool, error) { switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(haystack) - var item interface{} + var item any l := l2.Len() for i := 0; i < l; i++ { item = l2.Index(i).Interface() @@ -410,7 +410,7 @@ func mustHas(needle interface{}, haystack interface{}) (bool, error) { // slice $list 0 3 -> list[0:3] = list[:3] // slice $list 3 5 -> list[3:5] // slice $list 3 -> list[3:5] = list[3:] -func slice(list interface{}, indices ...interface{}) interface{} { +func slice(list any, indices ...any) any { l, err := mustSlice(list, indices...) if err != nil { panic(err) @@ -419,7 +419,7 @@ func slice(list interface{}, indices ...interface{}) interface{} { return l } -func mustSlice(list interface{}, indices ...interface{}) (interface{}, error) { +func mustSlice(list any, indices ...any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: @@ -446,8 +446,8 @@ func mustSlice(list interface{}, indices ...interface{}) (interface{}, error) { } } -func concat(lists ...interface{}) interface{} { - var res []interface{} +func concat(lists ...any) any { + var res []any for _, list := range lists { tp := reflect.TypeOf(list).Kind() switch tp { diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go index 0b23cd21..e41f61f5 100644 --- a/util/sprig/numeric.go +++ b/util/sprig/numeric.go @@ -9,7 +9,7 @@ import ( ) // toFloat64 converts 64-bit floats -func toFloat64(v interface{}) float64 { +func toFloat64(v any) float64 { if str, ok := v.(string); ok { iv, err := strconv.ParseFloat(str, 64) if err != nil { @@ -38,13 +38,13 @@ func toFloat64(v interface{}) float64 { } } -func toInt(v interface{}) int { +func toInt(v any) int { // It's not optimal. But I don't want duplicate toInt64 code. return int(toInt64(v)) } // toInt64 converts integer types to 64-bit integers -func toInt64(v interface{}) int64 { +func toInt64(v any) int64 { if str, ok := v.(string); ok { iv, err := strconv.ParseInt(str, 10, 64) if err != nil { @@ -78,7 +78,7 @@ func toInt64(v interface{}) int64 { } } -func max(a interface{}, i ...interface{}) int64 { +func max(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { bb := toInt64(b) @@ -89,7 +89,7 @@ func max(a interface{}, i ...interface{}) int64 { return aa } -func maxf(a interface{}, i ...interface{}) float64 { +func maxf(a any, i ...any) float64 { aa := toFloat64(a) for _, b := range i { bb := toFloat64(b) @@ -98,7 +98,7 @@ func maxf(a interface{}, i ...interface{}) float64 { return aa } -func min(a interface{}, i ...interface{}) int64 { +func min(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { bb := toInt64(b) @@ -109,7 +109,7 @@ func min(a interface{}, i ...interface{}) int64 { return aa } -func minf(a interface{}, i ...interface{}) float64 { +func minf(a any, i ...any) float64 { aa := toFloat64(a) for _, b := range i { bb := toFloat64(b) @@ -148,17 +148,17 @@ func untilStep(start, stop, step int) []int { return v } -func floor(a interface{}) float64 { +func floor(a any) float64 { aa := toFloat64(a) return math.Floor(aa) } -func ceil(a interface{}) float64 { +func ceil(a any) float64 { aa := toFloat64(a) return math.Ceil(aa) } -func round(a interface{}, p int, rOpt ...float64) float64 { +func round(a any, p int, rOpt ...float64) float64 { roundOn := .5 if len(rOpt) > 0 { roundOn = rOpt[0] @@ -179,7 +179,7 @@ func round(a interface{}, p int, rOpt ...float64) float64 { } // converts unix octal to decimal -func toDecimal(v interface{}) int64 { +func toDecimal(v any) int64 { result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) if err != nil { return 0 diff --git a/util/sprig/numeric_test.go b/util/sprig/numeric_test.go index 573873d8..63310c52 100644 --- a/util/sprig/numeric_test.go +++ b/util/sprig/numeric_test.go @@ -192,7 +192,7 @@ func TestToInt(t *testing.T) { } func TestToDecimal(t *testing.T) { - tests := map[interface{}]int64{ + tests := map[any]int64{ "777": 511, 777: 511, 770: 504, diff --git a/util/sprig/reflect.go b/util/sprig/reflect.go index 8a65c132..5e37f64f 100644 --- a/util/sprig/reflect.go +++ b/util/sprig/reflect.go @@ -6,23 +6,23 @@ import ( ) // typeIs returns true if the src is the type named in target. -func typeIs(target string, src interface{}) bool { +func typeIs(target string, src any) bool { return target == typeOf(src) } -func typeIsLike(target string, src interface{}) bool { +func typeIsLike(target string, src any) bool { t := typeOf(src) return target == t || "*"+target == t } -func typeOf(src interface{}) string { +func typeOf(src any) string { return fmt.Sprintf("%T", src) } -func kindIs(target string, src interface{}) bool { +func kindIs(target string, src any) bool { return target == kindOf(src) } -func kindOf(src interface{}) string { +func kindOf(src any) string { return reflect.ValueOf(src).Kind().String() } diff --git a/util/sprig/strings.go b/util/sprig/strings.go index 3c62d6b6..911aa6f4 100644 --- a/util/sprig/strings.go +++ b/util/sprig/strings.go @@ -33,7 +33,7 @@ func base32decode(v string) string { return string(data) } -func quote(str ...interface{}) string { +func quote(str ...any) string { out := make([]string, 0, len(str)) for _, s := range str { if s != nil { @@ -43,7 +43,7 @@ func quote(str ...interface{}) string { return strings.Join(out, " ") } -func squote(str ...interface{}) string { +func squote(str ...any) string { out := make([]string, 0, len(str)) for _, s := range str { if s != nil { @@ -53,7 +53,7 @@ func squote(str ...interface{}) string { return strings.Join(out, " ") } -func cat(v ...interface{}) string { +func cat(v ...any) string { v = removeNilElements(v) r := strings.TrimSpace(strings.Repeat("%v ", len(v))) return fmt.Sprintf(r, v...) @@ -79,11 +79,11 @@ func plural(one, many string, count int) string { return many } -func strslice(v interface{}) []string { +func strslice(v any) []string { switch v := v.(type) { case []string: return v - case []interface{}: + case []any: b := make([]string, 0, len(v)) for _, s := range v { if s != nil { @@ -114,8 +114,8 @@ func strslice(v interface{}) []string { } } -func removeNilElements(v []interface{}) []interface{} { - newSlice := make([]interface{}, 0, len(v)) +func removeNilElements(v []any) []any { + newSlice := make([]any, 0, len(v)) for _, i := range v { if i != nil { newSlice = append(newSlice, i) @@ -124,7 +124,7 @@ func removeNilElements(v []interface{}) []interface{} { return newSlice } -func strval(v interface{}) string { +func strval(v any) string { switch v := v.(type) { case string: return v @@ -149,7 +149,7 @@ func trunc(c int, s string) string { return s } -func join(sep string, v interface{}) string { +func join(sep string, v any) string { return strings.Join(strslice(v), sep) } diff --git a/util/sprig/strings_test.go b/util/sprig/strings_test.go index 38c96c4e..1e91d9b2 100644 --- a/util/sprig/strings_test.go +++ b/util/sprig/strings_test.go @@ -56,7 +56,7 @@ func TestQuote(t *testing.T) { t.Error(err) } tpl = `{{ .value | quote }}` - values := map[string]interface{}{"value": nil} + values := map[string]any{"value": nil} if err := runtv(tpl, ``, values); err != nil { t.Error(err) } @@ -71,7 +71,7 @@ func TestSquote(t *testing.T) { t.Error(err) } tpl = `{{ .value | squote }}` - values := map[string]interface{}{"value": nil} + values := map[string]any{"value": nil} if err := runtv(tpl, ``, values); err != nil { t.Error(err) } @@ -128,7 +128,7 @@ func TestToStrings(t *testing.T) { tpl := `{{ $s := list 1 2 3 | toStrings }}{{ index $s 1 | kindOf }}` assert.NoError(t, runt(tpl, "string")) tpl = `{{ list 1 .value 2 | toStrings }}` - values := map[string]interface{}{"value": nil} + values := map[string]any{"value": nil} if err := runtv(tpl, `[1 2]`, values); err != nil { t.Error(err) } @@ -137,10 +137,10 @@ func TestToStrings(t *testing.T) { func TestJoin(t *testing.T) { assert.NoError(t, runt(`{{ tuple "a" "b" "c" | join "-" }}`, "a-b-c")) assert.NoError(t, runt(`{{ tuple 1 2 3 | join "-" }}`, "1-2-3")) - assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]interface{}{"V": []string{"a", "b", "c"}})) - assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]interface{}{"V": "abc"})) - assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]interface{}{"V": []int{1, 2, 3}})) - assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]interface{}{"value": []interface{}{"1", nil, "2"}})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "a-b-c", map[string]any{"V": []string{"a", "b", "c"}})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "abc", map[string]any{"V": "abc"})) + assert.NoError(t, runtv(`{{ join "-" .V }}`, "1-2-3", map[string]any{"V": []int{1, 2, 3}})) + assert.NoError(t, runtv(`{{ join "-" .value }}`, "1-2", map[string]any{"value": []any{"1", nil, "2"}})) } func TestSortAlpha(t *testing.T) { @@ -194,7 +194,7 @@ func TestCat(t *testing.T) { t.Error(err) } tpl = `{{ .value | cat "a" "b"}}` - values := map[string]interface{}{"value": nil} + values := map[string]any{"value": nil} if err := runtv(tpl, "a b", values); err != nil { t.Error(err) } diff --git a/util/sprig/url.go b/util/sprig/url.go index b8e120e1..00826706 100644 --- a/util/sprig/url.go +++ b/util/sprig/url.go @@ -6,7 +6,7 @@ import ( "reflect" ) -func dictGetOrEmpty(dict map[string]interface{}, key string) string { +func dictGetOrEmpty(dict map[string]any, key string) string { value, ok := dict[key] if !ok { return "" @@ -19,8 +19,8 @@ func dictGetOrEmpty(dict map[string]interface{}, key string) string { } // parses given URL to return dict object -func urlParse(v string) map[string]interface{} { - dict := map[string]interface{}{} +func urlParse(v string) map[string]any { + dict := map[string]any{} parsedURL, err := url.Parse(v) if err != nil { panic(fmt.Sprintf("unable to parse url: %s", err)) @@ -42,7 +42,7 @@ func urlParse(v string) map[string]interface{} { } // join given dict to URL string -func urlJoin(d map[string]interface{}) string { +func urlJoin(d map[string]any) string { resURL := url.URL{ Scheme: dictGetOrEmpty(d, "scheme"), Host: dictGetOrEmpty(d, "host"), diff --git a/util/sprig/url_test.go b/util/sprig/url_test.go index f9c00b17..16d457a7 100644 --- a/util/sprig/url_test.go +++ b/util/sprig/url_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -var urlTests = map[string]map[string]interface{}{ +var urlTests = map[string]map[string]any{ "proto://auth@host:80/path?query#fragment": { "fragment": "fragment", "host": "host:80", From 8bf4727a1c7f0ace5fcbb943cc1216badc58709b Mon Sep 17 00:00:00 2001 From: Kristopher Paulsen Date: Sun, 13 Jul 2025 09:50:06 -0400 Subject: [PATCH 171/378] Missing double quote, sneaky little bugger --- docs/subscribe/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 78e160c8..36388c71 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -156,7 +156,7 @@ environment variables. Here are a few examples: ``` ntfy sub mytopic 'notify-send "$m"' ntfy sub topic1 /my/script.sh -ntfy sub topic1 'echo "Message $m was received. Its title was $t and it had priority $p' +ntfy sub topic1 'echo "Message $m was received. Its title was $t and it had priority $p"' ```

From 93e14b73bbab56f688099cde385969195e8df132 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 16 Jul 2025 10:01:59 +0200 Subject: [PATCH 172/378] Tempalte dir --- cmd/serve.go | 3 +++ server/config.go | 2 ++ server/server.go | 60 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index ef4d98d5..d762a7c6 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -107,6 +107,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "template-directory", Aliases: []string{"template_directory"}, EnvVars: []string{"NTFY_TEMPLATE_DIRECTORY"}, Usage: "directory to load named templates from"}), ) var cmdServe = &cli.Command{ @@ -205,6 +206,7 @@ func execServe(c *cli.Context) error { metricsListenHTTP := c.String("metrics-listen-http") enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != "" profileListenHTTP := c.String("profile-listen-http") + templateDirectory := c.String("template-directory") // Convert durations cacheDuration, err := util.ParseDuration(cacheDurationStr) @@ -461,6 +463,7 @@ func execServe(c *cli.Context) error { conf.WebPushStartupQueries = webPushStartupQueries conf.WebPushExpiryDuration = webPushExpiryDuration conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration + conf.TemplateDirectory = templateDirectory conf.Version = c.App.Version // Set up hot-reloading of config diff --git a/server/config.go b/server/config.go index 59b11c16..46848fe5 100644 --- a/server/config.go +++ b/server/config.go @@ -167,6 +167,7 @@ type Config struct { WebPushExpiryDuration time.Duration WebPushExpiryWarningDuration time.Duration Version string // injected by App + TemplateDirectory string // Directory to load named templates from } // NewConfig instantiates a default new server config @@ -257,5 +258,6 @@ func NewConfig() *Config { WebPushEmailAddress: "", WebPushExpiryDuration: DefaultWebPushExpiryDuration, WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration, + TemplateDirectory: "", } } diff --git a/server/server.go b/server/server.go index 7e5fbb94..51c56f3e 100644 --- a/server/server.go +++ b/server/server.go @@ -62,6 +62,7 @@ type Server struct { metricsHandler http.Handler // Handles /metrics if enable-metrics set, and listen-metrics-http not set closeChan chan bool mu sync.RWMutex + templates map[string]*template.Template // Loaded named templates } // handleFunc extends the normal http.HandlerFunc to be able to easily return errors @@ -222,8 +223,16 @@ func New(conf *Config) (*Server, error) { messagesHistory: []int64{messages}, visitors: make(map[string]*visitor), stripe: stripe, + templates: make(map[string]*template.Template), } s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration) + if conf.TemplateDirectory != "" { + tmpls, err := loadTemplatesFromDir(conf.TemplateDirectory) + if err != nil { + return nil, fmt.Errorf("failed to load templates from %s: %w", conf.TemplateDirectory, err) + } + s.templates = tmpls + } return s, nil } @@ -1113,10 +1122,10 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR return errHTTPEntityTooLargeJSONBody } peekedBody := strings.TrimSpace(string(body.PeekedBytes)) - if m.Message, err = replaceTemplate(m.Message, peekedBody); err != nil { + if m.Message, err = s.replaceTemplate(m.Message, peekedBody); err != nil { return err } - if m.Title, err = replaceTemplate(m.Title, peekedBody); err != nil { + if m.Title, err = s.replaceTemplate(m.Title, peekedBody); err != nil { return err } if len(m.Message) > s.config.MessageSizeLimit { @@ -1125,10 +1134,26 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR return nil } -func replaceTemplate(tpl string, source string) (string, error) { +func (s *Server) replaceTemplate(tpl string, source string) (string, error) { if templateDisallowedRegex.MatchString(tpl) { return "", errHTTPBadRequestTemplateDisallowedFunctionCalls } + if strings.HasPrefix(tpl, "@") { + name := strings.TrimPrefix(tpl, "@") + t, ok := s.templates[name] + if !ok { + return "", fmt.Errorf("template '@%s' not found", name) + } + var data any + if err := json.Unmarshal([]byte(source), &data); err != nil { + return "", errHTTPBadRequestTemplateMessageNotJSON + } + var buf bytes.Buffer + if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil { + return "", errHTTPBadRequestTemplateExecuteFailed + } + return buf.String(), nil + } var data any if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON @@ -2061,3 +2086,32 @@ func (s *Server) updateAndWriteStats(messagesCount int64) { } }() } + +func loadTemplatesFromDir(dir string) (map[string]*template.Template, error) { + templates := make(map[string]*template.Template) + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".tmpl") { + continue + } + path := filepath.Join(dir, name) + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read template %s: %w", name, err) + } + tmpl, err := template.New(name).Funcs(sprig.FuncMap()).Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to parse template %s: %w", name, err) + } + base := strings.TrimSuffix(name, ".tmpl") + templates[base] = tmpl + } + return templates, nil +} From b1e935da45365c5e7e731d544a1ad4c7ea3643cd Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 16 Jul 2025 13:49:15 +0200 Subject: [PATCH 173/378] TEmplate dir --- cmd/serve.go | 6 +- server/config.go | 4 +- server/errors.go | 3 + server/server.go | 143 ++++++++++++++++++++--------------- server/templates/github.yml | 23 ++++++ server/templates/grafana.yml | 9 +++ server/types.go | 19 ++++- 7 files changed, 142 insertions(+), 65 deletions(-) create mode 100644 server/templates/github.yml create mode 100644 server/templates/grafana.yml diff --git a/cmd/serve.go b/cmd/serve.go index d762a7c6..0cbade0f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -56,6 +56,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "template-dir", Aliases: []string{"template_dir"}, EnvVars: []string{"NTFY_TEMPLATE_DIR"}, Usage: "directory to load named message templates from"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}), @@ -107,7 +108,6 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "template-directory", Aliases: []string{"template_directory"}, EnvVars: []string{"NTFY_TEMPLATE_DIRECTORY"}, Usage: "directory to load named templates from"}), ) var cmdServe = &cli.Command{ @@ -162,6 +162,7 @@ func execServe(c *cli.Context) error { attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") attachmentExpiryDurationStr := c.String("attachment-expiry-duration") + templateDir := c.String("template-dir") keepaliveIntervalStr := c.String("keepalive-interval") managerIntervalStr := c.String("manager-interval") disallowedTopics := c.StringSlice("disallowed-topics") @@ -206,7 +207,6 @@ func execServe(c *cli.Context) error { metricsListenHTTP := c.String("metrics-listen-http") enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != "" profileListenHTTP := c.String("profile-listen-http") - templateDirectory := c.String("template-directory") // Convert durations cacheDuration, err := util.ParseDuration(cacheDurationStr) @@ -412,6 +412,7 @@ func execServe(c *cli.Context) error { conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit conf.AttachmentExpiryDuration = attachmentExpiryDuration + conf.TemplateDir = templateDir conf.KeepaliveInterval = keepaliveInterval conf.ManagerInterval = managerInterval conf.DisallowedTopics = disallowedTopics @@ -463,7 +464,6 @@ func execServe(c *cli.Context) error { conf.WebPushStartupQueries = webPushStartupQueries conf.WebPushExpiryDuration = webPushExpiryDuration conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration - conf.TemplateDirectory = templateDirectory conf.Version = c.App.Version // Set up hot-reloading of config diff --git a/server/config.go b/server/config.go index 46848fe5..c5560010 100644 --- a/server/config.go +++ b/server/config.go @@ -99,6 +99,7 @@ type Config struct { AttachmentTotalSizeLimit int64 AttachmentFileSizeLimit int64 AttachmentExpiryDuration time.Duration + TemplateDir string // Directory to load named templates from KeepaliveInterval time.Duration ManagerInterval time.Duration DisallowedTopics []string @@ -167,7 +168,6 @@ type Config struct { WebPushExpiryDuration time.Duration WebPushExpiryWarningDuration time.Duration Version string // injected by App - TemplateDirectory string // Directory to load named templates from } // NewConfig instantiates a default new server config @@ -258,6 +258,6 @@ func NewConfig() *Config { WebPushEmailAddress: "", WebPushExpiryDuration: DefaultWebPushExpiryDuration, WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration, - TemplateDirectory: "", + TemplateDir: "", } } diff --git a/server/errors.go b/server/errors.go index c6076f3f..fa504410 100644 --- a/server/errors.go +++ b/server/errors.go @@ -123,6 +123,9 @@ var ( errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} + errHTTPBadRequestTemplateDirectoryNotConfigured = &errHTTP{40046, http.StatusBadRequest, "invalid request: template directory not configured", "https://ntfy.sh/docs/publish/#message-templating", nil} + errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} + errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/server.go b/server/server.go index 51c56f3e..c6991ba8 100644 --- a/server/server.go +++ b/server/server.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "gopkg.in/yaml.v2" "io" "net" "net/http" @@ -56,13 +57,12 @@ type Server struct { userManager *user.Manager // Might be nil! messageCache *messageCache // Database that stores the messages webPush *webPushStore // Database that stores web push subscriptions - fileCache *fileCache // File system based cache that stores attachments + fileCache *fileCache // Name system based cache that stores attachments stripe stripeAPI // Stripe API, can be replaced with a mock priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!) metricsHandler http.Handler // Handles /metrics if enable-metrics set, and listen-metrics-http not set closeChan chan bool mu sync.RWMutex - templates map[string]*template.Template // Loaded named templates } // handleFunc extends the normal http.HandlerFunc to be able to easily return errors @@ -122,6 +122,15 @@ var ( //go:embed docs docsStaticFs embed.FS docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs} + + //go:embed templates + templatesFs embed.FS // Contains template config files (e.g. grafana.yml, github.yml, ...) + templatesDir = "templates" + + // templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they + // are not useful, and seem potentially troublesome. + templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`) + templateNameRegex = regexp.MustCompile(`^[-_A-Za-z0-9]+$`) ) const ( @@ -131,17 +140,12 @@ const ( newMessageBody = "New message" // Used in poll requests as generic message defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages - jsonBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher) + jsonBodyBytesLimit = 131072 // Max number of bytes for a request bodys (unless MessageLimit is higher) unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part messagesHistoryMax = 10 // Number of message count values to keep in memory - templateMaxExecutionTime = 100 * time.Millisecond -) - -var ( - // templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they - // are not useful, and seem potentially troublesome. - templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`) + templateMaxExecutionTime = 100 * time.Millisecond // Maximum time a template can take to execute, used to prevent DoS attacks + templateFileExtension = ".yml" // Template files must end with this extension ) // WebSocket constants @@ -223,16 +227,8 @@ func New(conf *Config) (*Server, error) { messagesHistory: []int64{messages}, visitors: make(map[string]*visitor), stripe: stripe, - templates: make(map[string]*template.Template), } s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration) - if conf.TemplateDirectory != "" { - tmpls, err := loadTemplatesFromDir(conf.TemplateDirectory) - if err != nil { - return nil, fmt.Errorf("failed to load templates from %s: %w", conf.TemplateDirectory, err) - } - s.templates = tmpls - } return s, nil } @@ -946,7 +942,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } } -func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) { cache = readBoolParam(r, true, "x-cache", "cache") firebase = readBoolParam(r, true, "x-firebase", "firebase") m.Title = readParam(r, "x-title", "title", "t") @@ -962,7 +958,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -980,19 +976,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid + return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid } m.Icon = icon } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") if s.smtpSender == nil && email != "" { - return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled + return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled } call = readParam(r, "x-call", "call") if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { - return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled + return false, false, "", "", "", false, errHTTPBadRequestPhoneCallsDisabled } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { - return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { @@ -1001,27 +997,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi var e error m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) if e != nil { - return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid + return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid } m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") if delayStr != "" { if !cache { - return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache + return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache } if email != "" { - return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + return false, false, "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) } if call != "" { - return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) + return false, false, "", "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) } delay, err := util.ParseFutureTime(delayStr, time.Now()) if err != nil { - return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse + return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { - return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", "", false, errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { - return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", "", false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -1029,14 +1025,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) + return false, false, "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) } } contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") if markdown || strings.ToLower(contentType) == "text/markdown" { m.ContentType = "text/markdown" } - template = readBoolParam(r, false, "x-template", "template", "tpl") + template = templateMode(readParam(r, "x-template", "template", "tpl")) unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! contentEncoding := readParam(r, "content-encoding") if unifiedpush || contentEncoding == "aes128gcm" { @@ -1068,7 +1064,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message // 7. curl -T file.txt ntfy.sh/mytopic // In all other cases, mostly if file.txt is > message limit, treat it as an attachment -func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error { +func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template templateMode, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) } else if unifiedpush { @@ -1077,8 +1073,8 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body return s.handleBodyAsTextMessage(m, body) // Case 3 } else if m.Attachment != nil && m.Attachment.Name != "" { return s.handleBodyAsAttachment(r, v, m, body) // Case 4 - } else if template { - return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5 + } else if template.Enabled() { + return s.handleBodyAsTemplatedTextMessage(m, template, body) // Case 5 } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { return s.handleBodyAsTextMessage(m, body) // Case 6 } @@ -1114,7 +1110,7 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser return nil } -func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error { +func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser) error { body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit)) if err != nil { return err @@ -1122,15 +1118,60 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR return errHTTPEntityTooLargeJSONBody } peekedBody := strings.TrimSpace(string(body.PeekedBytes)) + if templateName := template.Name(); templateName != "" { + if err := s.replaceTemplateFromFile(m, templateName, peekedBody); err != nil { + return err + } + } else { + if err := s.replaceTemplateFromParams(m, peekedBody); err != nil { + return err + } + } + if len(m.Message) > s.config.MessageSizeLimit { + return errHTTPBadRequestTemplateMessageTooLarge + } + return nil +} + +func (s *Server) replaceTemplateFromFile(m *message, templateName, peekedBody string) error { + if !templateNameRegex.MatchString(templateName) { + return errHTTPBadRequestTemplateFileNotFound + } + templateContent, _ := templatesFs.ReadFile(filepath.Join(templatesDir, templateName+templateFileExtension)) // Read from the embedded filesystem first + if s.config.TemplateDir != "" { + if b, _ := os.ReadFile(filepath.Join(s.config.TemplateDir, templateName+templateFileExtension)); len(b) > 0 { + templateContent = b + } + } + if len(templateContent) == 0 { + return errHTTPBadRequestTemplateFileNotFound + } + var tpl templateFile + if err := yaml.Unmarshal(templateContent, &tpl); err != nil { + return errHTTPBadRequestTemplateFileInvalid + } + var err error + if tpl.Message != nil { + if m.Message, err = s.replaceTemplate(*tpl.Message, peekedBody); err != nil { + return err + } + } + if tpl.Title != nil { + if m.Title, err = s.replaceTemplate(*tpl.Title, peekedBody); err != nil { + return err + } + } + return nil +} + +func (s *Server) replaceTemplateFromParams(m *message, peekedBody string) error { + var err error if m.Message, err = s.replaceTemplate(m.Message, peekedBody); err != nil { return err } if m.Title, err = s.replaceTemplate(m.Title, peekedBody); err != nil { return err } - if len(m.Message) > s.config.MessageSizeLimit { - return errHTTPBadRequestTemplateMessageTooLarge - } return nil } @@ -1138,35 +1179,19 @@ func (s *Server) replaceTemplate(tpl string, source string) (string, error) { if templateDisallowedRegex.MatchString(tpl) { return "", errHTTPBadRequestTemplateDisallowedFunctionCalls } - if strings.HasPrefix(tpl, "@") { - name := strings.TrimPrefix(tpl, "@") - t, ok := s.templates[name] - if !ok { - return "", fmt.Errorf("template '@%s' not found", name) - } - var data any - if err := json.Unmarshal([]byte(source), &data); err != nil { - return "", errHTTPBadRequestTemplateMessageNotJSON - } - var buf bytes.Buffer - if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil { - return "", errHTTPBadRequestTemplateExecuteFailed - } - return buf.String(), nil - } var data any if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON } t, err := template.New("").Funcs(sprig.FuncMap()).Parse(tpl) if err != nil { - return "", errHTTPBadRequestTemplateInvalid + return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error()) } var buf bytes.Buffer if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil { - return "", errHTTPBadRequestTemplateExecuteFailed + return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error()) } - return buf.String(), nil + return strings.TrimSpace(buf.String()), nil } func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error { diff --git a/server/templates/github.yml b/server/templates/github.yml new file mode 100644 index 00000000..54a17e9b --- /dev/null +++ b/server/templates/github.yml @@ -0,0 +1,23 @@ +message: | + {{- if .pull_request }} + 🔀 PR {{ .action }}: #{{ .pull_request.number }} — {{ .pull_request.title }} + 📦 {{ .repository.full_name }} + 👤 {{ .pull_request.user.login }} + 🌿 {{ .pull_request.head.ref }} → {{ .pull_request.base.ref }} + 🔗 {{ .pull_request.html_url }} + 📝 {{ .pull_request.body | default "(no description)" }} + {{- else if and .starred_at (eq .action "created")}} + ⭐ {{ .sender.login }} starred {{ .repository.full_name }} + 📦 {{ .repository.description | default "(no description)" }} + 🔗 {{ .repository.html_url }} + 📅 {{ .starred_at }} + {{- else if and .comment (eq .action "created") }} + 💬 New comment on issue #{{ .issue.number }} — {{ .issue.title }} + 📦 {{ .repository.full_name }} + 👤 {{ .comment.user.login }} + 🔗 {{ .comment.html_url }} + 📝 {{ .comment.body | default "(no comment body)" }} + {{- else }} + {{ fail "Unsupported GitHub event type or action." }} + {{- end }} + diff --git a/server/templates/grafana.yml b/server/templates/grafana.yml new file mode 100644 index 00000000..42a16deb --- /dev/null +++ b/server/templates/grafana.yml @@ -0,0 +1,9 @@ +message: | + {{if .alerts}} + {{.alerts | len}} alert(s) triggered + {{else}} + No alerts triggered. + {{end}} +title: | + ⚠️ Grafana alert: {{.title}} + diff --git a/server/types.go b/server/types.go index 30f5c468..ea6b8615 100644 --- a/server/types.go +++ b/server/types.go @@ -7,7 +7,6 @@ import ( "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/user" - "heckel.io/ntfy/v2/util" ) @@ -246,6 +245,24 @@ func (q *queryFilter) Pass(msg *message) bool { return true } +type templateMode string + +func (t templateMode) Enabled() bool { + return t != "" +} + +func (t templateMode) Name() string { + if isBoolValue(string(t)) { + return "" + } + return string(t) +} + +type templateFile struct { + Title *string `yaml:"title"` + Message *string `yaml:"message"` +} + type apiHealthResponse struct { Healthy bool `json:"healthy"` } From 610792b9024ad7bce364bd797bd78485b56d2923 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 16 Jul 2025 20:33:52 +0200 Subject: [PATCH 174/378] WIP --- docs/publish.md | 68 +- docs/template-functions.md | 1455 +++++++++++++++++ server/server_test.go | 18 + server/templates/github.yml | 51 +- .../webhook_github_comment_created.json | 261 +++ server/testdata/webhook_github_pr_opened.json | 541 ++++++ .../testdata/webhook_github_star_created.json | 141 ++ .../webhook_github_watch_created.json | 139 ++ 8 files changed, 2640 insertions(+), 34 deletions(-) create mode 100644 docs/template-functions.md create mode 100644 server/testdata/webhook_github_comment_created.json create mode 100644 server/testdata/webhook_github_pr_opened.json create mode 100644 server/testdata/webhook_github_star_created.json create mode 100644 server/testdata/webhook_github_watch_created.json diff --git a/docs/publish.md b/docs/publish.md index 91f75e3d..24aa443a 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -950,22 +950,22 @@ Instead of using a separate bridge program to parse the webhook body into the fo message and/or a templated title which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). -You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes` or `1`, or (more appropriately -for webhooks) by setting the `?template=yes` query parameter. Then, include templates in your `message` and/or `title`, using the following stanzas (see [Go docs](https://pkg.go.dev/text/template) for detailed syntax): +You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`): -* Variables, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` -* Conditionals (if/else, e.g. `{{if eq .action "opened"}}..{{else}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6Ilt7ey5wdWxsX3JlcXVlc3QuaGVhZC5yZXBvLmZ1bGxfbmFtZX19XSBQdWxsIHJlcXVlc3Qge3tpZiBlcSAuYWN0aW9uIFwib3BlbmVkXCJ9fU9QRU5FRHt7ZWxzZX19Q0xPU0VEe3tlbmR9fToge3sucHVsbF9yZXF1ZXN0LnRpdGxlfX0iLCJpbnB1dCI6IntcbiAgXCJhY3Rpb25cIjogXCJvcGVuZWRcIixcbiAgXCJudW1iZXJcIjogMSxcbiAgXCJwdWxsX3JlcXVlc3RcIjoge1xuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy8xXCIsXG4gICAgXCJpZFwiOiAxNzgzNDIwOTcyLFxuICAgIFwibm9kZV9pZFwiOiBcIlBSX2t3RE9IQWJkbzg1cVROZ3NcIixcbiAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiLFxuICAgIFwiZGlmZl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUvcHVsbC8xLmRpZmZcIixcbiAgICBcInBhdGNoX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsLzEucGF0Y2hcIixcbiAgICBcImlzc3VlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIixcbiAgICBcIm51bWJlclwiOiAxLFxuICAgIFwic3RhdGVcIjogXCJvcGVuXCIsXG4gICAgXCJsb2NrZWRcIjogZmFsc2UsXG4gICAgXCJ0aXRsZVwiOiBcIkEgc2FtcGxlIFBSIGZyb20gUGhpbFwiLFxuICAgIFwidXNlclwiOiB7XG4gICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgfSxcbiAgICBcImJvZHlcIjogbnVsbCxcbiAgICBcImNyZWF0ZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjA5WlwiLFxuICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjQtMDMtMjFUMDI6NTI6MDlaXCIsXG4gICAgXCJjbG9zZWRfYXRcIjogbnVsbCxcbiAgICBcIm1lcmdlZF9hdFwiOiBudWxsLFxuICAgIFwibWVyZ2VfY29tbWl0X3NoYVwiOiBudWxsLFxuICAgIFwiYXNzaWduZWVcIjogbnVsbCxcbiAgICBcImFzc2lnbmVlc1wiOiBbXSxcbiAgICBcInJlcXVlc3RlZF9yZXZpZXdlcnNcIjogW10sXG4gICAgXCJyZXF1ZXN0ZWRfdGVhbXNcIjogW10sXG4gICAgXCJsYWJlbHNcIjogW10sXG4gICAgXCJtaWxlc3RvbmVcIjogbnVsbCxcbiAgICBcImRyYWZ0XCI6IGZhbHNlLFxuICAgIFwiY29tbWl0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzLzEvY29tbWl0c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvMS9jb21tZW50c1wiLFxuICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCIsXG4gICAgXCJoZWFkXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOmFhXCIsXG4gICAgICBcInJlZlwiOiBcImFhXCIsXG4gICAgICBcInNoYVwiOiBcIjU3MDM4NDJjYzU3MTVlZDFlMzU4ZDIzZWJiNjkzZGIwOTc0N2FlOWJcIixcbiAgICAgIFwidXNlclwiOiB7XG4gICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgICB9LFxuICAgICAgXCJyZXBvXCI6IHtcbiAgICAgICAgXCJpZFwiOiA0NzAyMTIwMDMsXG4gICAgICAgIFwibm9kZV9pZFwiOiBcIlJfa2dET0hBYmRvd1wiLFxuICAgICAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICAgICAgXCJmdWxsX25hbWVcIjogXCJiaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcInByaXZhdGVcIjogZmFsc2UsXG4gICAgICAgIFwib3duZXJcIjoge1xuICAgICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgICB9LFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgICAgIFwiZm9ya1wiOiBmYWxzZSxcbiAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgICAgICBcImtleXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9rZXlzey9rZXlfaWR9XCIsXG4gICAgICAgIFwiY29sbGFib3JhdG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbGxhYm9yYXRvcnN7L2NvbGxhYm9yYXRvcn1cIixcbiAgICAgICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgICAgIFwiaG9va3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ob29rc1wiLFxuICAgICAgICBcImlzc3VlX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9ldmVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICAgICAgXCJhc3NpZ25lZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9hc3NpZ25lZXN7L3VzZXJ9XCIsXG4gICAgICAgIFwiYnJhbmNoZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9icmFuY2hlc3svYnJhbmNofVwiLFxuICAgICAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgICAgIFwiYmxvYnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvYmxvYnN7L3NoYX1cIixcbiAgICAgICAgXCJnaXRfdGFnc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90YWdzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgICAgICBcInRyZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3RyZWVzey9zaGF9XCIsXG4gICAgICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy97c2hhfVwiLFxuICAgICAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgICAgICBcInN0YXJnYXplcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGFyZ2F6ZXJzXCIsXG4gICAgICAgIFwiY29udHJpYnV0b3JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udHJpYnV0b3JzXCIsXG4gICAgICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgICAgICBcInN1YnNjcmlwdGlvbl91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N1YnNjcmlwdGlvblwiLFxuICAgICAgICBcImNvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21taXRzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImNvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJpc3N1ZV9jb21tZW50X3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgICAgIFwiY29tcGFyZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9XCIsXG4gICAgICAgIFwibWVyZ2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWVyZ2VzXCIsXG4gICAgICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICAgICAgXCJkb3dubG9hZHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9kb3dubG9hZHNcIixcbiAgICAgICAgXCJpc3N1ZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibWlsZXN0b25lc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21pbGVzdG9uZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJub3RpZmljYXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9XCIsXG4gICAgICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgICAgICBcInJlbGVhc2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcmVsZWFzZXN7L2lkfVwiLFxuICAgICAgICBcImRlcGxveW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZGVwbG95bWVudHNcIixcbiAgICAgICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJ1cGRhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJwdXNoZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjEwWlwiLFxuICAgICAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInNzaF91cmxcIjogXCJnaXRAZ2l0aHViLmNvbTpiaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJjbG9uZV91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImhvbWVwYWdlXCI6IG51bGwsXG4gICAgICAgIFwic2l6ZVwiOiAxLFxuICAgICAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJ3YXRjaGVyc19jb3VudFwiOiAwLFxuICAgICAgICBcImxhbmd1YWdlXCI6IG51bGwsXG4gICAgICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgICAgICBcImhhc19wcm9qZWN0c1wiOiB0cnVlLFxuICAgICAgICBcImhhc19kb3dubG9hZHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgICAgICBcImhhc19wYWdlc1wiOiBmYWxzZSxcbiAgICAgICAgXCJoYXNfZGlzY3Vzc2lvbnNcIjogZmFsc2UsXG4gICAgICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICAgICAgXCJtaXJyb3JfdXJsXCI6IG51bGwsXG4gICAgICAgIFwiYXJjaGl2ZWRcIjogZmFsc2UsXG4gICAgICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNfY291bnRcIjogMSxcbiAgICAgICAgXCJsaWNlbnNlXCI6IG51bGwsXG4gICAgICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgICAgICBcImlzX3RlbXBsYXRlXCI6IGZhbHNlLFxuICAgICAgICBcIndlYl9jb21taXRfc2lnbm9mZl9yZXF1aXJlZFwiOiBmYWxzZSxcbiAgICAgICAgXCJ0b3BpY3NcIjogW10sXG4gICAgICAgIFwidmlzaWJpbGl0eVwiOiBcInB1YmxpY1wiLFxuICAgICAgICBcImZvcmtzXCI6IDAsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICAgICAgXCJ3YXRjaGVyc1wiOiAwLFxuICAgICAgICBcImRlZmF1bHRfYnJhbmNoXCI6IFwibWFpblwiLFxuICAgICAgICBcImFsbG93X3NxdWFzaF9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X21lcmdlX2NvbW1pdFwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X3JlYmFzZV9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X2F1dG9fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiZGVsZXRlX2JyYW5jaF9vbl9tZXJnZVwiOiBmYWxzZSxcbiAgICAgICAgXCJhbGxvd191cGRhdGVfYnJhbmNoXCI6IGZhbHNlLFxuICAgICAgICBcInVzZV9zcXVhc2hfcHJfdGl0bGVfYXNfZGVmYXVsdFwiOiBmYWxzZSxcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJDT01NSVRfTUVTU0FHRVNcIixcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X3RpdGxlXCI6IFwiQ09NTUlUX09SX1BSX1RJVExFXCIsXG4gICAgICAgIFwibWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJQUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF90aXRsZVwiOiBcIk1FUkdFX01FU1NBR0VcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJiYXNlXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOm1haW5cIixcbiAgICAgIFwicmVmXCI6IFwibWFpblwiLFxuICAgICAgXCJzaGFcIjogXCI3MmQ5MzFhMjBiYjgzZDEyM2FiNDVhY2NhZjc2MTE1MGM4YjAxMjExXCIsXG4gICAgICBcInVzZXJcIjoge1xuICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImlkXCI6IDY2NDU5NyxcbiAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgfSxcbiAgICAgIFwicmVwb1wiOiB7XG4gICAgICAgIFwiaWRcIjogNDcwMjEyMDAzLFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICAgICAgXCJuYW1lXCI6IFwiZGFiYmxlXCIsXG4gICAgICAgIFwiZnVsbF9uYW1lXCI6IFwiYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgICAgICBcIm93bmVyXCI6IHtcbiAgICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICAgIFwibm9kZV9pZFwiOiBcIk1EUTZWWE5sY2pZMk5EVTVOdz09XCIsXG4gICAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgICAgXCJmb2xsb3dpbmdfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2luZ3svb3RoZXJfdXNlcn1cIixcbiAgICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgICBcInN1YnNjcmlwdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N1YnNjcmlwdGlvbnNcIixcbiAgICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9ldmVudHN7L3ByaXZhY3l9XCIsXG4gICAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgICBcInNpdGVfYWRtaW5cIjogZmFsc2VcbiAgICAgICAgfSxcbiAgICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQSByZXBvIGZvciBkYWJibGluZ1wiLFxuICAgICAgICBcImZvcmtcIjogZmFsc2UsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImZvcmtzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZm9ya3NcIixcbiAgICAgICAgXCJrZXlzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUva2V5c3sva2V5X2lkfVwiLFxuICAgICAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgICAgIFwidGVhbXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90ZWFtc1wiLFxuICAgICAgICBcImhvb2tzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaG9va3NcIixcbiAgICAgICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZXZlbnRzXCIsXG4gICAgICAgIFwiYXNzaWduZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYXNzaWduZWVzey91c2VyfVwiLFxuICAgICAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICAgICAgXCJ0YWdzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvdGFnc1wiLFxuICAgICAgICBcImJsb2JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2Jsb2Jzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgICAgICBcImdpdF9yZWZzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3JlZnN7L3NoYX1cIixcbiAgICAgICAgXCJ0cmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90cmVlc3svc2hhfVwiLFxuICAgICAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICAgICAgXCJsYW5ndWFnZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9sYW5ndWFnZXNcIixcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhcmdhemVyc1wiLFxuICAgICAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgICAgICBcInN1YnNjcmliZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaWJlcnNcIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25fdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpcHRpb25cIixcbiAgICAgICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImdpdF9jb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2NvbW1pdHN7L3NoYX1cIixcbiAgICAgICAgXCJjb21tZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgICAgICBcImNvbnRlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udGVudHMveytwYXRofVwiLFxuICAgICAgICBcImNvbXBhcmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21wYXJlL3tiYXNlfS4uLntoZWFkfVwiLFxuICAgICAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgICAgICBcImFyY2hpdmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS97YXJjaGl2ZV9mb3JtYXR9ey9yZWZ9XCIsXG4gICAgICAgIFwiZG93bmxvYWRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZG93bmxvYWRzXCIsXG4gICAgICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwicHVsbHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsc3svbnVtYmVyfVwiLFxuICAgICAgICBcIm1pbGVzdG9uZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9taWxlc3RvbmVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgICAgICBcImxhYmVsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhYmVsc3svbmFtZX1cIixcbiAgICAgICAgXCJyZWxlYXNlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3JlbGVhc2Vzey9pZH1cIixcbiAgICAgICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgICAgIFwiY3JlYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICAgICAgXCJnaXRfdXJsXCI6IFwiZ2l0Oi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJzc2hfdXJsXCI6IFwiZ2l0QGdpdGh1Yi5jb206Ymlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInN2bl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJob21lcGFnZVwiOiBudWxsLFxuICAgICAgICBcInNpemVcIjogMSxcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX2NvdW50XCI6IDAsXG4gICAgICAgIFwid2F0Y2hlcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgICAgICBcImhhc19pc3N1ZXNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcHJvamVjdHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgICAgIFwiaGFzX3dpa2lcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcGFnZXNcIjogZmFsc2UsXG4gICAgICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgICAgICBcImZvcmtzX2NvdW50XCI6IDAsXG4gICAgICAgIFwibWlycm9yX3VybFwiOiBudWxsLFxuICAgICAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgICAgICBcImRpc2FibGVkXCI6IGZhbHNlLFxuICAgICAgICBcIm9wZW5faXNzdWVzX2NvdW50XCI6IDEsXG4gICAgICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgICAgICBcImFsbG93X2ZvcmtpbmdcIjogdHJ1ZSxcbiAgICAgICAgXCJpc190ZW1wbGF0ZVwiOiBmYWxzZSxcbiAgICAgICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgICAgIFwidG9waWNzXCI6IFtdLFxuICAgICAgICBcInZpc2liaWxpdHlcIjogXCJwdWJsaWNcIixcbiAgICAgICAgXCJmb3Jrc1wiOiAwLFxuICAgICAgICBcIm9wZW5faXNzdWVzXCI6IDEsXG4gICAgICAgIFwid2F0Y2hlcnNcIjogMCxcbiAgICAgICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIixcbiAgICAgICAgXCJhbGxvd19zcXVhc2hfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19tZXJnZV9jb21taXRcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19yZWJhc2VfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19hdXRvX21lcmdlXCI6IGZhbHNlLFxuICAgICAgICBcImRlbGV0ZV9icmFuY2hfb25fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiYWxsb3dfdXBkYXRlX2JyYW5jaFwiOiBmYWxzZSxcbiAgICAgICAgXCJ1c2Vfc3F1YXNoX3ByX3RpdGxlX2FzX2RlZmF1bHRcIjogZmFsc2UsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiQ09NTUlUX01FU1NBR0VTXCIsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF90aXRsZVwiOiBcIkNPTU1JVF9PUl9QUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiUFJfVElUTEVcIixcbiAgICAgICAgXCJtZXJnZV9jb21taXRfdGl0bGVcIjogXCJNRVJHRV9NRVNTQUdFXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiX2xpbmtzXCI6IHtcbiAgICAgIFwic2VsZlwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMVwiXG4gICAgICB9LFxuICAgICAgXCJodG1sXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiXG4gICAgICB9LFxuICAgICAgXCJpc3N1ZVwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWVudHNcIjoge1xuICAgICAgICBcImhyZWZcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy8xL2NvbW1lbnRzXCJcbiAgICAgIH0sXG4gICAgICBcInJldmlld19jb21tZW50c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiXG4gICAgICB9LFxuICAgICAgXCJyZXZpZXdfY29tbWVudFwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvY29tbWVudHN7L251bWJlcn1cIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWl0c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21taXRzXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXR1c2VzXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiYXV0aG9yX2Fzc29jaWF0aW9uXCI6IFwiT1dORVJcIixcbiAgICBcImF1dG9fbWVyZ2VcIjogbnVsbCxcbiAgICBcImFjdGl2ZV9sb2NrX3JlYXNvblwiOiBudWxsLFxuICAgIFwibWVyZ2VkXCI6IGZhbHNlLFxuICAgIFwibWVyZ2VhYmxlXCI6IG51bGwsXG4gICAgXCJyZWJhc2VhYmxlXCI6IG51bGwsXG4gICAgXCJtZXJnZWFibGVfc3RhdGVcIjogXCJ1bmtub3duXCIsXG4gICAgXCJtZXJnZWRfYnlcIjogbnVsbCxcbiAgICBcImNvbW1lbnRzXCI6IDAsXG4gICAgXCJyZXZpZXdfY29tbWVudHNcIjogMCxcbiAgICBcIm1haW50YWluZXJfY2FuX21vZGlmeVwiOiBmYWxzZSxcbiAgICBcImNvbW1pdHNcIjogMSxcbiAgICBcImFkZGl0aW9uc1wiOiAxLFxuICAgIFwiZGVsZXRpb25zXCI6IDEsXG4gICAgXCJjaGFuZ2VkX2ZpbGVzXCI6IDFcbiAgfSxcbiAgXCJyZXBvc2l0b3J5XCI6IHtcbiAgICBcImlkXCI6IDQ3MDIxMjAwMyxcbiAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICBcImZ1bGxfbmFtZVwiOiBcImJpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgIFwib3duZXJcIjoge1xuICAgICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgIH0sXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgXCJmb3JrXCI6IGZhbHNlLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgIFwia2V5c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2tleXN7L2tleV9pZH1cIixcbiAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgXCJob29rc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2hvb2tzXCIsXG4gICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICBcImFzc2lnbmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Fzc2lnbmVlc3svdXNlcn1cIixcbiAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgXCJibG9ic191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC9ibG9ic3svc2hhfVwiLFxuICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgIFwidHJlZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdHJlZXN7L3NoYX1cIixcbiAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgIFwic3RhcmdhemVyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N0YXJnYXplcnNcIixcbiAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgIFwic3Vic2NyaXB0aW9uX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaXB0aW9uXCIsXG4gICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgXCJjb21wYXJlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tcGFyZS97YmFzZX0uLi57aGVhZH1cIixcbiAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICBcImRvd25sb2Fkc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Rvd25sb2Fkc1wiLFxuICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgXCJtaWxlc3RvbmVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWlsZXN0b25lc3svbnVtYmVyfVwiLFxuICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgIFwicmVsZWFzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9yZWxlYXNlc3svaWR9XCIsXG4gICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICBcInVwZGF0ZWRfYXRcIjogXCIyMDIyLTAzLTE1VDE1OjA2OjE3WlwiLFxuICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3NoX3VybFwiOiBcImdpdEBnaXRodWIuY29tOmJpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiaG9tZXBhZ2VcIjogbnVsbCxcbiAgICBcInNpemVcIjogMSxcbiAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICBcIndhdGNoZXJzX2NvdW50XCI6IDAsXG4gICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgIFwiaGFzX3Byb2plY3RzXCI6IHRydWUsXG4gICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgIFwiaGFzX3BhZ2VzXCI6IGZhbHNlLFxuICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICBcIm1pcnJvcl91cmxcIjogbnVsbCxcbiAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgXCJvcGVuX2lzc3Vlc19jb3VudFwiOiAxLFxuICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgIFwiaXNfdGVtcGxhdGVcIjogZmFsc2UsXG4gICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgXCJ0b3BpY3NcIjogW10sXG4gICAgXCJ2aXNpYmlsaXR5XCI6IFwicHVibGljXCIsXG4gICAgXCJmb3Jrc1wiOiAwLFxuICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICBcIndhdGNoZXJzXCI6IDAsXG4gICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIlxuICB9LFxuICBcInNlbmRlclwiOiB7XG4gICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICBcImlkXCI6IDY2NDU5NyxcbiAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gIH1cbn1cbiIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) -* Loops (e.g. `{{range .errors}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6IlNldmVyZSBVUkxzOlxue3tyYW5nZSAuZXJyb3JzfX17e2lmIGVxIC5sZXZlbCBcInNldmVyZVwifX0tIHt7LnVybH19XG57e2VuZH19e3tlbmR9fSIsImlucHV0Ijoie1wiZm9vXCI6IFwiYmFyXCIsIFwiZXJyb3JzXCI6IFt7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMS5jb21cIn0se1wibGV2ZWxcIjogXCJ3YXJuaW5nXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly93YXJuaW5nLmNvbVwifSx7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMi5jb21cIn1dfSIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) +* **Inline templating**: Setting the `X-Template` header or query parameter to `yes` or `1` (e.g. `?template=yes`) + will enable inline templating, which means that the `message` and/or `title` **will be parsed as a Go template**. + See [Inline templating](#inline-templating) and [Template syntax](#template-syntax) for details on how to use Go + templates in your messages and titles. +* **Pre-defined template files**: You can also set `X-Template` header or query parameter to a template name (e.g. `?template=github`). + ntfy will then read the template from either the built-in pre-defined template files, or from the template files defined in + the `template-dir`. See [Template files](#pre-defined-templates) for more details. -A good way to experiment with Go templates is the **[Go Template Playground](https://repeatit.io)**. It is _highly recommended_ to test -your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). +### Inline templating -ntfy supports a subset of the Sprig template functions that are included in the **[Go Template Playground](https://repeatit.io)**. Please see -[Template Functions](sprig.md) for a list of supported template functions. - -!!! info - Please note that the Go templating language is quite terrible. My apologies for using it for this feature. It is the best option for Go-based - programs like ntfy. Stay calm and don't harm yourself or others in despair. **You can do it. I believe in you!** +When `X-Template: yes` or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your +webhook payload. This is most useful if no [pre-defined template](#pre-defined-templates) exists, for templated one-off messages, +of if you do not control the ntfy server (e.g., if you're using ntfy.sh). Please consider using [template files](#pre-defined-templates) +if you control the ntfy server, as templates are much easier to maintain. Here's an **example for a Grafana alert**: @@ -1078,6 +1078,48 @@ This example uses the `message`/`m` and `title`/`t` query parameters, but obviou `Message`/`Title` headers. It will send a notification with a title `phil-pc: A severe error has occurred` and a message `Error message: Disk has run out of space`. +### Pre-defined templates + +XXXXXXXXXXXXxx + +### Template syntax +ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful, +yet also one of the worst templating languages out there. + +You can use the following features in your templates: + +* Variables, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` +* Conditionals (if/else, e.g. `{{if eq .action "opened"}}..{{else}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6Ilt7ey5wdWxsX3JlcXVlc3QuaGVhZC5yZXBvLmZ1bGxfbmFtZX19XSBQdWxsIHJlcXVlc3Qge3tpZiBlcSAuYWN0aW9uIFwib3BlbmVkXCJ9fU9QRU5FRHt7ZWxzZX19Q0xPU0VEe3tlbmR9fToge3sucHVsbF9yZXF1ZXN0LnRpdGxlfX0iLCJpbnB1dCI6IntcbiAgXCJhY3Rpb25cIjogXCJvcGVuZWRcIixcbiAgXCJudW1iZXJcIjogMSxcbiAgXCJwdWxsX3JlcXVlc3RcIjoge1xuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy8xXCIsXG4gICAgXCJpZFwiOiAxNzgzNDIwOTcyLFxuICAgIFwibm9kZV9pZFwiOiBcIlBSX2t3RE9IQWJkbzg1cVROZ3NcIixcbiAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiLFxuICAgIFwiZGlmZl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUvcHVsbC8xLmRpZmZcIixcbiAgICBcInBhdGNoX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsLzEucGF0Y2hcIixcbiAgICBcImlzc3VlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIixcbiAgICBcIm51bWJlclwiOiAxLFxuICAgIFwic3RhdGVcIjogXCJvcGVuXCIsXG4gICAgXCJsb2NrZWRcIjogZmFsc2UsXG4gICAgXCJ0aXRsZVwiOiBcIkEgc2FtcGxlIFBSIGZyb20gUGhpbFwiLFxuICAgIFwidXNlclwiOiB7XG4gICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgfSxcbiAgICBcImJvZHlcIjogbnVsbCxcbiAgICBcImNyZWF0ZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjA5WlwiLFxuICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjQtMDMtMjFUMDI6NTI6MDlaXCIsXG4gICAgXCJjbG9zZWRfYXRcIjogbnVsbCxcbiAgICBcIm1lcmdlZF9hdFwiOiBudWxsLFxuICAgIFwibWVyZ2VfY29tbWl0X3NoYVwiOiBudWxsLFxuICAgIFwiYXNzaWduZWVcIjogbnVsbCxcbiAgICBcImFzc2lnbmVlc1wiOiBbXSxcbiAgICBcInJlcXVlc3RlZF9yZXZpZXdlcnNcIjogW10sXG4gICAgXCJyZXF1ZXN0ZWRfdGVhbXNcIjogW10sXG4gICAgXCJsYWJlbHNcIjogW10sXG4gICAgXCJtaWxlc3RvbmVcIjogbnVsbCxcbiAgICBcImRyYWZ0XCI6IGZhbHNlLFxuICAgIFwiY29tbWl0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzLzEvY29tbWl0c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvMS9jb21tZW50c1wiLFxuICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCIsXG4gICAgXCJoZWFkXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOmFhXCIsXG4gICAgICBcInJlZlwiOiBcImFhXCIsXG4gICAgICBcInNoYVwiOiBcIjU3MDM4NDJjYzU3MTVlZDFlMzU4ZDIzZWJiNjkzZGIwOTc0N2FlOWJcIixcbiAgICAgIFwidXNlclwiOiB7XG4gICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgICB9LFxuICAgICAgXCJyZXBvXCI6IHtcbiAgICAgICAgXCJpZFwiOiA0NzAyMTIwMDMsXG4gICAgICAgIFwibm9kZV9pZFwiOiBcIlJfa2dET0hBYmRvd1wiLFxuICAgICAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICAgICAgXCJmdWxsX25hbWVcIjogXCJiaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcInByaXZhdGVcIjogZmFsc2UsXG4gICAgICAgIFwib3duZXJcIjoge1xuICAgICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgICB9LFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgICAgIFwiZm9ya1wiOiBmYWxzZSxcbiAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgICAgICBcImtleXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9rZXlzey9rZXlfaWR9XCIsXG4gICAgICAgIFwiY29sbGFib3JhdG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbGxhYm9yYXRvcnN7L2NvbGxhYm9yYXRvcn1cIixcbiAgICAgICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgICAgIFwiaG9va3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ob29rc1wiLFxuICAgICAgICBcImlzc3VlX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9ldmVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICAgICAgXCJhc3NpZ25lZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9hc3NpZ25lZXN7L3VzZXJ9XCIsXG4gICAgICAgIFwiYnJhbmNoZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9icmFuY2hlc3svYnJhbmNofVwiLFxuICAgICAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgICAgIFwiYmxvYnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvYmxvYnN7L3NoYX1cIixcbiAgICAgICAgXCJnaXRfdGFnc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90YWdzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgICAgICBcInRyZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3RyZWVzey9zaGF9XCIsXG4gICAgICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy97c2hhfVwiLFxuICAgICAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgICAgICBcInN0YXJnYXplcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGFyZ2F6ZXJzXCIsXG4gICAgICAgIFwiY29udHJpYnV0b3JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udHJpYnV0b3JzXCIsXG4gICAgICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgICAgICBcInN1YnNjcmlwdGlvbl91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N1YnNjcmlwdGlvblwiLFxuICAgICAgICBcImNvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21taXRzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImNvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJpc3N1ZV9jb21tZW50X3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgICAgIFwiY29tcGFyZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9XCIsXG4gICAgICAgIFwibWVyZ2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWVyZ2VzXCIsXG4gICAgICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICAgICAgXCJkb3dubG9hZHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9kb3dubG9hZHNcIixcbiAgICAgICAgXCJpc3N1ZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibWlsZXN0b25lc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21pbGVzdG9uZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJub3RpZmljYXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9XCIsXG4gICAgICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgICAgICBcInJlbGVhc2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcmVsZWFzZXN7L2lkfVwiLFxuICAgICAgICBcImRlcGxveW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZGVwbG95bWVudHNcIixcbiAgICAgICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJ1cGRhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJwdXNoZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjEwWlwiLFxuICAgICAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInNzaF91cmxcIjogXCJnaXRAZ2l0aHViLmNvbTpiaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJjbG9uZV91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImhvbWVwYWdlXCI6IG51bGwsXG4gICAgICAgIFwic2l6ZVwiOiAxLFxuICAgICAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJ3YXRjaGVyc19jb3VudFwiOiAwLFxuICAgICAgICBcImxhbmd1YWdlXCI6IG51bGwsXG4gICAgICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgICAgICBcImhhc19wcm9qZWN0c1wiOiB0cnVlLFxuICAgICAgICBcImhhc19kb3dubG9hZHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgICAgICBcImhhc19wYWdlc1wiOiBmYWxzZSxcbiAgICAgICAgXCJoYXNfZGlzY3Vzc2lvbnNcIjogZmFsc2UsXG4gICAgICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICAgICAgXCJtaXJyb3JfdXJsXCI6IG51bGwsXG4gICAgICAgIFwiYXJjaGl2ZWRcIjogZmFsc2UsXG4gICAgICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNfY291bnRcIjogMSxcbiAgICAgICAgXCJsaWNlbnNlXCI6IG51bGwsXG4gICAgICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgICAgICBcImlzX3RlbXBsYXRlXCI6IGZhbHNlLFxuICAgICAgICBcIndlYl9jb21taXRfc2lnbm9mZl9yZXF1aXJlZFwiOiBmYWxzZSxcbiAgICAgICAgXCJ0b3BpY3NcIjogW10sXG4gICAgICAgIFwidmlzaWJpbGl0eVwiOiBcInB1YmxpY1wiLFxuICAgICAgICBcImZvcmtzXCI6IDAsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICAgICAgXCJ3YXRjaGVyc1wiOiAwLFxuICAgICAgICBcImRlZmF1bHRfYnJhbmNoXCI6IFwibWFpblwiLFxuICAgICAgICBcImFsbG93X3NxdWFzaF9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X21lcmdlX2NvbW1pdFwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X3JlYmFzZV9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X2F1dG9fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiZGVsZXRlX2JyYW5jaF9vbl9tZXJnZVwiOiBmYWxzZSxcbiAgICAgICAgXCJhbGxvd191cGRhdGVfYnJhbmNoXCI6IGZhbHNlLFxuICAgICAgICBcInVzZV9zcXVhc2hfcHJfdGl0bGVfYXNfZGVmYXVsdFwiOiBmYWxzZSxcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJDT01NSVRfTUVTU0FHRVNcIixcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X3RpdGxlXCI6IFwiQ09NTUlUX09SX1BSX1RJVExFXCIsXG4gICAgICAgIFwibWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJQUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF90aXRsZVwiOiBcIk1FUkdFX01FU1NBR0VcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJiYXNlXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOm1haW5cIixcbiAgICAgIFwicmVmXCI6IFwibWFpblwiLFxuICAgICAgXCJzaGFcIjogXCI3MmQ5MzFhMjBiYjgzZDEyM2FiNDVhY2NhZjc2MTE1MGM4YjAxMjExXCIsXG4gICAgICBcInVzZXJcIjoge1xuICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImlkXCI6IDY2NDU5NyxcbiAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgfSxcbiAgICAgIFwicmVwb1wiOiB7XG4gICAgICAgIFwiaWRcIjogNDcwMjEyMDAzLFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICAgICAgXCJuYW1lXCI6IFwiZGFiYmxlXCIsXG4gICAgICAgIFwiZnVsbF9uYW1lXCI6IFwiYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgICAgICBcIm93bmVyXCI6IHtcbiAgICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICAgIFwibm9kZV9pZFwiOiBcIk1EUTZWWE5sY2pZMk5EVTVOdz09XCIsXG4gICAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgICAgXCJmb2xsb3dpbmdfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2luZ3svb3RoZXJfdXNlcn1cIixcbiAgICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgICBcInN1YnNjcmlwdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N1YnNjcmlwdGlvbnNcIixcbiAgICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9ldmVudHN7L3ByaXZhY3l9XCIsXG4gICAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgICBcInNpdGVfYWRtaW5cIjogZmFsc2VcbiAgICAgICAgfSxcbiAgICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQSByZXBvIGZvciBkYWJibGluZ1wiLFxuICAgICAgICBcImZvcmtcIjogZmFsc2UsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImZvcmtzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZm9ya3NcIixcbiAgICAgICAgXCJrZXlzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUva2V5c3sva2V5X2lkfVwiLFxuICAgICAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgICAgIFwidGVhbXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90ZWFtc1wiLFxuICAgICAgICBcImhvb2tzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaG9va3NcIixcbiAgICAgICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZXZlbnRzXCIsXG4gICAgICAgIFwiYXNzaWduZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYXNzaWduZWVzey91c2VyfVwiLFxuICAgICAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICAgICAgXCJ0YWdzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvdGFnc1wiLFxuICAgICAgICBcImJsb2JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2Jsb2Jzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgICAgICBcImdpdF9yZWZzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3JlZnN7L3NoYX1cIixcbiAgICAgICAgXCJ0cmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90cmVlc3svc2hhfVwiLFxuICAgICAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICAgICAgXCJsYW5ndWFnZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9sYW5ndWFnZXNcIixcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhcmdhemVyc1wiLFxuICAgICAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgICAgICBcInN1YnNjcmliZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaWJlcnNcIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25fdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpcHRpb25cIixcbiAgICAgICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImdpdF9jb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2NvbW1pdHN7L3NoYX1cIixcbiAgICAgICAgXCJjb21tZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgICAgICBcImNvbnRlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udGVudHMveytwYXRofVwiLFxuICAgICAgICBcImNvbXBhcmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21wYXJlL3tiYXNlfS4uLntoZWFkfVwiLFxuICAgICAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgICAgICBcImFyY2hpdmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS97YXJjaGl2ZV9mb3JtYXR9ey9yZWZ9XCIsXG4gICAgICAgIFwiZG93bmxvYWRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZG93bmxvYWRzXCIsXG4gICAgICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwicHVsbHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsc3svbnVtYmVyfVwiLFxuICAgICAgICBcIm1pbGVzdG9uZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9taWxlc3RvbmVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgICAgICBcImxhYmVsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhYmVsc3svbmFtZX1cIixcbiAgICAgICAgXCJyZWxlYXNlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3JlbGVhc2Vzey9pZH1cIixcbiAgICAgICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgICAgIFwiY3JlYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICAgICAgXCJnaXRfdXJsXCI6IFwiZ2l0Oi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJzc2hfdXJsXCI6IFwiZ2l0QGdpdGh1Yi5jb206Ymlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInN2bl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJob21lcGFnZVwiOiBudWxsLFxuICAgICAgICBcInNpemVcIjogMSxcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX2NvdW50XCI6IDAsXG4gICAgICAgIFwid2F0Y2hlcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgICAgICBcImhhc19pc3N1ZXNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcHJvamVjdHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgICAgIFwiaGFzX3dpa2lcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcGFnZXNcIjogZmFsc2UsXG4gICAgICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgICAgICBcImZvcmtzX2NvdW50XCI6IDAsXG4gICAgICAgIFwibWlycm9yX3VybFwiOiBudWxsLFxuICAgICAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgICAgICBcImRpc2FibGVkXCI6IGZhbHNlLFxuICAgICAgICBcIm9wZW5faXNzdWVzX2NvdW50XCI6IDEsXG4gICAgICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgICAgICBcImFsbG93X2ZvcmtpbmdcIjogdHJ1ZSxcbiAgICAgICAgXCJpc190ZW1wbGF0ZVwiOiBmYWxzZSxcbiAgICAgICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgICAgIFwidG9waWNzXCI6IFtdLFxuICAgICAgICBcInZpc2liaWxpdHlcIjogXCJwdWJsaWNcIixcbiAgICAgICAgXCJmb3Jrc1wiOiAwLFxuICAgICAgICBcIm9wZW5faXNzdWVzXCI6IDEsXG4gICAgICAgIFwid2F0Y2hlcnNcIjogMCxcbiAgICAgICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIixcbiAgICAgICAgXCJhbGxvd19zcXVhc2hfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19tZXJnZV9jb21taXRcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19yZWJhc2VfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19hdXRvX21lcmdlXCI6IGZhbHNlLFxuICAgICAgICBcImRlbGV0ZV9icmFuY2hfb25fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiYWxsb3dfdXBkYXRlX2JyYW5jaFwiOiBmYWxzZSxcbiAgICAgICAgXCJ1c2Vfc3F1YXNoX3ByX3RpdGxlX2FzX2RlZmF1bHRcIjogZmFsc2UsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiQ09NTUlUX01FU1NBR0VTXCIsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF90aXRsZVwiOiBcIkNPTU1JVF9PUl9QUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiUFJfVElUTEVcIixcbiAgICAgICAgXCJtZXJnZV9jb21taXRfdGl0bGVcIjogXCJNRVJHRV9NRVNTQUdFXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiX2xpbmtzXCI6IHtcbiAgICAgIFwic2VsZlwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMVwiXG4gICAgICB9LFxuICAgICAgXCJodG1sXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiXG4gICAgICB9LFxuICAgICAgXCJpc3N1ZVwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWVudHNcIjoge1xuICAgICAgICBcImhyZWZcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy8xL2NvbW1lbnRzXCJcbiAgICAgIH0sXG4gICAgICBcInJldmlld19jb21tZW50c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiXG4gICAgICB9LFxuICAgICAgXCJyZXZpZXdfY29tbWVudFwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvY29tbWVudHN7L251bWJlcn1cIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWl0c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21taXRzXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXR1c2VzXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiYXV0aG9yX2Fzc29jaWF0aW9uXCI6IFwiT1dORVJcIixcbiAgICBcImF1dG9fbWVyZ2VcIjogbnVsbCxcbiAgICBcImFjdGl2ZV9sb2NrX3JlYXNvblwiOiBudWxsLFxuICAgIFwibWVyZ2VkXCI6IGZhbHNlLFxuICAgIFwibWVyZ2VhYmxlXCI6IG51bGwsXG4gICAgXCJyZWJhc2VhYmxlXCI6IG51bGwsXG4gICAgXCJtZXJnZWFibGVfc3RhdGVcIjogXCJ1bmtub3duXCIsXG4gICAgXCJtZXJnZWRfYnlcIjogbnVsbCxcbiAgICBcImNvbW1lbnRzXCI6IDAsXG4gICAgXCJyZXZpZXdfY29tbWVudHNcIjogMCxcbiAgICBcIm1haW50YWluZXJfY2FuX21vZGlmeVwiOiBmYWxzZSxcbiAgICBcImNvbW1pdHNcIjogMSxcbiAgICBcImFkZGl0aW9uc1wiOiAxLFxuICAgIFwiZGVsZXRpb25zXCI6IDEsXG4gICAgXCJjaGFuZ2VkX2ZpbGVzXCI6IDFcbiAgfSxcbiAgXCJyZXBvc2l0b3J5XCI6IHtcbiAgICBcImlkXCI6IDQ3MDIxMjAwMyxcbiAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICBcImZ1bGxfbmFtZVwiOiBcImJpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgIFwib3duZXJcIjoge1xuICAgICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgIH0sXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgXCJmb3JrXCI6IGZhbHNlLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgIFwia2V5c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2tleXN7L2tleV9pZH1cIixcbiAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgXCJob29rc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2hvb2tzXCIsXG4gICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICBcImFzc2lnbmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Fzc2lnbmVlc3svdXNlcn1cIixcbiAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgXCJibG9ic191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC9ibG9ic3svc2hhfVwiLFxuICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgIFwidHJlZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdHJlZXN7L3NoYX1cIixcbiAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgIFwic3RhcmdhemVyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N0YXJnYXplcnNcIixcbiAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgIFwic3Vic2NyaXB0aW9uX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaXB0aW9uXCIsXG4gICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgXCJjb21wYXJlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tcGFyZS97YmFzZX0uLi57aGVhZH1cIixcbiAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICBcImRvd25sb2Fkc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Rvd25sb2Fkc1wiLFxuICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgXCJtaWxlc3RvbmVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWlsZXN0b25lc3svbnVtYmVyfVwiLFxuICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgIFwicmVsZWFzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9yZWxlYXNlc3svaWR9XCIsXG4gICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICBcInVwZGF0ZWRfYXRcIjogXCIyMDIyLTAzLTE1VDE1OjA2OjE3WlwiLFxuICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3NoX3VybFwiOiBcImdpdEBnaXRodWIuY29tOmJpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiaG9tZXBhZ2VcIjogbnVsbCxcbiAgICBcInNpemVcIjogMSxcbiAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICBcIndhdGNoZXJzX2NvdW50XCI6IDAsXG4gICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgIFwiaGFzX3Byb2plY3RzXCI6IHRydWUsXG4gICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgIFwiaGFzX3BhZ2VzXCI6IGZhbHNlLFxuICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICBcIm1pcnJvcl91cmxcIjogbnVsbCxcbiAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgXCJvcGVuX2lzc3Vlc19jb3VudFwiOiAxLFxuICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgIFwiaXNfdGVtcGxhdGVcIjogZmFsc2UsXG4gICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgXCJ0b3BpY3NcIjogW10sXG4gICAgXCJ2aXNpYmlsaXR5XCI6IFwicHVibGljXCIsXG4gICAgXCJmb3Jrc1wiOiAwLFxuICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICBcIndhdGNoZXJzXCI6IDAsXG4gICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIlxuICB9LFxuICBcInNlbmRlclwiOiB7XG4gICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICBcImlkXCI6IDY2NDU5NyxcbiAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gIH1cbn1cbiIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) +* Loops (e.g. `{{range .errors}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6IlNldmVyZSBVUkxzOlxue3tyYW5nZSAuZXJyb3JzfX17e2lmIGVxIC5sZXZlbCBcInNldmVyZVwifX0tIHt7LnVybH19XG57e2VuZH19e3tlbmR9fSIsImlucHV0Ijoie1wiZm9vXCI6IFwiYmFyXCIsIFwiZXJyb3JzXCI6IFt7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMS5jb21cIn0se1wibGV2ZWxcIjogXCJ3YXJuaW5nXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly93YXJuaW5nLmNvbVwifSx7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMi5jb21cIn1dfSIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) + +A good way to experiment with Go templates is the **[Go Template Playground](https://repeatit.io)**. It is _highly recommended_ to test +your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). + +### Template functions +ntfy supports a subset of the [Sprig](https://github.com/Masterminds/sprig) template functions. This is useful for advanced +message templating and for transforming the data provided through the JSON payload. + +Below are the functions that are available to use inside your message/title templates. + +* [String Functions](./sprig/strings.md): `trim`, `trunc`, `substr`, `plural`, etc. + * [String List Functions](./sprig/string_slice.md): `splitList`, `sortAlpha`, etc. +* [Integer Math Functions](./sprig/math.md): `add`, `max`, `mul`, etc. + * [Integer List Functions](./sprig/integer_slice.md): `until`, `untilStep` +* [Date Functions](./sprig/date.md): `now`, `date`, etc. +* [Defaults Functions](./sprig/defaults.md): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` +* [Encoding Functions](./sprig/encoding.md): `b64enc`, `b64dec`, etc. +* [Lists and List Functions](./sprig/lists.md): `list`, `first`, `uniq`, etc. +* [Dictionaries and Dict Functions](./sprig/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. +* [Type Conversion Functions](./sprig/conversion.md): `atoi`, `int64`, `toString`, etc. +* [Path and Filepath Functions](./sprig/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` +* [Flow Control Functions]( ./sprig/flow_control.md): `fail` +* Advanced Functions + * [UUID Functions](./sprig/uuid.md): `uuidv4` + * [Reflection](./sprig/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. + * [Cryptographic and Security Functions](./sprig/crypto.md): `sha256sum`, etc. + * [URL](./sprig/url.md): `urlParse`, `urlJoin` + + ## Publish as JSON _Supported on:_ :material-android: :material-apple: :material-firefox: diff --git a/docs/template-functions.md b/docs/template-functions.md new file mode 100644 index 00000000..75c0e7c4 --- /dev/null +++ b/docs/template-functions.md @@ -0,0 +1,1455 @@ +# String Functions + +Sprig has a number of string manipulation functions. + +## trim + +The `trim` function removes space from either side of a string: + +``` +trim " hello " +``` + +The above produces `hello` + +## trimAll + +Remove given characters from the front or back of a string: + +``` +trimAll "$" "$5.00" +``` + +The above returns `5.00` (as a string). + +## trimSuffix + +Trim just the suffix from a string: + +``` +trimSuffix "-" "hello-" +``` + +The above returns `hello` + +## trimPrefix + +Trim just the prefix from a string: + +``` +trimPrefix "-" "-hello" +``` + +The above returns `hello` + +## upper + +Convert the entire string to uppercase: + +``` +upper "hello" +``` + +The above returns `HELLO` + +## lower + +Convert the entire string to lowercase: + +``` +lower "HELLO" +``` + +The above returns `hello` + +## title + +Convert to title case: + +``` +title "hello world" +``` + +The above returns `Hello World` + +## repeat + +Repeat a string multiple times: + +``` +repeat 3 "hello" +``` + +The above returns `hellohellohello` + +## substr + +Get a substring from a string. It takes three parameters: + +- start (int) +- end (int) +- string (string) + +``` +substr 0 5 "hello world" +``` + +The above returns `hello` + +## trunc + +Truncate a string (and add no suffix) + +``` +trunc 5 "hello world" +``` + +The above produces `hello`. + +``` +trunc -5 "hello world" +``` + +The above produces `world`. + +## contains + +Test to see if one string is contained inside of another: + +``` +contains "cat" "catch" +``` + +The above returns `true` because `catch` contains `cat`. + +## hasPrefix and hasSuffix + +The `hasPrefix` and `hasSuffix` functions test whether a string has a given +prefix or suffix: + +``` +hasPrefix "cat" "catch" +``` + +The above returns `true` because `catch` has the prefix `cat`. + +## quote and squote + +These functions wrap a string in double quotes (`quote`) or single quotes +(`squote`). + +## cat + +The `cat` function concatenates multiple strings together into one, separating +them with spaces: + +``` +cat "hello" "beautiful" "world" +``` + +The above produces `hello beautiful world` + +## indent + +The `indent` function indents every line in a given string to the specified +indent width. This is useful when aligning multi-line strings: + +``` +indent 4 $lots_of_text +``` + +The above will indent every line of text by 4 space characters. + +## nindent + +The `nindent` function is the same as the indent function, but prepends a new +line to the beginning of the string. + +``` +nindent 4 $lots_of_text +``` + +The above will indent every line of text by 4 space characters and add a new +line to the beginning. + +## replace + +Perform simple string replacement. + +It takes three arguments: + +- string to replace +- string to replace with +- source string + +``` +"I Am Henry VIII" | replace " " "-" +``` + +The above will produce `I-Am-Henry-VIII` + +## plural + +Pluralize a string. + +``` +len $fish | plural "one anchovy" "many anchovies" +``` + +In the above, if the length of the string is 1, the first argument will be +printed (`one anchovy`). Otherwise, the second argument will be printed +(`many anchovies`). + +The arguments are: + +- singular string +- plural string +- length integer + +NOTE: Sprig does not currently support languages with more complex pluralization +rules. And `0` is considered a plural because the English language treats it +as such (`zero anchovies`). The Sprig developers are working on a solution for +better internationalization. + +## regexMatch, mustRegexMatch + +Returns true if the input string contains any match of the regular expression. + +``` +regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" +``` + +The above produces `true` + +`regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the +template engine if there is a problem. + +## regexFindAll, mustRegexFindAll + +Returns a slice of all matches of the regular expression in the input string. +The last parameter n determines the number of substrings to return, where -1 means return all matches + +``` +regexFindAll "[2,4,6,8]" "123456789" -1 +``` + +The above produces `[2 4 6 8]` + +`regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the +template engine if there is a problem. + +## regexFind, mustRegexFind + +Return the first (left most) match of the regular expression in the input string + +``` +regexFind "[a-zA-Z][1-9]" "abcd1234" +``` + +The above produces `d1` + +`regexFind` panics if there is a problem and `mustRegexFind` returns an error to the +template engine if there is a problem. + +## regexReplaceAll, mustRegexReplaceAll + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. +Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch + +``` +regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W" +``` + +The above produces `-W-xxW-` + +`regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the +template engine if there is a problem. + +## regexReplaceAllLiteral, mustRegexReplaceAllLiteral + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement +The replacement string is substituted directly, without using Expand + +``` +regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}" +``` + +The above produces `-${1}-${1}-` + +`regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the +template engine if there is a problem. + +## regexSplit, mustRegexSplit + +Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches + +``` +regexSplit "z+" "pizza" -1 +``` + +The above produces `[pi a]` + +`regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the +template engine if there is a problem. + +## regexQuoteMeta + +Returns a string that escapes all regular expression metacharacters inside the argument text; +the returned string is a regular expression matching the literal text. + +``` +regexQuoteMeta "1.2.3" +``` + +The above produces `1\.2\.3` + +## See Also... + +The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains +functions for working with an array of strings. +# String List Functions + +These function operate on or generate slices of strings. In Go, a slice is a +growable array. In Sprig, it's a special case of a `list`. + +## join + +Join a list of strings into a single string, with the given separator. + +``` +list "hello" "world" | join "_" +``` + +The above will produce `hello_world` + +`join` will try to convert non-strings to a string value: + +``` +list 1 2 3 | join "+" +``` + +The above will produce `1+2+3` + +## splitList and split + +Split a string into a list of strings: + +``` +splitList "$" "foo$bar$baz" +``` + +The above will return `[foo bar baz]` + +The older `split` function splits a string into a `dict`. It is designed to make +it easy to use template dot notation for accessing members: + +``` +$a := split "$" "foo$bar$baz" +``` + +The above produces a map with index keys. `{_0: foo, _1: bar, _2: baz}` + +``` +$a._0 +``` + +The above produces `foo` + +## splitn + +`splitn` function splits a string into a `dict` with `n` keys. It is designed to make +it easy to use template dot notation for accessing members: + +``` +$a := splitn "$" 2 "foo$bar$baz" +``` + +The above produces a map with index keys. `{_0: foo, _1: bar$baz}` + +``` +$a._0 +``` + +The above produces `foo` + +## sortAlpha + +The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) +order. + +It does _not_ sort in place, but returns a sorted copy of the list, in keeping +with the immutability of lists. +# Integer Math Functions + +The following math functions operate on `int64` values. + +## add + +Sum numbers with `add`. Accepts two or more inputs. + +``` +add 1 2 3 +``` + +## add1 + +To increment by 1, use `add1` + +## sub + +To subtract, use `sub` + +## div + +Perform integer division with `div` + +## mod + +Modulo with `mod` + +## mul + +Multiply with `mul`. Accepts two or more inputs. + +``` +mul 1 2 3 +``` + +## max + +Return the largest of a series of integers: + +This will return `3`: + +``` +max 1 2 3 +``` + +## min + +Return the smallest of a series of integers. + +`min 1 2 3` will return `1` + +## floor + +Returns the greatest float value less than or equal to input value + +`floor 123.9999` will return `123.0` + +## ceil + +Returns the greatest float value greater than or equal to input value + +`ceil 123.001` will return `124.0` + +## round + +Returns a float value with the remainder rounded to the given number to digits after the decimal point. + +`round 123.555555 3` will return `123.556` + +## randInt +Returns a random integer value from min (inclusive) to max (exclusive). + +``` +randInt 12 30 +``` + +The above will produce a random number in the range [12,30]. +# Integer List Functions + +## until + +The `until` function builds a range of integers. + +``` +until 5 +``` + +The above generates the list `[0, 1, 2, 3, 4]`. + +This is useful for looping with `range $i, $e := until 5`. + +## untilStep + +Like `until`, `untilStep` generates a list of counting integers. But it allows +you to define a start, stop, and step: + +``` +untilStep 3 6 2 +``` + +The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal +or greater than 6. This is similar to Python's `range` function. + +## seq + +Works like the bash `seq` command. +* 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. +* 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. +* 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. + +``` +seq 5 => 1 2 3 4 5 +seq -3 => 1 0 -1 -2 -3 +seq 0 2 => 0 1 2 +seq 2 -2 => 2 1 0 -1 -2 +seq 0 2 10 => 0 2 4 6 8 10 +seq 0 -2 -5 => 0 -2 -4 +``` +# Date Functions + +## now + +The current date/time. Use this in conjunction with other date functions. + +## ago + +The `ago` function returns duration from time.Now in seconds resolution. + +``` +ago .CreatedAt +``` + +returns in `time.Duration` String() format + +``` +2h34m7s +``` + +## date + +The `date` function formats a date. + +Format the date to YEAR-MONTH-DAY: + +``` +now | date "2006-01-02" +``` + +Date formatting in Go is a [little bit different](https://pauladamsmith.com/blog/2011/05/go_time.html). + +In short, take this as the base date: + +``` +Mon Jan 2 15:04:05 MST 2006 +``` + +Write it in the format you want. Above, `2006-01-02` is the same date, but +in the format we want. + +## dateInZone + +Same as `date`, but with a timezone. + +``` +dateInZone "2006-01-02" (now) "UTC" +``` + +## duration + +Formats a given amount of seconds as a `time.Duration`. + +This returns 1m35s + +``` +duration "95" +``` + +## durationRound + +Rounds a given duration to the most significant unit. Strings and `time.Duration` +gets parsed as a duration, while a `time.Time` is calculated as the duration since. + +This return 2h + +``` +durationRound "2h10m5s" +``` + +This returns 3mo + +``` +durationRound "2400h10m5s" +``` + +## unixEpoch + +Returns the seconds since the unix epoch for a `time.Time`. + +``` +now | unixEpoch +``` + +## dateModify, mustDateModify + +The `dateModify` takes a modification and a date and returns the timestamp. + +Subtract an hour and thirty minutes from the current time: + +``` +now | date_modify "-1.5h" +``` + +If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. + +## htmlDate + +The `htmlDate` function formats a date for inserting into an HTML date picker +input field. + +``` +now | htmlDate +``` + +## htmlDateInZone + +Same as htmlDate, but with a timezone. + +``` +htmlDateInZone (now) "UTC" +``` + +## toDate, mustToDate + +`toDate` converts a string to a date. The first argument is the date layout and +the second the date string. If the string can't be convert it returns the zero +value. +`mustToDate` will return an error in case the string cannot be converted. + +This is useful when you want to convert a string date to another format +(using pipe). The example below converts "2017-12-31" to "31/12/2017". + +``` +toDate "2006-01-02" "2017-12-31" | date "02/01/2006" +``` +# Default Functions + +Sprig provides tools for setting default values for templates. + +## default + +To set a simple default value, use `default`: + +``` +default "foo" .Bar +``` + +In the above, if `.Bar` evaluates to a non-empty value, it will be used. But if +it is empty, `foo` will be returned instead. + +The definition of "empty" depends on type: + +- Numeric: 0 +- String: "" +- Lists: `[]` +- Dicts: `{}` +- Boolean: `false` +- And always `nil` (aka null) + +For structs, there is no definition of empty, so a struct will never return the +default. + +## empty + +The `empty` function returns `true` if the given value is considered empty, and +`false` otherwise. The empty values are listed in the `default` section. + +``` +empty .Foo +``` + +Note that in Go template conditionals, emptiness is calculated for you. Thus, +you rarely need `if empty .Foo`. Instead, just use `if .Foo`. + +## coalesce + +The `coalesce` function takes a list of values and returns the first non-empty +one. + +``` +coalesce 0 1 2 +``` + +The above returns `1`. + +This function is useful for scanning through multiple variables or values: + +``` +coalesce .name .parent.name "Matt" +``` + +The above will first check to see if `.name` is empty. If it is not, it will return +that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. +Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. + +## all + +The `all` function takes a list of values and returns true if all values are non-empty. + +``` +all 0 1 2 +``` + +The above returns `false`. + +This function is useful for evaluating multiple conditions of variables or values: + +``` +all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Method "POST") +``` + +The above will check http.Request is POST with tls 1.3 and http/2. + +## any + +The `any` function takes a list of values and returns true if any value is non-empty. + +``` +any 0 1 2 +``` + +The above returns `true`. + +This function is useful for evaluating multiple conditions of variables or values: + +``` +any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method "OPTIONS") +``` + +The above will check http.Request method is one of GET/POST/OPTIONS. + +## fromJSON, mustFromJSON + +`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. +`mustFromJSON` will return an error in case the JSON is invalid. + +``` +fromJSON "{\"foo\": 55}" +``` + +## toJSON, mustToJSON + +The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. +`mustToJSON` will return an error in case the item cannot be encoded in JSON. + +``` +toJSON .Item +``` + +The above returns JSON string representation of `.Item`. + +## toPrettyJSON, mustToPrettyJSON + +The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. + +``` +toPrettyJSON .Item +``` + +The above returns indented JSON string representation of `.Item`. + +## toRawJSON, mustToRawJSON + +The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. + +``` +toRawJSON .Item +``` + +The above returns unescaped JSON string representation of `.Item`. + +## ternary + +The `ternary` function takes two values, and a test value. If the test value is +true, the first value will be returned. If the test value is empty, the second +value will be returned. This is similar to the c ternary operator. + +### true test value + +``` +ternary "foo" "bar" true +``` + +or + +``` +true | ternary "foo" "bar" +``` + +The above returns `"foo"`. + +### false test value + +``` +ternary "foo" "bar" false +``` + +or + +``` +false | ternary "foo" "bar" +``` + +The above returns `"bar"`. +# Encoding Functions + +Sprig has the following encoding and decoding functions: + +- `b64enc`/`b64dec`: Encode or decode with Base64 +- `b32enc`/`b32dec`: Encode or decode with Base32 +# Lists and List Functions + +Sprig provides a simple `list` type that can contain arbitrary sequential lists +of data. This is similar to arrays or slices, but lists are designed to be used +as immutable data types. + +Create a list of integers: + +``` +$myList := list 1 2 3 4 5 +``` + +The above creates a list of `[1 2 3 4 5]`. + +## first, mustFirst + +To get the head item on a list, use `first`. + +`first $myList` returns `1` + +`first` panics if there is a problem while `mustFirst` returns an error to the +template engine if there is a problem. + +## rest, mustRest + +To get the tail of the list (everything but the first item), use `rest`. + +`rest $myList` returns `[2 3 4 5]` + +`rest` panics if there is a problem while `mustRest` returns an error to the +template engine if there is a problem. + +## last, mustLast + +To get the last item on a list, use `last`: + +`last $myList` returns `5`. This is roughly analogous to reversing a list and +then calling `first`. + +`last` panics if there is a problem while `mustLast` returns an error to the +template engine if there is a problem. + +## initial, mustInitial + +This compliments `last` by returning all _but_ the last element. +`initial $myList` returns `[1 2 3 4]`. + +`initial` panics if there is a problem while `mustInitial` returns an error to the +template engine if there is a problem. + +## append, mustAppend + +Append a new item to an existing list, creating a new list. + +``` +$new = append $myList 6 +``` + +The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. + +`append` panics if there is a problem while `mustAppend` returns an error to the +template engine if there is a problem. + +## prepend, mustPrepend + +Push an element onto the front of a list, creating a new list. + +``` +prepend $myList 0 +``` + +The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. + +`prepend` panics if there is a problem while `mustPrepend` returns an error to the +template engine if there is a problem. + +## concat + +Concatenate arbitrary number of lists into one. + +``` +concat $myList ( list 6 7 ) ( list 8 ) +``` + +The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. + +## reverse, mustReverse + +Produce a new list with the reversed elements of the given list. + +``` +reverse $myList +``` + +The above would generate the list `[5 4 3 2 1]`. + +`reverse` panics if there is a problem while `mustReverse` returns an error to the +template engine if there is a problem. + +## uniq, mustUniq + +Generate a list with all of the duplicates removed. + +``` +list 1 1 1 2 | uniq +``` + +The above would produce `[1 2]` + +`uniq` panics if there is a problem while `mustUniq` returns an error to the +template engine if there is a problem. + +## without, mustWithout + +The `without` function filters items out of a list. + +``` +without $myList 3 +``` + +The above would produce `[1 2 4 5]` + +Without can take more than one filter: + +``` +without $myList 1 3 5 +``` + +That would produce `[2 4]` + +`without` panics if there is a problem while `mustWithout` returns an error to the +template engine if there is a problem. + +## has, mustHas + +Test to see if a list has a particular element. + +``` +has 4 $myList +``` + +The above would return `true`, while `has "hello" $myList` would return false. + +`has` panics if there is a problem while `mustHas` returns an error to the +template engine if there is a problem. + +## compact, mustCompact + +Accepts a list and removes entries with empty values. + +``` +$list := list 1 "a" "foo" "" +$copy := compact $list +``` + +`compact` will return a new list with the empty (i.e., "") item removed. + +`compact` panics if there is a problem and `mustCompact` returns an error to the +template engine if there is a problem. + +## slice, mustSlice + +To get partial elements of a list, use `slice list [n] [m]`. It is +equivalent of `list[n:m]`. + +- `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. +- `slice $myList 3` returns `[4 5]`. It is same as `myList[3:]`. +- `slice $myList 1 3` returns `[2 3]`. It is same as `myList[1:3]`. +- `slice $myList 0 3` returns `[1 2 3]`. It is same as `myList[:3]`. + +`slice` panics if there is a problem while `mustSlice` returns an error to the +template engine if there is a problem. + +## chunk + +To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. + +``` +chunk 3 (list 1 2 3 4 5 6 7 8) +``` + +This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. + +## A Note on List Internals + +A list is implemented in Go as a `[]interface{}`. For Go developers embedding +Sprig, you may pass `[]interface{}` items into your template context and be +able to use all of the `list` functions on those items. +# Dictionaries and Dict Functions + +Sprig provides a key/value storage type called a `dict` (short for "dictionary", +as in Python). A `dict` is an _unorder_ type. + +The key to a dictionary **must be a string**. However, the value can be any +type, even another `dict` or `list`. + +Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will +modify the contents of a dictionary. + +## dict + +Creating dictionaries is done by calling the `dict` function and passing it a +list of pairs. + +The following creates a dictionary with three items: + +``` +$myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" +``` + +## get + +Given a map and a key, get the value from the map. + +``` +get $myDict "name1" +``` + +The above returns `"value1"` + +Note that if the key is not found, this operation will simply return `""`. No error +will be generated. + +## set + +Use `set` to add a new key/value pair to a dictionary. + +``` +$_ := set $myDict "name4" "value4" +``` + +Note that `set` _returns the dictionary_ (a requirement of Go template functions), +so you may need to trap the value as done above with the `$_` assignment. + +## unset + +Given a map and a key, delete the key from the map. + +``` +$_ := unset $myDict "name4" +``` + +As with `set`, this returns the dictionary. + +Note that if the key is not found, this operation will simply return. No error +will be generated. + +## hasKey + +The `hasKey` function returns `true` if the given dict contains the given key. + +``` +hasKey $myDict "name1" +``` + +If the key is not found, this returns `false`. + +## pluck + +The `pluck` function makes it possible to give one key and multiple maps, and +get a list of all of the matches: + +``` +pluck "name1" $myDict $myOtherDict +``` + +The above will return a `list` containing every found value (`[value1 otherValue1]`). + +If the give key is _not found_ in a map, that map will not have an item in the +list (and the length of the returned list will be less than the number of dicts +in the call to `pluck`. + +If the key is _found_ but the value is an empty value, that value will be +inserted. + +A common idiom in Sprig templates is to uses `pluck... | first` to get the first +matching key out of a collection of dictionaries. + +## dig + +The `dig` function traverses a nested set of dicts, selecting keys from a list +of values. It returns a default value if any of the keys are not found at the +associated dict. + +``` +dig "user" "role" "humanName" "guest" $dict +``` + +Given a dict structured like +``` +{ + user: { + role: { + humanName: "curator" + } + } +} +``` + +the above would return `"curator"`. If the dict lacked even a `user` field, +the result would be `"guest"`. + +Dig can be very useful in cases where you'd like to avoid guard clauses, +especially since Go's template package's `and` doesn't shortcut. For instance +`and a.maybeNil a.maybeNil.iNeedThis` will always evaluate +`a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) + +`dig` accepts its dict argument last in order to support pipelining. + +## keys + +The `keys` function will return a `list` of all of the keys in one or more `dict` +types. Since a dictionary is _unordered_, the keys will not be in a predictable order. +They can be sorted with `sortAlpha`. + +``` +keys $myDict | sortAlpha +``` + +When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` +function along with `sortAlpha` to get a unqiue, sorted list of keys. + +``` +keys $myDict $myOtherDict | uniq | sortAlpha +``` + +## pick + +The `pick` function selects just the given keys out of a dictionary, creating a +new `dict`. + +``` +$new := pick $myDict "name1" "name2" +``` + +The above returns `{name1: value1, name2: value2}` + +## omit + +The `omit` function is similar to `pick`, except it returns a new `dict` with all +the keys that _do not_ match the given keys. + +``` +$new := omit $myDict "name1" "name3" +``` + +The above returns `{name2: value2}` + +## values + +The `values` function is similar to `keys`, except it returns a new `list` with +all the values of the source `dict` (only one dictionary is supported). + +``` +$vals := values $myDict +``` + +The above returns `list["value1", "value2", "value 3"]`. Note that the `values` +function gives no guarantees about the result ordering- if you care about this, +then use `sortAlpha`. +# Type Conversion Functions + +The following type conversion functions are provided by Sprig: + +- `atoi`: Convert a string to an integer. +- `float64`: Convert to a `float64`. +- `int`: Convert to an `int` at the system's width. +- `int64`: Convert to an `int64`. +- `toDecimal`: Convert a unix octal to a `int64`. +- `toString`: Convert to a string. +- `toStrings`: Convert a list, slice, or array to a list of strings. + +Only `atoi` requires that the input be a specific type. The others will attempt +to convert from any type to the destination type. For example, `int64` can convert +floats to ints, and it can also convert strings to ints. + +## toStrings + +Given a list-like collection, produce a slice of strings. + +``` +list 1 2 3 | toStrings +``` + +The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns +them as a list. + +## toDecimal + +Given a unix octal permission, produce a decimal. + +``` +"0777" | toDecimal +``` + +The above converts `0777` to `511` and returns the value as an int64. +# Path and Filepath Functions + +While Sprig does not grant access to the filesystem, it does provide functions +for working with strings that follow file path conventions. + +## Paths + +Paths separated by the slash character (`/`), processed by the `path` package. + +Examples: + +* The [Linux](https://en.wikipedia.org/wiki/Linux) and + [MacOS](https://en.wikipedia.org/wiki/MacOS) + [filesystems](https://en.wikipedia.org/wiki/File_system): + `/home/user/file`, `/etc/config`; +* The path component of + [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): + `https://example.com/some/content/`, `ftp://example.com/file/`. + +### base + +Return the last element of a path. + +``` +base "foo/bar/baz" +``` + +The above prints "baz". + +### dir + +Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` +returns `foo/bar`. + +### clean + +Clean up a path. + +``` +clean "foo/bar/../baz" +``` + +The above resolves the `..` and returns `foo/baz`. + +### ext + +Return the file extension. + +``` +ext "foo.bar" +``` + +The above returns `.bar`. + +### isAbs + +To check whether a path is absolute, use `isAbs`. + +## Filepaths + +Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package. + +These are the recommended functions to use when parsing paths of local filesystems, usually when dealing with local files, directories, etc. + +Examples: + +* Running on Linux or MacOS the filesystem path is separated by the slash character (`/`): + `/home/user/file`, `/etc/config`; +* Running on [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) + the filesystem path is separated by the backslash character (`\`): + `C:\Users\Username\`, `C:\Program Files\Application\`; + +### osBase + +Return the last element of a filepath. + +``` +osBase "/foo/bar/baz" +osBase "C:\\foo\\bar\\baz" +``` + +The above prints "baz" on Linux and Windows, respectively. + +### osDir + +Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` +returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` +returns `C:\\foo\\bar` on Windows. + +### osClean + +Clean up a path. + +``` +osClean "/foo/bar/../baz" +osClean "C:\\foo\\bar\\..\\baz" +``` + +The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. + +### osExt + +Return the file extension. + +``` +osExt "/foo.bar" +osExt "C:\\foo.bar" +``` + +The above returns `.bar` on Linux and Windows, respectively. + +### osIsAbs + +To check whether a file path is absolute, use `osIsAbs`. +# Flow Control Functions + +## fail + +Unconditionally returns an empty `string` and an `error` with the specified +text. This is useful in scenarios where other conditionals have determined that +template rendering should fail. + +``` +fail "Please accept the end user license agreement" +``` +# UUID Functions + +Sprig can generate UUID v4 universally unique IDs. + +``` +uuidv4 +``` + +The above returns a new UUID of the v4 (randomly generated) type. +# Reflection Functions + +Sprig provides rudimentary reflection tools. These help advanced template +developers understand the underlying Go type information for a particular value. + +Go has several primitive _kinds_, like `string`, `slice`, `int64`, and `bool`. + +Go has an open _type_ system that allows developers to create their own types. + +Sprig provides a set of functions for each. + +## Kind Functions + +There are two Kind functions: `kindOf` returns the kind of an object. + +``` +kindOf "hello" +``` + +The above would return `string`. For simple tests (like in `if` blocks), the +`kindIs` function will let you verify that a value is a particular kind: + +``` +kindIs "int" 123 +``` + +The above will return `true` + +## Type Functions + +Types are slightly harder to work with, so there are three different functions: + +- `typeOf` returns the underlying type of a value: `typeOf $foo` +- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` +- `typeIsLike` works as `typeIs`, except that it also dereferences pointers. + +**Note:** None of these can test whether or not something implements a given +interface, since doing so would require compiling the interface in ahead of time. + +## deepEqual + +`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) + +Works for non-primitive types as well (compared to the built-in `eq`). + +``` +deepEqual (list 1 2 3) (list 1 2 3) +``` + +The above will return `true` +# Cryptographic and Security Functions + +Sprig provides a couple of advanced cryptographic functions. + +## sha1sum + +The `sha1sum` function receives a string, and computes it's SHA1 digest. + +``` +sha1sum "Hello world!" +``` + +## sha256sum + +The `sha256sum` function receives a string, and computes it's SHA256 digest. + +``` +sha256sum "Hello world!" +``` + +The above will compute the SHA 256 sum in an "ASCII armored" format that is +safe to print. + +## sha512sum + +The `sha512sum` function receives a string, and computes it's SHA512 digest. + +``` +sha512sum "Hello world!" +``` + +The above will compute the SHA 512 sum in an "ASCII armored" format that is +safe to print. + +## adler32sum + +The `adler32sum` function receives a string, and computes its Adler-32 checksum. + +``` +adler32sum "Hello world!" +``` +# URL Functions + +## urlParse +Parses string for URL and produces dict with URL parts + +``` +urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" +``` + +The above returns a dict, containing URL object: +```yaml +scheme: 'http' +host: 'server.com:8080' +path: '/api' +query: 'list=false' +opaque: nil +fragment: 'anchor' +userinfo: 'admin:secret' +``` + +For more info, check https://golang.org/pkg/net/url/#URL + +## urlJoin +Joins map (produced by `urlParse`) to produce URL string + +``` +urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") +``` + +The above returns the following string: +``` +proto://host:80/path?query#fragment +``` diff --git a/server/server_test.go b/server/server_test.go index 4fa059b6..a783dbd2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "crypto/rand" + _ "embed" "encoding/base64" "encoding/json" "fmt" @@ -3069,6 +3070,23 @@ func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) { require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code) } +var ( + //go:embed testdata/webhook_github_comment_created.json + githubCommentCreatedJSON string +) + +func TestServer_MessageTemplate_FromNamedTemplate(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", githubCommentCreatedJSON, map[string]string{ + "Template": "github", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "💬 New comment on issue #1389 — instant alerts without Pull to refresh", m.Title) + require.Equal(t, "💬 New comment on issue #1389 — instant alerts without Pull to refresh", m.Message) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/server/templates/github.yml b/server/templates/github.yml index 54a17e9b..92f3ab13 100644 --- a/server/templates/github.yml +++ b/server/templates/github.yml @@ -1,23 +1,32 @@ +title: | + {{- if .pull_request }} + Pull request {{ .action }}: #{{ .pull_request.number }} {{ .pull_request.title }} + {{- else if and .starred_at (eq .action "created")}} + ⭐ {{ .sender.login }} starred {{ .repository.full_name }} + {{- else if and .comment (eq .action "created") }} + 💬 New comment on issue #{{ .issue.number }} — {{ .issue.title }} + {{- else }} + Unsupported GitHub event type or action. + {{- end }} message: | - {{- if .pull_request }} - 🔀 PR {{ .action }}: #{{ .pull_request.number }} — {{ .pull_request.title }} - 📦 {{ .repository.full_name }} - 👤 {{ .pull_request.user.login }} - 🌿 {{ .pull_request.head.ref }} → {{ .pull_request.base.ref }} - 🔗 {{ .pull_request.html_url }} - 📝 {{ .pull_request.body | default "(no description)" }} - {{- else if and .starred_at (eq .action "created")}} - ⭐ {{ .sender.login }} starred {{ .repository.full_name }} - 📦 {{ .repository.description | default "(no description)" }} - 🔗 {{ .repository.html_url }} - 📅 {{ .starred_at }} - {{- else if and .comment (eq .action "created") }} - 💬 New comment on issue #{{ .issue.number }} — {{ .issue.title }} - 📦 {{ .repository.full_name }} - 👤 {{ .comment.user.login }} - 🔗 {{ .comment.html_url }} - 📝 {{ .comment.body | default "(no comment body)" }} - {{- else }} - {{ fail "Unsupported GitHub event type or action." }} - {{- end }} + {{- if .pull_request }} + Repository: {{ .repository.full_name }}, branch {{ .pull_request.head.ref }} → {{ .pull_request.base.ref }} + Created by: {{ .pull_request.user.login }} + Link: {{ .pull_request.html_url }} + {{ if .pull_request.body }}Description: + {{ .pull_request.body }}{{ end }} + {{- else if and .starred_at (eq .action "created")}} + ⭐ {{ .sender.login }} starred {{ .repository.full_name }} + 📦 {{ .repository.description | default "(no description)" }} + 🔗 {{ .repository.html_url }} + 📅 {{ .starred_at }} + {{- else if and .comment (eq .action "created") }} + 💬 New comment on issue #{{ .issue.number }} — {{ .issue.title }} + 📦 {{ .repository.full_name }} + 👤 {{ .comment.user.login }} + 🔗 {{ .comment.html_url }} + 📝 {{ .comment.body | default "(no comment body)" }} + {{- else }} + {{ fail "Unsupported GitHub event type or action." }} + {{- end }} diff --git a/server/testdata/webhook_github_comment_created.json b/server/testdata/webhook_github_comment_created.json new file mode 100644 index 00000000..04e7cddb --- /dev/null +++ b/server/testdata/webhook_github_comment_created.json @@ -0,0 +1,261 @@ +{ + "action": "created", + "issue": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389", + "repository_url": "https://api.github.com/repos/binwiederhier/ntfy", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/labels{/name}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/comments", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/events", + "html_url": "https://github.com/binwiederhier/ntfy/issues/1389", + "id": 3230655753, + "node_id": "I_kwDOGRBhi87Aj-UJ", + "number": 1389, + "title": "instant alerts without Pull to refresh", + "user": { + "login": "edbraunh", + "id": 8795846, + "node_id": "MDQ6VXNlcjg3OTU4NDY=", + "avatar_url": "https://avatars.githubusercontent.com/u/8795846?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/edbraunh", + "html_url": "https://github.com/edbraunh", + "followers_url": "https://api.github.com/users/edbraunh/followers", + "following_url": "https://api.github.com/users/edbraunh/following{/other_user}", + "gists_url": "https://api.github.com/users/edbraunh/gists{/gist_id}", + "starred_url": "https://api.github.com/users/edbraunh/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/edbraunh/subscriptions", + "organizations_url": "https://api.github.com/users/edbraunh/orgs", + "repos_url": "https://api.github.com/users/edbraunh/repos", + "events_url": "https://api.github.com/users/edbraunh/events{/privacy}", + "received_events_url": "https://api.github.com/users/edbraunh/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 3480884105, + "node_id": "LA_kwDOGRBhi87PehOJ", + "url": "https://api.github.com/repos/binwiederhier/ntfy/labels/enhancement", + "name": "enhancement", + "color": "a2eeef", + "default": true, + "description": "New feature or request" + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + ], + "milestone": null, + "comments": 3, + "created_at": "2025-07-15T03:46:30Z", + "updated_at": "2025-07-16T11:45:57Z", + "closed_at": null, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": "Hello ntfy Team,\n\nFirst off, thank you for developing such a powerful and lightweight notification app — it’s been invaluable for receiving timely alerts.\n\nI’m a user who relies heavily on ntfy for real-time trading alerts and have noticed that while push notifications arrive instantly, the in-app alert list does not automatically refresh with new messages. Currently, I need to manually pull-to-refresh the alert list to see the latest alerts.\n\nWould it be possible to add a feature that enables automatic refreshing of the alert list as new notifications arrive? This would greatly enhance usability and streamline the user experience, especially for users monitoring time-sensitive information.\n\nThank you for considering this request. I appreciate your hard work and look forward to future updates!", + "reactions": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + "comment": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments/3078214289", + "html_url": "https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289", + "issue_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389", + "id": 3078214289, + "node_id": "IC_kwDOGRBhi863edKR", + "user": { + "login": "wunter8", + "id": 8421688, + "node_id": "MDQ6VXNlcjg0MjE2ODg=", + "avatar_url": "https://avatars.githubusercontent.com/u/8421688?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wunter8", + "html_url": "https://github.com/wunter8", + "followers_url": "https://api.github.com/users/wunter8/followers", + "following_url": "https://api.github.com/users/wunter8/following{/other_user}", + "gists_url": "https://api.github.com/users/wunter8/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wunter8/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wunter8/subscriptions", + "organizations_url": "https://api.github.com/users/wunter8/orgs", + "repos_url": "https://api.github.com/users/wunter8/repos", + "events_url": "https://api.github.com/users/wunter8/events{/privacy}", + "received_events_url": "https://api.github.com/users/wunter8/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "created_at": "2025-07-16T11:45:57Z", + "updated_at": "2025-07-16T11:45:57Z", + "author_association": "CONTRIBUTOR", + "body": "These are the things you need to do to get iOS push notifications to work:\n1. open a browser to the web app of your ntfy instance and copy the URL (including \"http://\" or \"https://\", your domain or IP address, and any ports, and excluding any trailing slashes)\n2. put the URL you copied in the ntfy `base-url` config in server.yml or NTFY_BASE_URL in env variables\n3. put the URL you copied in the default server URL setting in the iOS ntfy app\n4. set `upstream-base-url` in server.yml or NTFY_UPSTREAM_BASE_URL in env variables to \"https://ntfy.sh\" (without a trailing slash)", + "reactions": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments/3078214289/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "performed_via_github_app": null + }, + "repository": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T10:18:34Z", + "pushed_at": "2025-07-13T13:56:19Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36740, + "stargazers_count": 25111, + "watchers_count": 25111, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 367, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 367, + "watchers": 25111, + "default_branch": "main" + }, + "sender": { + "login": "wunter8", + "id": 8421688, + "node_id": "MDQ6VXNlcjg0MjE2ODg=", + "avatar_url": "https://avatars.githubusercontent.com/u/8421688?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wunter8", + "html_url": "https://github.com/wunter8", + "followers_url": "https://api.github.com/users/wunter8/followers", + "following_url": "https://api.github.com/users/wunter8/following{/other_user}", + "gists_url": "https://api.github.com/users/wunter8/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wunter8/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wunter8/subscriptions", + "organizations_url": "https://api.github.com/users/wunter8/orgs", + "repos_url": "https://api.github.com/users/wunter8/repos", + "events_url": "https://api.github.com/users/wunter8/events{/privacy}", + "received_events_url": "https://api.github.com/users/wunter8/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } +} diff --git a/server/testdata/webhook_github_pr_opened.json b/server/testdata/webhook_github_pr_opened.json new file mode 100644 index 00000000..c89d1c3b --- /dev/null +++ b/server/testdata/webhook_github_pr_opened.json @@ -0,0 +1,541 @@ +{ + "action": "opened", + "number": 1390, + "pull_request": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390", + "id": 2670425869, + "node_id": "PR_kwDOGRBhi86fK3cN", + "html_url": "https://github.com/binwiederhier/ntfy/pull/1390", + "diff_url": "https://github.com/binwiederhier/ntfy/pull/1390.diff", + "patch_url": "https://github.com/binwiederhier/ntfy/pull/1390.patch", + "issue_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390", + "number": 1390, + "state": "open", + "locked": false, + "title": "WIP Template dir", + "user": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "body": null, + "created_at": "2025-07-16T11:49:31Z", + "updated_at": "2025-07-16T11:49:31Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "assignees": [ + ], + "requested_reviewers": [ + ], + "requested_teams": [ + ], + "labels": [ + ], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/commits", + "review_comments_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/comments", + "review_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390/comments", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/b1e935da45365c5e7e731d544a1ad4c7ea3643cd", + "head": { + "label": "binwiederhier:template-dir", + "ref": "template-dir", + "sha": "b1e935da45365c5e7e731d544a1ad4c7ea3643cd", + "user": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T10:18:34Z", + "pushed_at": "2025-07-16T11:49:26Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36740, + "stargazers_count": 25111, + "watchers_count": 25111, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 368, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 368, + "watchers": 25111, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": true, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "base": { + "label": "binwiederhier:main", + "ref": "main", + "sha": "81a486adc11fe24efcbedefb28ae946028597c2f", + "user": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T10:18:34Z", + "pushed_at": "2025-07-16T11:49:26Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36740, + "stargazers_count": 25111, + "watchers_count": 25111, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 368, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 368, + "watchers": 25111, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": true, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390" + }, + "html": { + "href": "https://github.com/binwiederhier/ntfy/pull/1390" + }, + "issue": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390" + }, + "comments": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/binwiederhier/ntfy/statuses/b1e935da45365c5e7e731d544a1ad4c7ea3643cd" + } + }, + "author_association": "OWNER", + "auto_merge": null, + "active_lock_reason": null, + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "maintainer_can_modify": false, + "commits": 7, + "additions": 5506, + "deletions": 42, + "changed_files": 58 + }, + "repository": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T10:18:34Z", + "pushed_at": "2025-07-16T11:49:26Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36740, + "stargazers_count": 25111, + "watchers_count": 25111, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 368, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 368, + "watchers": 25111, + "default_branch": "main" + }, + "sender": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } +} diff --git a/server/testdata/webhook_github_star_created.json b/server/testdata/webhook_github_star_created.json new file mode 100644 index 00000000..30099145 --- /dev/null +++ b/server/testdata/webhook_github_star_created.json @@ -0,0 +1,141 @@ +{ + "action": "created", + "starred_at": "2025-07-16T12:57:43Z", + "repository": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T12:57:43Z", + "pushed_at": "2025-07-16T11:49:26Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36831, + "stargazers_count": 25112, + "watchers_count": 25112, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 368, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 368, + "watchers": 25112, + "default_branch": "main" + }, + "sender": { + "login": "mbilby", + "id": 51273322, + "node_id": "MDQ6VXNlcjUxMjczMzIy", + "avatar_url": "https://avatars.githubusercontent.com/u/51273322?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mbilby", + "html_url": "https://github.com/mbilby", + "followers_url": "https://api.github.com/users/mbilby/followers", + "following_url": "https://api.github.com/users/mbilby/following{/other_user}", + "gists_url": "https://api.github.com/users/mbilby/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mbilby/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mbilby/subscriptions", + "organizations_url": "https://api.github.com/users/mbilby/orgs", + "repos_url": "https://api.github.com/users/mbilby/repos", + "events_url": "https://api.github.com/users/mbilby/events{/privacy}", + "received_events_url": "https://api.github.com/users/mbilby/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } +} + diff --git a/server/testdata/webhook_github_watch_created.json b/server/testdata/webhook_github_watch_created.json new file mode 100644 index 00000000..47440ebf --- /dev/null +++ b/server/testdata/webhook_github_watch_created.json @@ -0,0 +1,139 @@ +{ + "action": "started", + "repository": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T12:57:43Z", + "pushed_at": "2025-07-16T11:49:26Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36831, + "stargazers_count": 25112, + "watchers_count": 25112, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 368, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 368, + "watchers": 25112, + "default_branch": "main" + }, + "sender": { + "login": "mbilby", + "id": 51273322, + "node_id": "MDQ6VXNlcjUxMjczMzIy", + "avatar_url": "https://avatars.githubusercontent.com/u/51273322?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mbilby", + "html_url": "https://github.com/mbilby", + "followers_url": "https://api.github.com/users/mbilby/followers", + "following_url": "https://api.github.com/users/mbilby/following{/other_user}", + "gists_url": "https://api.github.com/users/mbilby/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mbilby/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mbilby/subscriptions", + "organizations_url": "https://api.github.com/users/mbilby/orgs", + "repos_url": "https://api.github.com/users/mbilby/repos", + "events_url": "https://api.github.com/users/mbilby/events{/privacy}", + "received_events_url": "https://api.github.com/users/mbilby/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } +} From 4603802f62bdb6495c159d33c9dfc80801204159 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 16 Jul 2025 21:50:29 +0200 Subject: [PATCH 175/378] WIP --- docs/template-functions.md | 1093 +++++++++++------ server/server.go | 31 +- server/templates/github.yml | 69 +- .../testdata/webhook_github_issue_opened.json | 216 ++++ util/sprig/functions.go | 43 +- 5 files changed, 994 insertions(+), 458 deletions(-) create mode 100644 server/testdata/webhook_github_issue_opened.json diff --git a/docs/template-functions.md b/docs/template-functions.md index 75c0e7c4..7c9593e6 100644 --- a/docs/template-functions.md +++ b/docs/template-functions.md @@ -1,90 +1,128 @@ -# String Functions +# Template functions + +## Table of Contents + +- [String Functions](#string-functions) +- [String List Functions](#string-list-functions) +- [Integer Math Functions](#integer-math-functions) +- [Integer List Functions](#integer-list-functions) +- [Date Functions](#date-functions) +- [Default Functions](#default-functions) +- [Encoding Functions](#encoding-functions) +- [Lists and List Functions](#lists-and-list-functions) +- [Dictionaries and Dict Functions](#dictionaries-and-dict-functions) +- [Type Conversion Functions](#type-conversion-functions) +- [Path and Filepath Functions](#path-and-filepath-functions) +- [Flow Control Functions](#flow-control-functions) +- [UUID Functions](#uuid-functions) +- [Reflection Functions](#reflection-functions) +- [Cryptographic and Security Functions](#cryptographic-and-security-functions) +- [URL Functions](#url-functions) + +## String Functions Sprig has a number of string manipulation functions. -## trim - -The `trim` function removes space from either side of a string: + + + + + -## trimAll - -Remove given characters from the front or back of a string: + + + + -## trimSuffix - -Trim just the suffix from a string: + + + + -## trimPrefix - -Trim just the prefix from a string: + + + + -## upper - -Convert the entire string to uppercase: + + + + -## lower - -Convert the entire string to lowercase: + + + + -## title - -Convert to title case: + + + + -## repeat - -Repeat a string multiple times: + + + + -## substr - -Get a substring from a string. It takes three parameters: + + + + -## trunc - -Truncate a string (and add no suffix) + + + + -## contains - -Test to see if one string is contained inside of another: + + + + -## hasPrefix and hasSuffix - -The `hasPrefix` and `hasSuffix` functions test whether a string has a given + + + + -## quote and squote - -These functions wrap a string in double quotes (`quote`) or single quotes + + + + -## cat - -The `cat` function concatenates multiple strings together into one, separating + + + + -## indent - -The `indent` function indents every line in a given string to the specified + + + + -## nindent - -The `nindent` function is the same as the indent function, but prepends a new + + + + -## replace - -Perform simple string replacement. + + + + -## plural - -Pluralize a string. + + + + -## regexMatch, mustRegexMatch - -Returns true if the input string contains any match of the regular expression. + + + + -## regexFindAll, mustRegexFindAll - -Returns a slice of all matches of the regular expression in the input string. + + + + -## regexFind, mustRegexFind - -Return the first (left most) match of the regular expression in the input string + + + + -## regexReplaceAll, mustRegexReplaceAll - -Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. + + + + -## regexReplaceAllLiteral, mustRegexReplaceAllLiteral - -Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement + + + + -## regexSplit, mustRegexSplit - -Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches + + + + -## regexQuoteMeta - -Returns a string that escapes all regular expression metacharacters inside the argument text; + + + + +
trimThe `trim` function removes space from either side of a string: ``` trim " hello " ``` The above produces `hello` +
trimAllRemove given characters from the front or back of a string: ``` trimAll "$" "$5.00" ``` The above returns `5.00` (as a string). +
trimSuffixTrim just the suffix from a string: ``` trimSuffix "-" "hello-" ``` The above returns `hello` +
trimPrefixTrim just the prefix from a string: ``` trimPrefix "-" "-hello" ``` The above returns `hello` +
upperConvert the entire string to uppercase: ``` upper "hello" ``` The above returns `HELLO` +
lowerConvert the entire string to lowercase: ``` lower "HELLO" ``` The above returns `hello` +
titleConvert to title case: ``` title "hello world" ``` The above returns `Hello World` +
repeatRepeat a string multiple times: ``` repeat 3 "hello" ``` The above returns `hellohellohello` +
substrGet a substring from a string. It takes three parameters: - start (int) - end (int) @@ -95,10 +133,12 @@ substr 0 5 "hello world" ``` The above returns `hello` +
truncTruncate a string (and add no suffix) ``` trunc 5 "hello world" @@ -111,20 +151,24 @@ trunc -5 "hello world" ``` The above produces `world`. +
containsTest to see if one string is contained inside of another: ``` contains "cat" "catch" ``` The above returns `true` because `catch` contains `cat`. +
hasPrefix and hasSuffixThe `hasPrefix` and `hasSuffix` functions test whether a string has a given prefix or suffix: ``` @@ -132,15 +176,19 @@ hasPrefix "cat" "catch" ``` The above returns `true` because `catch` has the prefix `cat`. +
quote and squoteThese functions wrap a string in double quotes (`quote`) or single quotes (`squote`). +
catThe `cat` function concatenates multiple strings together into one, separating them with spaces: ``` @@ -148,10 +196,12 @@ cat "hello" "beautiful" "world" ``` The above produces `hello beautiful world` +
indentThe `indent` function indents every line in a given string to the specified indent width. This is useful when aligning multi-line strings: ``` @@ -159,10 +209,12 @@ indent 4 $lots_of_text ``` The above will indent every line of text by 4 space characters. +
nindentThe `nindent` function is the same as the indent function, but prepends a new line to the beginning of the string. ``` @@ -171,10 +223,12 @@ nindent 4 $lots_of_text The above will indent every line of text by 4 space characters and add a new line to the beginning. +
replacePerform simple string replacement. It takes three arguments: @@ -187,10 +241,12 @@ It takes three arguments: ``` The above will produce `I-Am-Henry-VIII` +
pluralPluralize a string. ``` len $fish | plural "one anchovy" "many anchovies" @@ -210,10 +266,12 @@ NOTE: Sprig does not currently support languages with more complex pluralization rules. And `0` is considered a plural because the English language treats it as such (`zero anchovies`). The Sprig developers are working on a solution for better internationalization. +
regexMatch, mustRegexMatchReturns true if the input string contains any match of the regular expression. ``` regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" @@ -223,10 +281,12 @@ The above produces `true` `regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the template engine if there is a problem. +
regexFindAll, mustRegexFindAllReturns a slice of all matches of the regular expression in the input string. The last parameter n determines the number of substrings to return, where -1 means return all matches ``` @@ -237,10 +297,12 @@ The above produces `[2 4 6 8]` `regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the template engine if there is a problem. +
regexFind, mustRegexFindReturn the first (left most) match of the regular expression in the input string ``` regexFind "[a-zA-Z][1-9]" "abcd1234" @@ -250,10 +312,12 @@ The above produces `d1` `regexFind` panics if there is a problem and `mustRegexFind` returns an error to the template engine if there is a problem. +
regexReplaceAll, mustRegexReplaceAllReturns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch ``` @@ -264,10 +328,12 @@ The above produces `-W-xxW-` `regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the template engine if there is a problem. +
regexReplaceAllLiteral, mustRegexReplaceAllLiteralReturns a copy of the input string, replacing matches of the Regexp with the replacement string replacement The replacement string is substituted directly, without using Expand ``` @@ -278,10 +344,12 @@ The above produces `-${1}-${1}-` `regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the template engine if there is a problem. +
regexSplit, mustRegexSplitSlices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches ``` regexSplit "z+" "pizza" -1 @@ -291,10 +359,12 @@ The above produces `[pi a]` `regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the template engine if there is a problem. +
regexQuoteMetaReturns a string that escapes all regular expression metacharacters inside the argument text; the returned string is a regular expression matching the literal text. ``` @@ -302,19 +372,22 @@ regexQuoteMeta "1.2.3" ``` The above produces `1\.2\.3` - -## See Also... +
The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains functions for working with an array of strings. -# String List Functions + +## String List Functions These function operate on or generate slices of strings. In Go, a slice is a growable array. In Sprig, it's a special case of a `list`. -## join - -Join a list of strings into a single string, with the given separator. + + + + + -## splitList and split - -Split a string into a list of strings: + + + + -## splitn - -`splitn` function splits a string into a `dict` with `n` keys. It is designed to make + + + + -## sortAlpha - -The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) + + + + +
joinJoin a list of strings into a single string, with the given separator. ``` list "hello" "world" | join "_" @@ -329,10 +402,12 @@ list 1 2 3 | join "+" ``` The above will produce `1+2+3` +
splitList and splitSplit a string into a list of strings: ``` splitList "$" "foo$bar$baz" @@ -354,10 +429,12 @@ $a._0 ``` The above produces `foo` +
splitn`splitn` function splits a string into a `dict` with `n` keys. It is designed to make it easy to use template dot notation for accessing members: ``` @@ -371,97 +448,132 @@ $a._0 ``` The above produces `foo` +
sortAlphaThe `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) order. It does _not_ sort in place, but returns a sorted copy of the list, in keeping with the immutability of lists. -# Integer Math Functions +
+ +## Integer Math Functions The following math functions operate on `int64` values. -## add - -Sum numbers with `add`. Accepts two or more inputs. + + + + + -## add1 + + + + -To increment by 1, use `add1` + + + + -## sub + + + + -To subtract, use `sub` + + + + -## div - -Perform integer division with `div` - -## mod - -Modulo with `mod` - -## mul - -Multiply with `mul`. Accepts two or more inputs. + + + + -## max - -Return the largest of a series of integers: + + + + -## min - -Return the smallest of a series of integers. + + + + -## floor - -Returns the greatest float value less than or equal to input value + + + + -## ceil - -Returns the greatest float value greater than or equal to input value + + + + -## round - -Returns a float value with the remainder rounded to the given number to digits after the decimal point. + + + + -## randInt -Returns a random integer value from min (inclusive) to max (exclusive). + + + + +
addSum numbers with `add`. Accepts two or more inputs. ``` add 1 2 3 ``` +
add1To increment by 1, use `add1` +
subTo subtract, use `sub` +
divPerform integer division with `div` +
modModulo with `mod` +
mulMultiply with `mul`. Accepts two or more inputs. ``` mul 1 2 3 ``` +
maxReturn the largest of a series of integers: This will return `3`: ``` max 1 2 3 ``` +
minReturn the smallest of a series of integers. `min 1 2 3` will return `1` +
floorReturns the greatest float value less than or equal to input value `floor 123.9999` will return `123.0` +
ceilReturns the greatest float value greater than or equal to input value `ceil 123.001` will return `124.0` +
roundReturns a float value with the remainder rounded to the given number to digits after the decimal point. `round 123.555555 3` will return `123.556` +
randIntReturns a random integer value from min (inclusive) to max (exclusive). ``` randInt 12 30 ``` The above will produce a random number in the range [12,30]. -# Integer List Functions +
-## until +## Integer List Functions -The `until` function builds a range of integers. + + + + + -## untilStep - -Like `until`, `untilStep` generates a list of counting integers. But it allows + + + + -## seq - -Works like the bash `seq` command. + + + + +
untilThe `until` function builds a range of integers. ``` until 5 @@ -470,10 +582,12 @@ until 5 The above generates the list `[0, 1, 2, 3, 4]`. This is useful for looping with `range $i, $e := until 5`. +
untilStepLike `until`, `untilStep` generates a list of counting integers. But it allows you to define a start, stop, and step: ``` @@ -482,10 +596,12 @@ untilStep 3 6 2 The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal or greater than 6. This is similar to Python's `range` function. +
seqWorks like the bash `seq` command. * 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. * 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. * 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. @@ -498,15 +614,22 @@ seq 2 -2 => 2 1 0 -1 -2 seq 0 2 10 => 0 2 4 6 8 10 seq 0 -2 -5 => 0 -2 -4 ``` -# Date Functions +
-## now +## Date Functions -The current date/time. Use this in conjunction with other date functions. + + + + + -## ago - -The `ago` function returns duration from time.Now in seconds resolution. + + + + -## date - -The `date` function formats a date. + + + + -## dateInZone - -Same as `date`, but with a timezone. + + + + -## duration - -Formats a given amount of seconds as a `time.Duration`. + + + + -## durationRound - -Rounds a given duration to the most significant unit. Strings and `time.Duration` + + + + -## unixEpoch - -Returns the seconds since the unix epoch for a `time.Time`. + + + + -## dateModify, mustDateModify - -The `dateModify` takes a modification and a date and returns the timestamp. + + + + -## htmlDate - -The `htmlDate` function formats a date for inserting into an HTML date picker + + + + -## htmlDateInZone - -Same as htmlDate, but with a timezone. + + + + -## toDate, mustToDate - -`toDate` converts a string to a date. The first argument is the date layout and + + + + +
nowThe current date/time. Use this in conjunction with other date functions. +
agoThe `ago` function returns duration from time.Now in seconds resolution. ``` ago .CreatedAt @@ -517,10 +640,12 @@ returns in `time.Duration` String() format ``` 2h34m7s ``` +
dateThe `date` function formats a date. Format the date to YEAR-MONTH-DAY: @@ -538,28 +663,34 @@ Mon Jan 2 15:04:05 MST 2006 Write it in the format you want. Above, `2006-01-02` is the same date, but in the format we want. +
dateInZoneSame as `date`, but with a timezone. ``` dateInZone "2006-01-02" (now) "UTC" ``` +
durationFormats a given amount of seconds as a `time.Duration`. This returns 1m35s ``` duration "95" ``` +
durationRoundRounds a given duration to the most significant unit. Strings and `time.Duration` gets parsed as a duration, while a `time.Time` is calculated as the duration since. This return 2h @@ -573,18 +704,22 @@ This returns 3mo ``` durationRound "2400h10m5s" ``` +
unixEpochReturns the seconds since the unix epoch for a `time.Time`. ``` now | unixEpoch ``` +
dateModify, mustDateModifyThe `dateModify` takes a modification and a date and returns the timestamp. Subtract an hour and thirty minutes from the current time: @@ -593,27 +728,33 @@ now | date_modify "-1.5h" ``` If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. +
htmlDateThe `htmlDate` function formats a date for inserting into an HTML date picker input field. ``` now | htmlDate ``` +
htmlDateInZoneSame as htmlDate, but with a timezone. ``` htmlDateInZone (now) "UTC" ``` +
toDate, mustToDate`toDate` converts a string to a date. The first argument is the date layout and the second the date string. If the string can't be convert it returns the zero value. `mustToDate` will return an error in case the string cannot be converted. @@ -624,13 +765,18 @@ This is useful when you want to convert a string date to another format ``` toDate "2006-01-02" "2017-12-31" | date "02/01/2006" ``` -# Default Functions +
+ +## Default Functions Sprig provides tools for setting default values for templates. -## default - -To set a simple default value, use `default`: + + + + + -## empty - -The `empty` function returns `true` if the given value is considered empty, and + + + + -## coalesce - -The `coalesce` function takes a list of values and returns the first non-empty + + + + -## all - -The `all` function takes a list of values and returns true if all values are non-empty. + + + + -## any - -The `any` function takes a list of values and returns true if any value is non-empty. + + + + -## fromJSON, mustFromJSON - -`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. + + + + -## toJSON, mustToJSON - -The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. + + + + -## toPrettyJSON, mustToPrettyJSON - -The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. + + + + -## toRawJSON, mustToRawJSON - -The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. + + + + -## ternary - -The `ternary` function takes two values, and a test value. If the test value is + + + + +
defaultTo set a simple default value, use `default`: ``` default "foo" .Bar @@ -650,10 +796,12 @@ The definition of "empty" depends on type: For structs, there is no definition of empty, so a struct will never return the default. +
emptyThe `empty` function returns `true` if the given value is considered empty, and `false` otherwise. The empty values are listed in the `default` section. ``` @@ -662,10 +810,12 @@ empty .Foo Note that in Go template conditionals, emptiness is calculated for you. Thus, you rarely need `if empty .Foo`. Instead, just use `if .Foo`. +
coalesceThe `coalesce` function takes a list of values and returns the first non-empty one. ``` @@ -683,10 +833,12 @@ coalesce .name .parent.name "Matt" The above will first check to see if `.name` is empty. If it is not, it will return that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. +
allThe `all` function takes a list of values and returns true if all values are non-empty. ``` all 0 1 2 @@ -701,10 +853,12 @@ all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Me ``` The above will check http.Request is POST with tls 1.3 and http/2. +
anyThe `any` function takes a list of values and returns true if any value is non-empty. ``` any 0 1 2 @@ -719,19 +873,23 @@ any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method " ``` The above will check http.Request method is one of GET/POST/OPTIONS. +
fromJSON, mustFromJSON`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. `mustFromJSON` will return an error in case the JSON is invalid. ``` fromJSON "{\"foo\": 55}" ``` +
toJSON, mustToJSONThe `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. `mustToJSON` will return an error in case the item cannot be encoded in JSON. ``` @@ -739,30 +897,36 @@ toJSON .Item ``` The above returns JSON string representation of `.Item`. +
toPrettyJSON, mustToPrettyJSONThe `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. ``` toPrettyJSON .Item ``` The above returns indented JSON string representation of `.Item`. +
toRawJSON, mustToRawJSONThe `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. ``` toRawJSON .Item ``` The above returns unescaped JSON string representation of `.Item`. +
ternaryThe `ternary` function takes two values, and a test value. If the test value is true, the first value will be returned. If the test value is empty, the second value will be returned. This is similar to the c ternary operator. @@ -793,13 +957,29 @@ false | ternary "foo" "bar" ``` The above returns `"bar"`. -# Encoding Functions +
+ +## Encoding Functions Sprig has the following encoding and decoding functions: -- `b64enc`/`b64dec`: Encode or decode with Base64 -- `b32enc`/`b32dec`: Encode or decode with Base32 -# Lists and List Functions + + + + + + + + + + +
b64enc/b64decEncode or decode with Base64 +
b32enc/b32decEncode or decode with Base32 +
+ +## Lists and List Functions Sprig provides a simple `list` type that can contain arbitrary sequential lists of data. This is similar to arrays or slices, but lists are designed to be used @@ -813,45 +993,54 @@ $myList := list 1 2 3 4 5 The above creates a list of `[1 2 3 4 5]`. -## first, mustFirst - -To get the head item on a list, use `first`. + + + + + -## rest, mustRest - -To get the tail of the list (everything but the first item), use `rest`. + + + + -## last, mustLast - -To get the last item on a list, use `last`: + + + + -## initial, mustInitial - -This compliments `last` by returning all _but_ the last element. + + + + -## append, mustAppend - -Append a new item to an existing list, creating a new list. + + + + -## prepend, mustPrepend - -Push an element onto the front of a list, creating a new list. + + + + -## concat - -Concatenate arbitrary number of lists into one. + + + + -## reverse, mustReverse - -Produce a new list with the reversed elements of the given list. + + + + -## uniq, mustUniq - -Generate a list with all of the duplicates removed. + + + + -## without, mustWithout - -The `without` function filters items out of a list. + + + + -## has, mustHas - -Test to see if a list has a particular element. + + + + -## compact, mustCompact - -Accepts a list and removes entries with empty values. + + + + -## slice, mustSlice - -To get partial elements of a list, use `slice list [n] [m]`. It is + + + + -## chunk - -To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. + + + + +
first, mustFirstTo get the head item on a list, use `first`. `first $myList` returns `1` `first` panics if there is a problem while `mustFirst` returns an error to the template engine if there is a problem. +
rest, mustRestTo get the tail of the list (everything but the first item), use `rest`. `rest $myList` returns `[2 3 4 5]` `rest` panics if there is a problem while `mustRest` returns an error to the template engine if there is a problem. +
last, mustLastTo get the last item on a list, use `last`: `last $myList` returns `5`. This is roughly analogous to reversing a list and then calling `first`. `last` panics if there is a problem while `mustLast` returns an error to the template engine if there is a problem. +
initial, mustInitialThis compliments `last` by returning all _but_ the last element. `initial $myList` returns `[1 2 3 4]`. `initial` panics if there is a problem while `mustInitial` returns an error to the template engine if there is a problem. +
append, mustAppendAppend a new item to an existing list, creating a new list. ``` $new = append $myList 6 @@ -861,10 +1050,12 @@ The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. `append` panics if there is a problem while `mustAppend` returns an error to the template engine if there is a problem. +
prepend, mustPrependPush an element onto the front of a list, creating a new list. ``` prepend $myList 0 @@ -874,20 +1065,24 @@ The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. `prepend` panics if there is a problem while `mustPrepend` returns an error to the template engine if there is a problem. +
concatConcatenate arbitrary number of lists into one. ``` concat $myList ( list 6 7 ) ( list 8 ) ``` The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. +
reverse, mustReverseProduce a new list with the reversed elements of the given list. ``` reverse $myList @@ -897,10 +1092,12 @@ The above would generate the list `[5 4 3 2 1]`. `reverse` panics if there is a problem while `mustReverse` returns an error to the template engine if there is a problem. +
uniq, mustUniqGenerate a list with all of the duplicates removed. ``` list 1 1 1 2 | uniq @@ -910,10 +1107,12 @@ The above would produce `[1 2]` `uniq` panics if there is a problem while `mustUniq` returns an error to the template engine if there is a problem. +
without, mustWithoutThe `without` function filters items out of a list. ``` without $myList 3 @@ -931,10 +1130,12 @@ That would produce `[2 4]` `without` panics if there is a problem while `mustWithout` returns an error to the template engine if there is a problem. +
has, mustHasTest to see if a list has a particular element. ``` has 4 $myList @@ -944,10 +1145,12 @@ The above would return `true`, while `has "hello" $myList` would return false. `has` panics if there is a problem while `mustHas` returns an error to the template engine if there is a problem. +
compact, mustCompactAccepts a list and removes entries with empty values. ``` $list := list 1 "a" "foo" "" @@ -958,10 +1161,12 @@ $copy := compact $list `compact` panics if there is a problem and `mustCompact` returns an error to the template engine if there is a problem. +
slice, mustSliceTo get partial elements of a list, use `slice list [n] [m]`. It is equivalent of `list[n:m]`. - `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. @@ -971,23 +1176,29 @@ equivalent of `list[n:m]`. `slice` panics if there is a problem while `mustSlice` returns an error to the template engine if there is a problem. +
chunkTo split a list into chunks of given size, use `chunk size list`. This is useful for pagination. ``` chunk 3 (list 1 2 3 4 5 6 7 8) ``` This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. +
-## A Note on List Internals +### A Note on List Internals A list is implemented in Go as a `[]interface{}`. For Go developers embedding Sprig, you may pass `[]interface{}` items into your template context and be able to use all of the `list` functions on those items. -# Dictionaries and Dict Functions + +## Dictionaries and Dict Functions Sprig provides a key/value storage type called a `dict` (short for "dictionary", as in Python). A `dict` is an _unorder_ type. @@ -998,9 +1209,10 @@ type, even another `dict` or `list`. Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will modify the contents of a dictionary. -## dict - -Creating dictionaries is done by calling the `dict` function and passing it a + + + + + -## get - -Given a map and a key, get the value from the map. + + + + -## set - -Use `set` to add a new key/value pair to a dictionary. + + + + -## unset - -Given a map and a key, delete the key from the map. + + + + -## hasKey - -The `hasKey` function returns `true` if the given dict contains the given key. + + + + -## pluck - -The `pluck` function makes it possible to give one key and multiple maps, and + + + + -## dig - -The `dig` function traverses a nested set of dicts, selecting keys from a list + + + + -## keys - -The `keys` function will return a `list` of all of the keys in one or more `dict` + + + + -## pick - -The `pick` function selects just the given keys out of a dictionary, creating a + + + + -## omit - -The `omit` function is similar to `pick`, except it returns a new `dict` with all + + + + -## values - -The `values` function is similar to `keys`, except it returns a new `list` with + + + + +
dictCreating dictionaries is done by calling the `dict` function and passing it a list of pairs. The following creates a dictionary with three items: @@ -1008,10 +1220,12 @@ The following creates a dictionary with three items: ``` $myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" ``` +
getGiven a map and a key, get the value from the map. ``` get $myDict "name1" @@ -1021,10 +1235,12 @@ The above returns `"value1"` Note that if the key is not found, this operation will simply return `""`. No error will be generated. +
setUse `set` to add a new key/value pair to a dictionary. ``` $_ := set $myDict "name4" "value4" @@ -1032,10 +1248,12 @@ $_ := set $myDict "name4" "value4" Note that `set` _returns the dictionary_ (a requirement of Go template functions), so you may need to trap the value as done above with the `$_` assignment. +
unsetGiven a map and a key, delete the key from the map. ``` $_ := unset $myDict "name4" @@ -1045,20 +1263,24 @@ As with `set`, this returns the dictionary. Note that if the key is not found, this operation will simply return. No error will be generated. +
hasKeyThe `hasKey` function returns `true` if the given dict contains the given key. ``` hasKey $myDict "name1" ``` If the key is not found, this returns `false`. +
pluckThe `pluck` function makes it possible to give one key and multiple maps, and get a list of all of the matches: ``` @@ -1076,10 +1298,12 @@ inserted. A common idiom in Sprig templates is to uses `pluck... | first` to get the first matching key out of a collection of dictionaries. +
digThe `dig` function traverses a nested set of dicts, selecting keys from a list of values. It returns a default value if any of the keys are not found at the associated dict. @@ -1107,10 +1331,12 @@ especially since Go's template package's `and` doesn't shortcut. For instance `a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) `dig` accepts its dict argument last in order to support pipelining. +
keysThe `keys` function will return a `list` of all of the keys in one or more `dict` types. Since a dictionary is _unordered_, the keys will not be in a predictable order. They can be sorted with `sortAlpha`. @@ -1124,10 +1350,12 @@ function along with `sortAlpha` to get a unqiue, sorted list of keys. ``` keys $myDict $myOtherDict | uniq | sortAlpha ``` +
pickThe `pick` function selects just the given keys out of a dictionary, creating a new `dict`. ``` @@ -1135,10 +1363,12 @@ $new := pick $myDict "name1" "name2" ``` The above returns `{name1: value1, name2: value2}` +
omitThe `omit` function is similar to `pick`, except it returns a new `dict` with all the keys that _do not_ match the given keys. ``` @@ -1146,10 +1376,12 @@ $new := omit $myDict "name1" "name3" ``` The above returns `{name2: value2}` +
valuesThe `values` function is similar to `keys`, except it returns a new `list` with all the values of the source `dict` (only one dictionary is supported). ``` @@ -1159,48 +1391,68 @@ $vals := values $myDict The above returns `list["value1", "value2", "value 3"]`. Note that the `values` function gives no guarantees about the result ordering- if you care about this, then use `sortAlpha`. -# Type Conversion Functions +
+ +## Type Conversion Functions The following type conversion functions are provided by Sprig: -- `atoi`: Convert a string to an integer. -- `float64`: Convert to a `float64`. -- `int`: Convert to an `int` at the system's width. -- `int64`: Convert to an `int64`. -- `toDecimal`: Convert a unix octal to a `int64`. -- `toString`: Convert to a string. -- `toStrings`: Convert a list, slice, or array to a list of strings. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
atoiConvert a string to an integer. +
float64Convert to a `float64`. +
intConvert to an `int` at the system's width. +
int64Convert to an `int64`. +
toDecimalConvert a unix octal to a `int64`. +
toStringConvert to a string. +
toStringsConvert a list, slice, or array to a list of strings. +
Only `atoi` requires that the input be a specific type. The others will attempt to convert from any type to the destination type. For example, `int64` can convert floats to ints, and it can also convert strings to ints. -## toStrings - -Given a list-like collection, produce a slice of strings. - -``` -list 1 2 3 | toStrings -``` - -The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns -them as a list. - -## toDecimal - -Given a unix octal permission, produce a decimal. - -``` -"0777" | toDecimal -``` - -The above converts `0777` to `511` and returns the value as an int64. -# Path and Filepath Functions +## Path and Filepath Functions While Sprig does not grant access to the filesystem, it does provide functions for working with strings that follow file path conventions. -## Paths +### Paths Paths separated by the slash character (`/`), processed by the `path` package. @@ -1214,46 +1466,58 @@ Examples: [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): `https://example.com/some/content/`, `ftp://example.com/file/`. -### base - -Return the last element of a path. + + + + + -### dir - -Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` + + + + -### clean - -Clean up a path. + + + + -### ext - -Return the file extension. + + + + -### isAbs + + + + +
baseReturn the last element of a path. ``` base "foo/bar/baz" ``` The above prints "baz". +
dirReturn the directory, stripping the last part of the path. So `dir "foo/bar/baz"` returns `foo/bar`. +
cleanClean up a path. ``` clean "foo/bar/../baz" ``` The above resolves the `..` and returns `foo/baz`. +
extReturn the file extension. ``` ext "foo.bar" ``` The above returns `.bar`. +
isAbsTo check whether a path is absolute, use `isAbs`. +
-To check whether a path is absolute, use `isAbs`. - -## Filepaths +### Filepaths Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package. @@ -1267,9 +1531,10 @@ Examples: the filesystem path is separated by the backslash character (`\`): `C:\Users\Username\`, `C:\Program Files\Application\`; -### osBase - -Return the last element of a filepath. + + + + + -### osDir - -Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` + + + + -### osClean - -Clean up a path. + + + + -### osExt - -Return the file extension. + + + + -### osIsAbs + + + + +
osBaseReturn the last element of a filepath. ``` osBase "/foo/bar/baz" @@ -1277,16 +1542,20 @@ osBase "C:\\foo\\bar\\baz" ``` The above prints "baz" on Linux and Windows, respectively. +
osDirReturn the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` returns `C:\\foo\\bar` on Windows. +
osCleanClean up a path. ``` osClean "/foo/bar/../baz" @@ -1294,10 +1563,12 @@ osClean "C:\\foo\\bar\\..\\baz" ``` The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. +
osExtReturn the file extension. ``` osExt "/foo.bar" @@ -1305,31 +1576,50 @@ osExt "C:\\foo.bar" ``` The above returns `.bar` on Linux and Windows, respectively. +
osIsAbsTo check whether a file path is absolute, use `osIsAbs`. +
-To check whether a file path is absolute, use `osIsAbs`. -# Flow Control Functions +## Flow Control Functions -## fail - -Unconditionally returns an empty `string` and an `error` with the specified + + + + + +
failUnconditionally returns an empty `string` and an `error` with the specified text. This is useful in scenarios where other conditionals have determined that template rendering should fail. ``` fail "Please accept the end user license agreement" ``` -# UUID Functions +
+ +## UUID Functions Sprig can generate UUID v4 universally unique IDs. + + + + + +
uuidv4 ``` uuidv4 ``` The above returns a new UUID of the v4 (randomly generated) type. -# Reflection Functions +
+ +## Reflection Functions Sprig provides rudimentary reflection tools. These help advanced template developers understand the underlying Go type information for a particular value. @@ -1340,37 +1630,65 @@ Go has an open _type_ system that allows developers to create their own types. Sprig provides a set of functions for each. -## Kind Functions +### Kind Functions -There are two Kind functions: `kindOf` returns the kind of an object. + + + + + + + + + + +
kindOfReturns the kind of an object. ``` kindOf "hello" ``` -The above would return `string`. For simple tests (like in `if` blocks), the -`kindIs` function will let you verify that a value is a particular kind: +The above would return `string`. +
kindIsFor simple tests (like in `if` blocks), the `kindIs` function will let you verify that a value is a particular kind: ``` kindIs "int" 123 ``` The above will return `true` +
-## Type Functions +### Type Functions Types are slightly harder to work with, so there are three different functions: -- `typeOf` returns the underlying type of a value: `typeOf $foo` -- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` -- `typeIsLike` works as `typeIs`, except that it also dereferences pointers. + + + + + + + + + + + + + + + +
typeOfReturns the underlying type of a value: `typeOf $foo` +
typeIsLike `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` +
typeIsLikeWorks as `typeIs`, except that it also dereferences pointers. +
**Note:** None of these can test whether or not something implements a given interface, since doing so would require compiling the interface in ahead of time. -## deepEqual - -`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) + + + + + +
deepEqualReturns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) Works for non-primitive types as well (compared to the built-in `eq`). @@ -1379,21 +1697,28 @@ deepEqual (list 1 2 3) (list 1 2 3) ``` The above will return `true` -# Cryptographic and Security Functions +
+ +## Cryptographic and Security Functions Sprig provides a couple of advanced cryptographic functions. -## sha1sum - -The `sha1sum` function receives a string, and computes it's SHA1 digest. + + + + + -## sha256sum - -The `sha256sum` function receives a string, and computes it's SHA256 digest. + + + + -## sha512sum - -The `sha512sum` function receives a string, and computes it's SHA512 digest. + + + + -## adler32sum - -The `adler32sum` function receives a string, and computes its Adler-32 checksum. + + + + +
sha1sumThe `sha1sum` function receives a string, and computes it's SHA1 digest. ``` sha1sum "Hello world!" ``` +
sha256sumThe `sha256sum` function receives a string, and computes it's SHA256 digest. ``` sha256sum "Hello world!" @@ -1401,10 +1726,12 @@ sha256sum "Hello world!" The above will compute the SHA 256 sum in an "ASCII armored" format that is safe to print. +
sha512sumThe `sha512sum` function receives a string, and computes it's SHA512 digest. ``` sha512sum "Hello world!" @@ -1412,18 +1739,26 @@ sha512sum "Hello world!" The above will compute the SHA 512 sum in an "ASCII armored" format that is safe to print. +
adler32sumThe `adler32sum` function receives a string, and computes its Adler-32 checksum. ``` adler32sum "Hello world!" ``` -# URL Functions +
-## urlParse -Parses string for URL and produces dict with URL parts +## URL Functions + + + + + + -## urlJoin -Joins map (produced by `urlParse`) to produce URL string + + + + +
urlParseParses string for URL and produces dict with URL parts ``` urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" @@ -1441,9 +1776,12 @@ userinfo: 'admin:secret' ``` For more info, check https://golang.org/pkg/net/url/#URL +
urlJoinJoins map (produced by `urlParse`) to produce URL string ``` urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") @@ -1453,3 +1791,6 @@ The above returns the following string: ``` proto://host:80/path?query#fragment ``` +
diff --git a/server/server.go b/server/server.go index c6991ba8..7bad3fde 100644 --- a/server/server.go +++ b/server/server.go @@ -1183,7 +1183,7 @@ func (s *Server) replaceTemplate(tpl string, source string) (string, error) { if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON } - t, err := template.New("").Funcs(sprig.FuncMap()).Parse(tpl) + t, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(tpl) if err != nil { return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error()) } @@ -2111,32 +2111,3 @@ func (s *Server) updateAndWriteStats(messagesCount int64) { } }() } - -func loadTemplatesFromDir(dir string) (map[string]*template.Template, error) { - templates := make(map[string]*template.Template) - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - if !strings.HasSuffix(name, ".tmpl") { - continue - } - path := filepath.Join(dir, name) - content, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read template %s: %w", name, err) - } - tmpl, err := template.New(name).Funcs(sprig.FuncMap()).Parse(string(content)) - if err != nil { - return nil, fmt.Errorf("failed to parse template %s: %w", name, err) - } - base := strings.TrimSuffix(name, ".tmpl") - templates[base] = tmpl - } - return templates, nil -} diff --git a/server/templates/github.yml b/server/templates/github.yml index 92f3ab13..5d1b0b46 100644 --- a/server/templates/github.yml +++ b/server/templates/github.yml @@ -1,31 +1,56 @@ title: | - {{- if .pull_request }} - Pull request {{ .action }}: #{{ .pull_request.number }} {{ .pull_request.title }} - {{- else if and .starred_at (eq .action "created")}} - ⭐ {{ .sender.login }} starred {{ .repository.full_name }} + {{- if and .starred_at (eq .action "created")}} + ⭐ {{ .sender.login }} starred {{ .repository.name }} + + {{- else if and .repository (eq .action "started")}} + 👀 {{ .sender.login }} started watching {{ .repository.name }} + {{- else if and .comment (eq .action "created") }} - 💬 New comment on issue #{{ .issue.number }} — {{ .issue.title }} + 💬 New comment on #{{ .issue.number }}: {{ .issue.title }} + + {{- else if .pull_request }} + 🔀 Pull request {{ .action }}: #{{ .pull_request.number }} {{ .pull_request.title }} + + {{- else if .issue }} + 🐛 Issue {{ .action }}: #{{ .issue.number }} {{ .issue.title }} + {{- else }} - Unsupported GitHub event type or action. + {{ fail "Unsupported GitHub event type or action." }} {{- end }} message: | - {{- if .pull_request }} - Repository: {{ .repository.full_name }}, branch {{ .pull_request.head.ref }} → {{ .pull_request.base.ref }} - Created by: {{ .pull_request.user.login }} - Link: {{ .pull_request.html_url }} - {{ if .pull_request.body }}Description: - {{ .pull_request.body }}{{ end }} - {{- else if and .starred_at (eq .action "created")}} - ⭐ {{ .sender.login }} starred {{ .repository.full_name }} - 📦 {{ .repository.description | default "(no description)" }} - 🔗 {{ .repository.html_url }} - 📅 {{ .starred_at }} + {{ if and .starred_at (eq .action "created")}} + Stargazer: {{ .sender.html_url }} + Repository: {{ .repository.html_url }} + + {{- else if and .repository (eq .action "started")}} + Watcher: {{ .sender.html_url }} + Repository: {{ .repository.html_url }} + {{- else if and .comment (eq .action "created") }} - 💬 New comment on issue #{{ .issue.number }} — {{ .issue.title }} - 📦 {{ .repository.full_name }} - 👤 {{ .comment.user.login }} - 🔗 {{ .comment.html_url }} - 📝 {{ .comment.body | default "(no comment body)" }} + Commenter: {{ .comment.user.html_url }} + Repository: {{ .repository.html_url }} + Comment link: {{ .comment.html_url }} + {{ if .comment.body }} + Comment: + {{ .comment.body | trunc 2000 }}{{ end }} + + {{- else if .pull_request }} + Branch: {{ .pull_request.head.ref }} → {{ .pull_request.base.ref }} + {{ .action | title }} by: {{ .pull_request.user.html_url }} + Repository: {{ .repository.html_url }} + Pull request: {{ .pull_request.html_url }} + {{ if .pull_request.body }} + Description: + {{ .pull_request.body | trunc 2000 }}{{ end }} + + {{- else if .issue }} + {{ .action | title }} by: {{ .issue.user.html_url }} + Repository: {{ .repository.html_url }} + Issue link: {{ .issue.html_url }} + {{ if .issue.body }} + Description: + {{ .issue.body | trunc 2000 }}{{ end }} + {{- else }} {{ fail "Unsupported GitHub event type or action." }} {{- end }} diff --git a/server/testdata/webhook_github_issue_opened.json b/server/testdata/webhook_github_issue_opened.json new file mode 100644 index 00000000..1b3e74c0 --- /dev/null +++ b/server/testdata/webhook_github_issue_opened.json @@ -0,0 +1,216 @@ +{ + "action": "opened", + "issue": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391", + "repository_url": "https://api.github.com/repos/binwiederhier/ntfy", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/labels{/name}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/comments", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/events", + "html_url": "https://github.com/binwiederhier/ntfy/issues/1391", + "id": 3236389051, + "node_id": "I_kwDOGRBhi87A52C7", + "number": 1391, + "title": "http 500 error (ntfy error 50001)", + "user": { + "login": "TheUser-dev", + "id": 213207407, + "node_id": "U_kgDODLVJbw", + "avatar_url": "https://avatars.githubusercontent.com/u/213207407?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/TheUser-dev", + "html_url": "https://github.com/TheUser-dev", + "followers_url": "https://api.github.com/users/TheUser-dev/followers", + "following_url": "https://api.github.com/users/TheUser-dev/following{/other_user}", + "gists_url": "https://api.github.com/users/TheUser-dev/gists{/gist_id}", + "starred_url": "https://api.github.com/users/TheUser-dev/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/TheUser-dev/subscriptions", + "organizations_url": "https://api.github.com/users/TheUser-dev/orgs", + "repos_url": "https://api.github.com/users/TheUser-dev/repos", + "events_url": "https://api.github.com/users/TheUser-dev/events{/privacy}", + "received_events_url": "https://api.github.com/users/TheUser-dev/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 3480884102, + "node_id": "LA_kwDOGRBhi87PehOG", + "url": "https://api.github.com/repos/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug", + "name": "🪲 bug", + "color": "d73a4a", + "default": false, + "description": "Something isn't working" + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + ], + "milestone": null, + "comments": 0, + "created_at": "2025-07-16T15:20:56Z", + "updated_at": "2025-07-16T15:20:56Z", + "closed_at": null, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": ":lady_beetle: **Describe the bug**\nWhen sending a notification (especially when it happens with multiple requests) this error occurs\n\n:computer: **Components impacted**\nntfy server 2.13.0 in docker, debian 12 arm64\n\n:bulb: **Screenshots and/or logs**\n```\nclosed with HTTP 500 (ntfy error 50001) (error=database table is locked, http_method=POST, http_path=/_matrix/push/v1/notify, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=30, visitor_id=ip:, visitor_ip=, visitor_messages=448, visitor_messages_limit=17280, visitor_messages_remaining=16832, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=57.049697891799994, visitor_seen=2025-07-16T15:06:35.429Z)\n```\n\n:crystal_ball: **Additional context**\nLooks like this has already been fixed by #498, regression?\n", + "reactions": { + "url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + "repository": { + "id": 420503947, + "node_id": "R_kgDOGRBhiw", + "name": "ntfy", + "full_name": "binwiederhier/ntfy", + "private": false, + "owner": { + "login": "binwiederhier", + "id": 664597, + "node_id": "MDQ6VXNlcjY2NDU5Nw==", + "avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/binwiederhier", + "html_url": "https://github.com/binwiederhier", + "followers_url": "https://api.github.com/users/binwiederhier/followers", + "following_url": "https://api.github.com/users/binwiederhier/following{/other_user}", + "gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}", + "starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions", + "organizations_url": "https://api.github.com/users/binwiederhier/orgs", + "repos_url": "https://api.github.com/users/binwiederhier/repos", + "events_url": "https://api.github.com/users/binwiederhier/events{/privacy}", + "received_events_url": "https://api.github.com/users/binwiederhier/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/binwiederhier/ntfy", + "description": "Send push notifications to your phone or desktop using PUT/POST", + "fork": false, + "url": "https://api.github.com/repos/binwiederhier/ntfy", + "forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks", + "keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams", + "hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks", + "issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}", + "events_url": "https://api.github.com/repos/binwiederhier/ntfy/events", + "assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}", + "branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}", + "tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags", + "blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}", + "languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages", + "stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers", + "contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors", + "subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers", + "subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription", + "commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}", + "compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges", + "archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads", + "issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}", + "pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}", + "milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}", + "notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}", + "releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}", + "deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments", + "created_at": "2021-10-23T19:25:32Z", + "updated_at": "2025-07-16T14:54:16Z", + "pushed_at": "2025-07-16T11:49:26Z", + "git_url": "git://github.com/binwiederhier/ntfy.git", + "ssh_url": "git@github.com:binwiederhier/ntfy.git", + "clone_url": "https://github.com/binwiederhier/ntfy.git", + "svn_url": "https://github.com/binwiederhier/ntfy", + "homepage": "https://ntfy.sh", + "size": 36831, + "stargazers_count": 25112, + "watchers_count": 25112, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 984, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 369, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "curl", + "notifications", + "ntfy", + "ntfysh", + "pubsub", + "push-notifications", + "rest-api" + ], + "visibility": "public", + "forks": 984, + "open_issues": 369, + "watchers": 25112, + "default_branch": "main" + }, + "sender": { + "login": "TheUser-dev", + "id": 213207407, + "node_id": "U_kgDODLVJbw", + "avatar_url": "https://avatars.githubusercontent.com/u/213207407?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/TheUser-dev", + "html_url": "https://github.com/TheUser-dev", + "followers_url": "https://api.github.com/users/TheUser-dev/followers", + "following_url": "https://api.github.com/users/TheUser-dev/following{/other_user}", + "gists_url": "https://api.github.com/users/TheUser-dev/gists{/gist_id}", + "starred_url": "https://api.github.com/users/TheUser-dev/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/TheUser-dev/subscriptions", + "organizations_url": "https://api.github.com/users/TheUser-dev/orgs", + "repos_url": "https://api.github.com/users/TheUser-dev/repos", + "events_url": "https://api.github.com/users/TheUser-dev/events{/privacy}", + "received_events_url": "https://api.github.com/users/TheUser-dev/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } +} diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 68ef516d..1cd026c6 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -2,40 +2,26 @@ package sprig import ( "errors" - "html/template" + "golang.org/x/text/cases" + "golang.org/x/text/language" "math/rand" "path" "path/filepath" "reflect" "strconv" "strings" - ttemplate "text/template" + "text/template" "time" - - "golang.org/x/text/cases" ) -// FuncMap produces the function map. +// TxtFuncMap produces the function map. // // Use this to pass the functions into the template engine: // // tpl := template.New("foo").Funcs(sprig.FuncMap())) -func FuncMap() template.FuncMap { - return HTMLFuncMap() -} - +// // TxtFuncMap returns a 'text/template'.FuncMap -func TxtFuncMap() ttemplate.FuncMap { - return GenericFuncMap() -} - -// HTMLFuncMap returns an 'html/template'.Funcmap -func HTMLFuncMap() template.FuncMap { - return GenericFuncMap() -} - -// GenericFuncMap returns a copy of the basic function map as a map[string]any. -func GenericFuncMap() map[string]any { +func TxtFuncMap() template.FuncMap { gfm := make(map[string]any, len(genericMap)) for k, v := range genericMap { gfm[k] = v @@ -63,11 +49,13 @@ var genericMap = map[string]any{ "unixEpoch": unixEpoch, // Strings - "trunc": trunc, - "trim": strings.TrimSpace, - "upper": strings.ToUpper, - "lower": strings.ToLower, - "title": cases.Title, + "trunc": trunc, + "trim": strings.TrimSpace, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": func(s string) string { + return cases.Title(language.English).String(s) + }, "substr": substring, // Switch order so that "foo" | repeat 5 "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, @@ -99,11 +87,6 @@ var genericMap = map[string]any{ "seq": seq, "toDecimal": toDecimal, - //"gt": func(a, b int) bool {return a > b}, - //"gte": func(a, b int) bool {return a >= b}, - //"lt": func(a, b int) bool {return a < b}, - //"lte": func(a, b int) bool {return a <= b}, - // split "/" foo/bar returns map[int]string{0: foo, 1: bar} "split": split, "splitList": func(sep, orig string) []string { return strings.Split(orig, sep) }, From ae62e0d9556381bdc835f803fa55476c381975f1 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 15:37:05 +0200 Subject: [PATCH 176/378] Docs docs docs --- client/options.go | 6 + cmd/publish.go | 5 + cmd/serve.go | 8 +- cmd/user.go | 3 +- docs/publish.md | 196 +++- docs/{ => publish}/template-functions.md | 1041 ++++++----------- docs/sprig.md | 24 - docs/sprig/conversion.md | 36 - docs/sprig/crypto.md | 41 - docs/sprig/date.md | 126 -- docs/sprig/defaults.md | 169 --- docs/sprig/dicts.md | 172 --- docs/sprig/encoding.md | 6 - docs/sprig/flow_control.md | 11 - docs/sprig/integer_slice.md | 41 - docs/sprig/lists.md | 188 --- docs/sprig/math.md | 78 -- docs/sprig/paths.md | 114 -- docs/sprig/reflection.md | 50 - docs/sprig/string_slice.md | 72 -- docs/sprig/strings.md | 309 ----- docs/sprig/url.md | 33 - docs/sprig/uuid.md | 9 - .../android-screenshot-template-custom.png | Bin 0 -> 45032 bytes ...android-screenshot-template-predefined.png | Bin 0 -> 86828 bytes .../img/screenshot-github-webhook-config.png | Bin 0 -> 98734 bytes mkdocs.yml | 1 - server/config.go | 6 +- server/server.yml | 20 + server/server_test.go | 75 +- server/templates/alertmanager.yml | 29 + server/templates/github.yml | 3 +- server/templates/grafana.yml | 16 +- .../testdata/webhook_alertmanager_firing.json | 33 + server/testdata/webhook_grafana_resolved.json | 51 + 35 files changed, 764 insertions(+), 2208 deletions(-) rename docs/{ => publish}/template-functions.md (59%) delete mode 100644 docs/sprig.md delete mode 100644 docs/sprig/conversion.md delete mode 100644 docs/sprig/crypto.md delete mode 100644 docs/sprig/date.md delete mode 100644 docs/sprig/defaults.md delete mode 100644 docs/sprig/dicts.md delete mode 100644 docs/sprig/encoding.md delete mode 100644 docs/sprig/flow_control.md delete mode 100644 docs/sprig/integer_slice.md delete mode 100644 docs/sprig/lists.md delete mode 100644 docs/sprig/math.md delete mode 100644 docs/sprig/paths.md delete mode 100644 docs/sprig/reflection.md delete mode 100644 docs/sprig/string_slice.md delete mode 100644 docs/sprig/strings.md delete mode 100644 docs/sprig/url.md delete mode 100644 docs/sprig/uuid.md create mode 100644 docs/static/img/android-screenshot-template-custom.png create mode 100644 docs/static/img/android-screenshot-template-predefined.png create mode 100644 docs/static/img/screenshot-github-webhook-config.png create mode 100644 server/templates/alertmanager.yml create mode 100644 server/testdata/webhook_alertmanager_firing.json create mode 100644 server/testdata/webhook_grafana_resolved.json diff --git a/client/options.go b/client/options.go index 027b7fb5..f4711834 100644 --- a/client/options.go +++ b/client/options.go @@ -77,6 +77,12 @@ func WithMarkdown() PublishOption { return WithHeader("X-Markdown", "yes") } +// WithTemplate instructs the server to use a specific template for the message. If templateName is is "yes" or "1", +// the server will interpret the message and title as a template. +func WithTemplate(templateName string) PublishOption { + return WithHeader("X-Template", templateName) +} + // WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment func WithFilename(filename string) PublishOption { return WithHeader("X-Filename", filename) diff --git a/cmd/publish.go b/cmd/publish.go index c15761ab..f3139a63 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -32,6 +32,7 @@ var flagsPublish = append( &cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"}, &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"}, &cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"}, + &cli.StringFlag{Name: "template", Aliases: []string{"tpl"}, EnvVars: []string{"NTFY_TEMPLATE"}, Usage: "use templates to transform JSON message body"}, &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"}, &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, @@ -98,6 +99,7 @@ func execPublish(c *cli.Context) error { actions := c.String("actions") attach := c.String("attach") markdown := c.Bool("markdown") + template := c.String("template") filename := c.String("filename") file := c.String("file") email := c.String("email") @@ -146,6 +148,9 @@ func execPublish(c *cli.Context) error { if markdown { options = append(options, client.WithMarkdown()) } + if template != "" { + options = append(options, client.WithTemplate(template)) + } if filename != "" { options = append(options, client.WithFilename(filename)) } diff --git a/cmd/serve.go b/cmd/serve.go index 0cbade0f..f894fe65 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -29,13 +29,9 @@ func init() { commands = append(commands, cmdServe) } -const ( - defaultServerConfigFile = "/etc/ntfy/server.yml" -) - var flagsServe = append( append([]cli.Flag{}, flagsDefault...), - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, Usage: "config file"}, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, Usage: "config file"}, altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used as HTTP listen address"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}), @@ -56,7 +52,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "template-dir", Aliases: []string{"template_dir"}, EnvVars: []string{"NTFY_TEMPLATE_DIR"}, Usage: "directory to load named message templates from"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "template-dir", Aliases: []string{"template_dir"}, EnvVars: []string{"NTFY_TEMPLATE_DIR"}, Value: server.DefaultTemplateDir, Usage: "directory to load named message templates from"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}), diff --git a/cmd/user.go b/cmd/user.go index e6867b11..0ee45bc3 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -6,6 +6,7 @@ import ( "crypto/subtle" "errors" "fmt" + "heckel.io/ntfy/v2/server" "heckel.io/ntfy/v2/user" "os" "strings" @@ -25,7 +26,7 @@ func init() { var flagsUser = append( append([]cli.Flag{}, flagsDefault...), - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"}, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, DefaultText: server.DefaultConfigFile, Usage: "config file"}, altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), ) diff --git a/docs/publish.md b/docs/publish.md index 24aa443a..6410bece 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -944,27 +944,165 @@ Templating lets you **format a JSON message body into human-friendly message and [Go templates](https://pkg.go.dev/text/template) (see tutorials [here](https://blog.gopheracademy.com/advent-2017/using-go-templates/), [here](https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go), and [here](https://developer.hashicorp.com/nomad/tutorials/templates/go-template-syntax)). This is specifically useful when -**combined with webhooks** from services such as GitHub, Grafana, or other services that emit JSON webhooks. +**combined with webhooks** from services such as [GitHub](https://docs.github.com/en/webhooks/about-webhooks), +[Grafana](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/), +[Alertmanager](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config), or other services that emit JSON webhooks. Instead of using a separate bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). -You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`): +You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`, or the query parameter `?template=...`): +* **Pre-defined template files**: Setting the `X-Template` header or query parameter to a template name (e.g. `?template=github`) + to a pre-defined template name (e.g. `github`, `grafana`, or `alertmanager`) will use the template with that name. + See [pre-defined templates](#pre-defined-templates) for more details. +* **Custom template files**: Setting the `X-Template` header or query parameter to a custom template name (e.g. `?template=myapp`) + will use a custom template file from the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`). + See [custom templates](#custom-templates) for more details. * **Inline templating**: Setting the `X-Template` header or query parameter to `yes` or `1` (e.g. `?template=yes`) - will enable inline templating, which means that the `message` and/or `title` **will be parsed as a Go template**. - See [Inline templating](#inline-templating) and [Template syntax](#template-syntax) for details on how to use Go - templates in your messages and titles. -* **Pre-defined template files**: You can also set `X-Template` header or query parameter to a template name (e.g. `?template=github`). - ntfy will then read the template from either the built-in pre-defined template files, or from the template files defined in - the `template-dir`. See [Template files](#pre-defined-templates) for more details. + will enable inline templating, which means that the `message` and/or `title` will be parsed as a Go template. + See [inline templating](#inline-templating) for more details. + +To learn the basics of Go's templating language, please see [template syntax](#template-syntax). + +### Pre-defined templates + +When `X-Template: ` (aliases: `Template: `, `Tpl: `) or `?template=` is set, ntfy will transform the +message and/or title based on one of the built-in pre-defined templates + +The following **pre-defined templates** are available: + +* `github`: Formats a subset of [GitHub webhook](https://docs.github.com/en/webhooks/about-webhooks) payloads (PRs, issues, new star, new watcher, new comment) +* `grafana`: Formats [Grafana webhook](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/) payloads (firing/resolved alerts) +* `alertmanager`: Formats [Alertmanager webhook](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) payloads (firing/resolved alerts) + +Here's an example of how to use the pre-defined `github` template: First, configure the webhook in GitHub to send a webhook to your ntfy topic, e.g. `https://ntfy.sh/mytopic?template=github`. +
+ ![GitHub webhook config](static/img/screenshot-github-webhook-config.png){ width=600 } +
GitHub webhook configuration
+
+ +After that, when GitHub publishes a JSON webhook to the topic, ntfy will transform it according to the template rules +and you'll receive notifications in the ntfy app. Here's an example for when somebody stars your repository: + +
+ ![pre-defined template](static/img/android-screenshot-template-predefined.png){ width=500 } +
Receiving a webhook, formatted using the pre-defined "github" template
+
+ +### Custom templates + +To define **your own custom templates**, place a template file in the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`) +and set the `X-Template` header or query parameter to the name of the template file (without the `.yml` extension). + +For example, if you have a template file `/etc/ntfy/templates/myapp.yml`, you can set the header `X-Template: myapp` or +the query parameter `?template=myapp` to use it. + +Template files must have the `.yml` (not: `.yaml`!) extension and must be formatted as YAML. They may contain `title` and `message` keys, +which are interpreted as Go templates. + +Here's an **example custom template**: + +=== "Custom template (/etc/ntfy/templates/myapp.yml)" + ```yaml + title: | + {{- if eq .status "firing" }} + {{- if gt .percent 90.0 }}🚨 Critical alert + {{- else }}⚠️ Alert{{- end }} + {{- else if eq .status "resolved" }} + ✅ Alert resolved + {{- end }} + message: | + Status: {{ .status }} + Type: {{ .type | upper }} ({{ .percent }}%) + Server: {{ .server }} + ``` + +Once you have the template file in place, you can send the payload to your topic using the `X-Template` +header or query parameter: + +=== "Command line (curl)" + ``` + echo '{"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}' | \ + curl -sT- "https://ntfy.example.com/mytopic?template=myapp" + ``` + +=== "ntfy CLI" + ``` + echo '{"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}' | \ + ntfy publish --template=myapp https://ntfy.example.com/mytopic + ``` + +=== "HTTP" + ``` http + POST /mytopic?template=myapp HTTP/1.1 + Host: ntfy.example.com + + { + "status": "firing", + "type": "cpu", + "server": "ntfy.sh", + "percent": 99 + } + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.example.com/mytopic?template=myapp', { + method: 'POST', + body: '{"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}' + }) + ``` + +=== "Go" + ``` go + payload := `{"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}` + req, _ := http.NewRequest("POST", "https://ntfy.example.com/mytopic?template=myapp", strings.NewReader(payload)) + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + $Request = @{ + Method = "POST" + Uri = "https://ntfy.example.com/mytopic?template=myapp" + Body = '{"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}' + } + Invoke-RestMethod @Request + ``` + +=== "Python" + ``` python + requests.post("https://ntfy.example.com/mytopic?template=myapp", + json={"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.example.com/mytopic?template=myapp', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json", + 'content' => '{"status":"firing","type":"cpu","server":"ntfy.sh","percent":99}' + ] + ])); + ``` + +Which will result in a notification that looks like this: + +
+ ![notification from custom JSON webhook template](static/img/android-screenshot-template-custom.png){ width=500 } +
JSON webhook, transformed using a custom template
+
### Inline templating -When `X-Template: yes` or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your -webhook payload. This is most useful if no [pre-defined template](#pre-defined-templates) exists, for templated one-off messages, -of if you do not control the ntfy server (e.g., if you're using ntfy.sh). Please consider using [template files](#pre-defined-templates) +When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your +webhook payload. + +Inline templates are most useful for templated one-off messages, of if you do not control the ntfy server (e.g., if you're using ntfy.sh). +Consider using [pre-defined templates](#pre-defined-templates) or [custom templates](#custom-templates) instead, if you control the ntfy server, as templates are much easier to maintain. Here's an **example for a Grafana alert**: @@ -1078,10 +1216,6 @@ This example uses the `message`/`m` and `title`/`t` query parameters, but obviou `Message`/`Title` headers. It will send a notification with a title `phil-pc: A severe error has occurred` and a message `Error message: Disk has run out of space`. -### Pre-defined templates - -XXXXXXXXXXXXxx - ### Template syntax ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful, yet also one of the worst templating languages out there. @@ -1101,23 +1235,23 @@ message templating and for transforming the data provided through the JSON paylo Below are the functions that are available to use inside your message/title templates. -* [String Functions](./sprig/strings.md): `trim`, `trunc`, `substr`, `plural`, etc. - * [String List Functions](./sprig/string_slice.md): `splitList`, `sortAlpha`, etc. -* [Integer Math Functions](./sprig/math.md): `add`, `max`, `mul`, etc. - * [Integer List Functions](./sprig/integer_slice.md): `until`, `untilStep` -* [Date Functions](./sprig/date.md): `now`, `date`, etc. -* [Defaults Functions](./sprig/defaults.md): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` -* [Encoding Functions](./sprig/encoding.md): `b64enc`, `b64dec`, etc. -* [Lists and List Functions](./sprig/lists.md): `list`, `first`, `uniq`, etc. -* [Dictionaries and Dict Functions](./sprig/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. -* [Type Conversion Functions](./sprig/conversion.md): `atoi`, `int64`, `toString`, etc. -* [Path and Filepath Functions](./sprig/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` -* [Flow Control Functions]( ./sprig/flow_control.md): `fail` +* [String Functions](publish/template-functions.md#string-functions): `trim`, `trunc`, `substr`, `plural`, etc. +* [String List Functions](publish/template-functions.md#string-list-functions): `splitList`, `sortAlpha`, etc. +* [Integer Math Functions](publish/template-functions.md#integer-math-functions): `add`, `max`, `mul`, etc. +* [Integer List Functions](publish/template-functions.md#integer-list-functions): `until`, `untilStep` +* [Date Functions](publish/template-functions.md#date-functions): `now`, `date`, etc. +* [Defaults Functions](publish/template-functions.md#default-functions): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` +* [Encoding Functions](publish/template-functions.md#encoding-functions): `b64enc`, `b64dec`, etc. +* [Lists and List Functions](publish/template-functions.md#lists-and-list-functions): `list`, `first`, `uniq`, etc. +* [Dictionaries and Dict Functions](publish/template-functions.md#dictionaries-and-dict-functions): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. +* [Type Conversion Functions](publish/template-functions.md#type-conversion-functions): `atoi`, `int64`, `toString`, etc. +* [Path and Filepath Functions](publish/template-functions.md#path-and-filepath-functions): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` +* [Flow Control Functions](publish/template-functions.md#flow-control-functions): `fail` * Advanced Functions - * [UUID Functions](./sprig/uuid.md): `uuidv4` - * [Reflection](./sprig/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. - * [Cryptographic and Security Functions](./sprig/crypto.md): `sha256sum`, etc. - * [URL](./sprig/url.md): `urlParse`, `urlJoin` + * [UUID Functions](publish/template-functions.md#uuid-functions): `uuidv4` + * [Reflection](publish/template-functions.md#reflection-functions): `typeOf`, `kindIs`, `typeIsLike`, etc. + * [Cryptographic and Security Functions](publish/template-functions.md#cryptographic-and-security-functions): `sha256sum`, etc. + * [URL](publish/template-functions.md#url-functions): `urlParse`, `urlJoin` ## Publish as JSON diff --git a/docs/template-functions.md b/docs/publish/template-functions.md similarity index 59% rename from docs/template-functions.md rename to docs/publish/template-functions.md index 7c9593e6..238bddd9 100644 --- a/docs/template-functions.md +++ b/docs/publish/template-functions.md @@ -1,4 +1,8 @@ -# Template functions +# Template Functions + +These template functions may be used in the [message template](../publish.md#message-templating) feature of ntfy. Please refer to the examples in the documentation for how to use them. + +The original set of template functions is based on the [Sprig library](https://masterminds.github.io/sprig/). This documentation page is a (slightly modified) copy of their docs. **Thank you to the Sprig developers for their work!** 🙏 ## Table of Contents @@ -23,106 +27,89 @@ Sprig has a number of string manipulation functions. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
trimThe `trim` function removes space from either side of a string: +### trim + +The `trim` function removes space from either side of a string: ``` trim " hello " ``` The above produces `hello` -
trimAllRemove given characters from the front or back of a string: +### trimAll + +Remove given characters from the front or back of a string: ``` trimAll "$" "$5.00" ``` The above returns `5.00` (as a string). -
trimSuffixTrim just the suffix from a string: +### trimSuffix + +Trim just the suffix from a string: ``` trimSuffix "-" "hello-" ``` The above returns `hello` -
trimPrefixTrim just the prefix from a string: +### trimPrefix + +Trim just the prefix from a string: ``` trimPrefix "-" "-hello" ``` The above returns `hello` -
upperConvert the entire string to uppercase: +### upper + +Convert the entire string to uppercase: ``` upper "hello" ``` The above returns `HELLO` -
lowerConvert the entire string to lowercase: +### lower + +Convert the entire string to lowercase: ``` lower "HELLO" ``` The above returns `hello` -
titleConvert to title case: +### title + +Convert to title case: ``` title "hello world" ``` The above returns `Hello World` -
repeatRepeat a string multiple times: +### repeat + +Repeat a string multiple times: ``` repeat 3 "hello" ``` The above returns `hellohellohello` -
substrGet a substring from a string. It takes three parameters: +### substr + +Get a substring from a string. It takes three parameters: - start (int) - end (int) @@ -133,12 +120,10 @@ substr 0 5 "hello world" ``` The above returns `hello` -
truncTruncate a string (and add no suffix) +### trunc + +Truncate a string (and add no suffix) ``` trunc 5 "hello world" @@ -151,24 +136,20 @@ trunc -5 "hello world" ``` The above produces `world`. -
containsTest to see if one string is contained inside of another: +### contains + +Test to see if one string is contained inside of another: ``` contains "cat" "catch" ``` The above returns `true` because `catch` contains `cat`. -
hasPrefix and hasSuffixThe `hasPrefix` and `hasSuffix` functions test whether a string has a given +### hasPrefix and hasSuffix + +The `hasPrefix` and `hasSuffix` functions test whether a string has a given prefix or suffix: ``` @@ -176,19 +157,15 @@ hasPrefix "cat" "catch" ``` The above returns `true` because `catch` has the prefix `cat`. -
quote and squoteThese functions wrap a string in double quotes (`quote`) or single quotes +### quote and squote + +These functions wrap a string in double quotes (`quote`) or single quotes (`squote`). -
catThe `cat` function concatenates multiple strings together into one, separating +### cat + +The `cat` function concatenates multiple strings together into one, separating them with spaces: ``` @@ -196,12 +173,10 @@ cat "hello" "beautiful" "world" ``` The above produces `hello beautiful world` -
indentThe `indent` function indents every line in a given string to the specified +### indent + +The `indent` function indents every line in a given string to the specified indent width. This is useful when aligning multi-line strings: ``` @@ -209,12 +184,10 @@ indent 4 $lots_of_text ``` The above will indent every line of text by 4 space characters. -
nindentThe `nindent` function is the same as the indent function, but prepends a new +### nindent + +The `nindent` function is the same as the indent function, but prepends a new line to the beginning of the string. ``` @@ -223,12 +196,10 @@ nindent 4 $lots_of_text The above will indent every line of text by 4 space characters and add a new line to the beginning. -
replacePerform simple string replacement. +### replace + +Perform simple string replacement. It takes three arguments: @@ -241,12 +212,10 @@ It takes three arguments: ``` The above will produce `I-Am-Henry-VIII` -
pluralPluralize a string. +### plural + +Pluralize a string. ``` len $fish | plural "one anchovy" "many anchovies" @@ -266,12 +235,10 @@ NOTE: Sprig does not currently support languages with more complex pluralization rules. And `0` is considered a plural because the English language treats it as such (`zero anchovies`). The Sprig developers are working on a solution for better internationalization. -
regexMatch, mustRegexMatchReturns true if the input string contains any match of the regular expression. +### regexMatch, mustRegexMatch + +Returns true if the input string contains any match of the regular expression. ``` regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" @@ -281,12 +248,10 @@ The above produces `true` `regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the template engine if there is a problem. -
regexFindAll, mustRegexFindAllReturns a slice of all matches of the regular expression in the input string. +### regexFindAll, mustRegexFindAll + +Returns a slice of all matches of the regular expression in the input string. The last parameter n determines the number of substrings to return, where -1 means return all matches ``` @@ -297,12 +262,10 @@ The above produces `[2 4 6 8]` `regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the template engine if there is a problem. -
regexFind, mustRegexFindReturn the first (left most) match of the regular expression in the input string +### regexFind, mustRegexFind + +Return the first (left most) match of the regular expression in the input string ``` regexFind "[a-zA-Z][1-9]" "abcd1234" @@ -312,12 +275,10 @@ The above produces `d1` `regexFind` panics if there is a problem and `mustRegexFind` returns an error to the template engine if there is a problem. -
regexReplaceAll, mustRegexReplaceAllReturns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. +### regexReplaceAll, mustRegexReplaceAll + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch ``` @@ -328,12 +289,10 @@ The above produces `-W-xxW-` `regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the template engine if there is a problem. -
regexReplaceAllLiteral, mustRegexReplaceAllLiteralReturns a copy of the input string, replacing matches of the Regexp with the replacement string replacement +### regexReplaceAllLiteral, mustRegexReplaceAllLiteral + +Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement The replacement string is substituted directly, without using Expand ``` @@ -344,12 +303,10 @@ The above produces `-${1}-${1}-` `regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the template engine if there is a problem. -
regexSplit, mustRegexSplitSlices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches +### regexSplit, mustRegexSplit + +Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches ``` regexSplit "z+" "pizza" -1 @@ -359,12 +316,10 @@ The above produces `[pi a]` `regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the template engine if there is a problem. -
regexQuoteMetaReturns a string that escapes all regular expression metacharacters inside the argument text; +### regexQuoteMeta + +Returns a string that escapes all regular expression metacharacters inside the argument text; the returned string is a regular expression matching the literal text. ``` @@ -372,22 +327,20 @@ regexQuoteMeta "1.2.3" ``` The above produces `1\.2\.3` -
-The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains +### See Also... + +The [Conversion Functions](#type-conversion-functions) contain functions for converting strings. The [String List Functions](#string-list-functions) contains functions for working with an array of strings. ## String List Functions -These function operate on or generate slices of strings. In Go, a slice is a +These functions operate on or generate slices of strings. In Go, a slice is a growable array. In Sprig, it's a special case of a `list`. - - - - - - - - - - - - - - - - - -
joinJoin a list of strings into a single string, with the given separator. +### join + +Join a list of strings into a single string, with the given separator. ``` list "hello" "world" | join "_" @@ -402,12 +355,10 @@ list 1 2 3 | join "+" ``` The above will produce `1+2+3` -
splitList and splitSplit a string into a list of strings: +### splitList and split + +Split a string into a list of strings: ``` splitList "$" "foo$bar$baz" @@ -429,12 +380,10 @@ $a._0 ``` The above produces `foo` -
splitn`splitn` function splits a string into a `dict` with `n` keys. It is designed to make +### splitn + +`splitn` function splits a string into a `dict` with `n` keys. It is designed to make it easy to use template dot notation for accessing members: ``` @@ -448,132 +397,99 @@ $a._0 ``` The above produces `foo` -
sortAlphaThe `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) +### sortAlpha + +The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) order. It does _not_ sort in place, but returns a sorted copy of the list, in keeping with the immutability of lists. -
## Integer Math Functions The following math functions operate on `int64` values. - - - - - - - - - +### add1 - - - - +To increment by 1, use `add1` - - - - +### sub - - - - +To subtract, use `sub` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
addSum numbers with `add`. Accepts two or more inputs. +### add + +Sum numbers with `add`. Accepts two or more inputs. ``` add 1 2 3 ``` -
add1To increment by 1, use `add1` -
subTo subtract, use `sub` -
divPerform integer division with `div` -
modModulo with `mod` -
mulMultiply with `mul`. Accepts two or more inputs. +### div + +Perform integer division with `div` + +### mod + +Modulo with `mod` + +### mul + +Multiply with `mul`. Accepts two or more inputs. ``` mul 1 2 3 ``` -
maxReturn the largest of a series of integers: +### max + +Return the largest of a series of integers: This will return `3`: ``` max 1 2 3 ``` -
minReturn the smallest of a series of integers. +### min + +Return the smallest of a series of integers. `min 1 2 3` will return `1` -
floorReturns the greatest float value less than or equal to input value +### floor + +Returns the greatest float value less than or equal to input value `floor 123.9999` will return `123.0` -
ceilReturns the greatest float value greater than or equal to input value +### ceil + +Returns the greatest float value greater than or equal to input value `ceil 123.001` will return `124.0` -
roundReturns a float value with the remainder rounded to the given number to digits after the decimal point. +### round + +Returns a float value with the remainder rounded to the given number to digits after the decimal point. `round 123.555555 3` will return `123.556` -
randIntReturns a random integer value from min (inclusive) to max (exclusive). +### randInt +Returns a random integer value from min (inclusive) to max (exclusive). ``` randInt 12 30 ``` The above will produce a random number in the range [12,30]. -
## Integer List Functions - - - - - - - - - - - - - -
untilThe `until` function builds a range of integers. +### until + +The `until` function builds a range of integers. ``` until 5 @@ -582,12 +498,10 @@ until 5 The above generates the list `[0, 1, 2, 3, 4]`. This is useful for looping with `range $i, $e := until 5`. -
untilStepLike `until`, `untilStep` generates a list of counting integers. But it allows +### untilStep + +Like `until`, `untilStep` generates a list of counting integers. But it allows you to define a start, stop, and step: ``` @@ -596,12 +510,10 @@ untilStep 3 6 2 The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal or greater than 6. This is similar to Python's `range` function. -
seqWorks like the bash `seq` command. +### seq + +Works like the bash `seq` command. * 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. * 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. * 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. @@ -614,22 +526,16 @@ seq 2 -2 => 2 1 0 -1 -2 seq 0 2 10 => 0 2 4 6 8 10 seq 0 -2 -5 => 0 -2 -4 ``` -
## Date Functions - - - - - +### now - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
nowThe current date/time. Use this in conjunction with other date functions. -
agoThe `ago` function returns duration from time.Now in seconds resolution. +The current date/time. Use this in conjunction with other date functions. + +### ago + +The `ago` function returns duration from time.Now in seconds resolution. ``` ago .CreatedAt @@ -640,12 +546,10 @@ returns in `time.Duration` String() format ``` 2h34m7s ``` -
dateThe `date` function formats a date. +### date + +The `date` function formats a date. Format the date to YEAR-MONTH-DAY: @@ -663,34 +567,28 @@ Mon Jan 2 15:04:05 MST 2006 Write it in the format you want. Above, `2006-01-02` is the same date, but in the format we want. -
dateInZoneSame as `date`, but with a timezone. +### dateInZone + +Same as `date`, but with a timezone. ``` dateInZone "2006-01-02" (now) "UTC" ``` -
durationFormats a given amount of seconds as a `time.Duration`. +### duration + +Formats a given amount of seconds as a `time.Duration`. This returns 1m35s ``` duration "95" ``` -
durationRoundRounds a given duration to the most significant unit. Strings and `time.Duration` +### durationRound + +Rounds a given duration to the most significant unit. Strings and `time.Duration` gets parsed as a duration, while a `time.Time` is calculated as the duration since. This return 2h @@ -704,22 +602,18 @@ This returns 3mo ``` durationRound "2400h10m5s" ``` -
unixEpochReturns the seconds since the unix epoch for a `time.Time`. +### unixEpoch + +Returns the seconds since the unix epoch for a `time.Time`. ``` now | unixEpoch ``` -
dateModify, mustDateModifyThe `dateModify` takes a modification and a date and returns the timestamp. +### dateModify, mustDateModify + +The `dateModify` takes a modification and a date and returns the timestamp. Subtract an hour and thirty minutes from the current time: @@ -728,33 +622,27 @@ now | date_modify "-1.5h" ``` If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. -
htmlDateThe `htmlDate` function formats a date for inserting into an HTML date picker +### htmlDate + +The `htmlDate` function formats a date for inserting into an HTML date picker input field. ``` now | htmlDate ``` -
htmlDateInZoneSame as htmlDate, but with a timezone. +### htmlDateInZone + +Same as htmlDate, but with a timezone. ``` htmlDateInZone (now) "UTC" ``` -
toDate, mustToDate`toDate` converts a string to a date. The first argument is the date layout and +### toDate, mustToDate + +`toDate` converts a string to a date. The first argument is the date layout and the second the date string. If the string can't be convert it returns the zero value. `mustToDate` will return an error in case the string cannot be converted. @@ -765,18 +653,14 @@ This is useful when you want to convert a string date to another format ``` toDate "2006-01-02" "2017-12-31" | date "02/01/2006" ``` -
## Default Functions Sprig provides tools for setting default values for templates. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
defaultTo set a simple default value, use `default`: +### default + +To set a simple default value, use `default`: ``` default "foo" .Bar @@ -796,12 +680,10 @@ The definition of "empty" depends on type: For structs, there is no definition of empty, so a struct will never return the default. -
emptyThe `empty` function returns `true` if the given value is considered empty, and +### empty + +The `empty` function returns `true` if the given value is considered empty, and `false` otherwise. The empty values are listed in the `default` section. ``` @@ -810,12 +692,10 @@ empty .Foo Note that in Go template conditionals, emptiness is calculated for you. Thus, you rarely need `if empty .Foo`. Instead, just use `if .Foo`. -
coalesceThe `coalesce` function takes a list of values and returns the first non-empty +### coalesce + +The `coalesce` function takes a list of values and returns the first non-empty one. ``` @@ -833,12 +713,10 @@ coalesce .name .parent.name "Matt" The above will first check to see if `.name` is empty. If it is not, it will return that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. -
allThe `all` function takes a list of values and returns true if all values are non-empty. +### all + +The `all` function takes a list of values and returns true if all values are non-empty. ``` all 0 1 2 @@ -853,12 +731,10 @@ all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Me ``` The above will check http.Request is POST with tls 1.3 and http/2. -
anyThe `any` function takes a list of values and returns true if any value is non-empty. +### any + +The `any` function takes a list of values and returns true if any value is non-empty. ``` any 0 1 2 @@ -873,23 +749,19 @@ any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method " ``` The above will check http.Request method is one of GET/POST/OPTIONS. -
fromJSON, mustFromJSON`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. +### fromJSON, mustFromJSON + +`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. `mustFromJSON` will return an error in case the JSON is invalid. ``` fromJSON "{\"foo\": 55}" ``` -
toJSON, mustToJSONThe `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. +### toJSON, mustToJSON + +The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. `mustToJSON` will return an error in case the item cannot be encoded in JSON. ``` @@ -897,40 +769,34 @@ toJSON .Item ``` The above returns JSON string representation of `.Item`. -
toPrettyJSON, mustToPrettyJSONThe `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. +### toPrettyJSON, mustToPrettyJSON + +The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. ``` toPrettyJSON .Item ``` The above returns indented JSON string representation of `.Item`. -
toRawJSON, mustToRawJSONThe `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. +### toRawJSON, mustToRawJSON + +The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. ``` toRawJSON .Item ``` The above returns unescaped JSON string representation of `.Item`. -
ternaryThe `ternary` function takes two values, and a test value. If the test value is +### ternary + +The `ternary` function takes two values, and a test value. If the test value is true, the first value will be returned. If the test value is empty, the second value will be returned. This is similar to the c ternary operator. -### true test value +#### true test value ``` ternary "foo" "bar" true @@ -944,7 +810,7 @@ true | ternary "foo" "bar" The above returns `"foo"`. -### false test value +#### false test value ``` ternary "foo" "bar" false @@ -957,27 +823,13 @@ false | ternary "foo" "bar" ``` The above returns `"bar"`. -
## Encoding Functions Sprig has the following encoding and decoding functions: - - - - - - - - - - -
b64enc/b64decEncode or decode with Base64 -
b32enc/b32decEncode or decode with Base32 -
+- `b64enc`/`b64dec`: Encode or decode with Base64 +- `b32enc`/`b32dec`: Encode or decode with Base32 ## Lists and List Functions @@ -993,54 +845,45 @@ $myList := list 1 2 3 4 5 The above creates a list of `[1 2 3 4 5]`. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
first, mustFirstTo get the head item on a list, use `first`. +### first, mustFirst + +To get the head item on a list, use `first`. `first $myList` returns `1` `first` panics if there is a problem while `mustFirst` returns an error to the template engine if there is a problem. -
rest, mustRestTo get the tail of the list (everything but the first item), use `rest`. +### rest, mustRest + +To get the tail of the list (everything but the first item), use `rest`. `rest $myList` returns `[2 3 4 5]` `rest` panics if there is a problem while `mustRest` returns an error to the template engine if there is a problem. -
last, mustLastTo get the last item on a list, use `last`: +### last, mustLast + +To get the last item on a list, use `last`: `last $myList` returns `5`. This is roughly analogous to reversing a list and then calling `first`. `last` panics if there is a problem while `mustLast` returns an error to the template engine if there is a problem. -
initial, mustInitialThis compliments `last` by returning all _but_ the last element. +### initial, mustInitial + +This compliments `last` by returning all _but_ the last element. `initial $myList` returns `[1 2 3 4]`. `initial` panics if there is a problem while `mustInitial` returns an error to the template engine if there is a problem. -
append, mustAppendAppend a new item to an existing list, creating a new list. +### append, mustAppend + +Append a new item to an existing list, creating a new list. ``` $new = append $myList 6 @@ -1050,12 +893,10 @@ The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. `append` panics if there is a problem while `mustAppend` returns an error to the template engine if there is a problem. -
prepend, mustPrependPush an element onto the front of a list, creating a new list. +### prepend, mustPrepend + +Push an element onto the front of a list, creating a new list. ``` prepend $myList 0 @@ -1065,24 +906,20 @@ The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. `prepend` panics if there is a problem while `mustPrepend` returns an error to the template engine if there is a problem. -
concatConcatenate arbitrary number of lists into one. +### concat + +Concatenate arbitrary number of lists into one. ``` concat $myList ( list 6 7 ) ( list 8 ) ``` The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. -
reverse, mustReverseProduce a new list with the reversed elements of the given list. +### reverse, mustReverse + +Produce a new list with the reversed elements of the given list. ``` reverse $myList @@ -1092,12 +929,10 @@ The above would generate the list `[5 4 3 2 1]`. `reverse` panics if there is a problem while `mustReverse` returns an error to the template engine if there is a problem. -
uniq, mustUniqGenerate a list with all of the duplicates removed. +### uniq, mustUniq + +Generate a list with all of the duplicates removed. ``` list 1 1 1 2 | uniq @@ -1107,12 +942,10 @@ The above would produce `[1 2]` `uniq` panics if there is a problem while `mustUniq` returns an error to the template engine if there is a problem. -
without, mustWithoutThe `without` function filters items out of a list. +### without, mustWithout + +The `without` function filters items out of a list. ``` without $myList 3 @@ -1130,12 +963,10 @@ That would produce `[2 4]` `without` panics if there is a problem while `mustWithout` returns an error to the template engine if there is a problem. -
has, mustHasTest to see if a list has a particular element. +### has, mustHas + +Test to see if a list has a particular element. ``` has 4 $myList @@ -1145,12 +976,10 @@ The above would return `true`, while `has "hello" $myList` would return false. `has` panics if there is a problem while `mustHas` returns an error to the template engine if there is a problem. -
compact, mustCompactAccepts a list and removes entries with empty values. +### compact, mustCompact + +Accepts a list and removes entries with empty values. ``` $list := list 1 "a" "foo" "" @@ -1161,12 +990,10 @@ $copy := compact $list `compact` panics if there is a problem and `mustCompact` returns an error to the template engine if there is a problem. -
slice, mustSliceTo get partial elements of a list, use `slice list [n] [m]`. It is +### slice, mustSlice + +To get partial elements of a list, use `slice list [n] [m]`. It is equivalent of `list[n:m]`. - `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. @@ -1176,26 +1003,21 @@ equivalent of `list[n:m]`. `slice` panics if there is a problem while `mustSlice` returns an error to the template engine if there is a problem. -
chunkTo split a list into chunks of given size, use `chunk size list`. This is useful for pagination. +### chunk + +To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. ``` chunk 3 (list 1 2 3 4 5 6 7 8) ``` This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. -
### A Note on List Internals -A list is implemented in Go as a `[]interface{}`. For Go developers embedding -Sprig, you may pass `[]interface{}` items into your template context and be +A list is implemented in Go as a `[]any`. For Go developers embedding +Sprig, you may pass `[]any` items into your template context and be able to use all of the `list` functions on those items. ## Dictionaries and Dict Functions @@ -1209,10 +1031,9 @@ type, even another `dict` or `list`. Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will modify the contents of a dictionary. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
dictCreating dictionaries is done by calling the `dict` function and passing it a +### dict + +Creating dictionaries is done by calling the `dict` function and passing it a list of pairs. The following creates a dictionary with three items: @@ -1220,12 +1041,10 @@ The following creates a dictionary with three items: ``` $myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" ``` -
getGiven a map and a key, get the value from the map. +### get + +Given a map and a key, get the value from the map. ``` get $myDict "name1" @@ -1235,12 +1054,10 @@ The above returns `"value1"` Note that if the key is not found, this operation will simply return `""`. No error will be generated. -
setUse `set` to add a new key/value pair to a dictionary. +### set + +Use `set` to add a new key/value pair to a dictionary. ``` $_ := set $myDict "name4" "value4" @@ -1248,12 +1065,10 @@ $_ := set $myDict "name4" "value4" Note that `set` _returns the dictionary_ (a requirement of Go template functions), so you may need to trap the value as done above with the `$_` assignment. -
unsetGiven a map and a key, delete the key from the map. +### unset + +Given a map and a key, delete the key from the map. ``` $_ := unset $myDict "name4" @@ -1263,24 +1078,20 @@ As with `set`, this returns the dictionary. Note that if the key is not found, this operation will simply return. No error will be generated. -
hasKeyThe `hasKey` function returns `true` if the given dict contains the given key. +### hasKey + +The `hasKey` function returns `true` if the given dict contains the given key. ``` hasKey $myDict "name1" ``` If the key is not found, this returns `false`. -
pluckThe `pluck` function makes it possible to give one key and multiple maps, and +### pluck + +The `pluck` function makes it possible to give one key and multiple maps, and get a list of all of the matches: ``` @@ -1298,12 +1109,10 @@ inserted. A common idiom in Sprig templates is to uses `pluck... | first` to get the first matching key out of a collection of dictionaries. -
digThe `dig` function traverses a nested set of dicts, selecting keys from a list +### dig + +The `dig` function traverses a nested set of dicts, selecting keys from a list of values. It returns a default value if any of the keys are not found at the associated dict. @@ -1331,12 +1140,10 @@ especially since Go's template package's `and` doesn't shortcut. For instance `a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) `dig` accepts its dict argument last in order to support pipelining. -
keysThe `keys` function will return a `list` of all of the keys in one or more `dict` +### keys + +The `keys` function will return a `list` of all of the keys in one or more `dict` types. Since a dictionary is _unordered_, the keys will not be in a predictable order. They can be sorted with `sortAlpha`. @@ -1350,12 +1157,10 @@ function along with `sortAlpha` to get a unqiue, sorted list of keys. ``` keys $myDict $myOtherDict | uniq | sortAlpha ``` -
pickThe `pick` function selects just the given keys out of a dictionary, creating a +### pick + +The `pick` function selects just the given keys out of a dictionary, creating a new `dict`. ``` @@ -1363,12 +1168,10 @@ $new := pick $myDict "name1" "name2" ``` The above returns `{name1: value1, name2: value2}` -
omitThe `omit` function is similar to `pick`, except it returns a new `dict` with all +### omit + +The `omit` function is similar to `pick`, except it returns a new `dict` with all the keys that _do not_ match the given keys. ``` @@ -1376,12 +1179,10 @@ $new := omit $myDict "name1" "name3" ``` The above returns `{name2: value2}` -
valuesThe `values` function is similar to `keys`, except it returns a new `list` with +### values + +The `values` function is similar to `keys`, except it returns a new `list` with all the values of the source `dict` (only one dictionary is supported). ``` @@ -1391,62 +1192,44 @@ $vals := values $myDict The above returns `list["value1", "value2", "value 3"]`. Note that the `values` function gives no guarantees about the result ordering- if you care about this, then use `sortAlpha`. -
## Type Conversion Functions The following type conversion functions are provided by Sprig: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
atoiConvert a string to an integer. -
float64Convert to a `float64`. -
intConvert to an `int` at the system's width. -
int64Convert to an `int64`. -
toDecimalConvert a unix octal to a `int64`. -
toStringConvert to a string. -
toStringsConvert a list, slice, or array to a list of strings. -
+- `atoi`: Convert a string to an integer. +- `float64`: Convert to a `float64`. +- `int`: Convert to an `int` at the system's width. +- `int64`: Convert to an `int64`. +- `toDecimal`: Convert a unix octal to a `int64`. +- `toString`: Convert to a string. +- `toStrings`: Convert a list, slice, or array to a list of strings. Only `atoi` requires that the input be a specific type. The others will attempt to convert from any type to the destination type. For example, `int64` can convert floats to ints, and it can also convert strings to ints. +### toStrings + +Given a list-like collection, produce a slice of strings. + +``` +list 1 2 3 | toStrings +``` + +The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns +them as a list. + +### toDecimal + +Given a unix octal permission, produce a decimal. + +``` +"0777" | toDecimal +``` + +The above converts `0777` to `511` and returns the value as an int64. + ## Path and Filepath Functions While Sprig does not grant access to the filesystem, it does provide functions @@ -1466,56 +1249,44 @@ Examples: [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): `https://example.com/some/content/`, `ftp://example.com/file/`. - - - - - - - - - - - - - - - - - - - - - -
baseReturn the last element of a path. +#### base + +Return the last element of a path. ``` base "foo/bar/baz" ``` The above prints "baz". -
dirReturn the directory, stripping the last part of the path. So `dir "foo/bar/baz"` +#### dir + +Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` returns `foo/bar`. -
cleanClean up a path. +#### clean + +Clean up a path. ``` clean "foo/bar/../baz" ``` The above resolves the `..` and returns `foo/baz`. -
extReturn the file extension. +#### ext + +Return the file extension. ``` ext "foo.bar" ``` The above returns `.bar`. -
isAbsTo check whether a path is absolute, use `isAbs`. -
+#### isAbs + +To check whether a path is absolute, use `isAbs`. ### Filepaths @@ -1531,10 +1302,9 @@ Examples: the filesystem path is separated by the backslash character (`\`): `C:\Users\Username\`, `C:\Program Files\Application\`; - - - - - - - - - - - - - - - - - - - - - -
osBaseReturn the last element of a filepath. +#### osBase + +Return the last element of a filepath. ``` osBase "/foo/bar/baz" @@ -1542,20 +1312,16 @@ osBase "C:\\foo\\bar\\baz" ``` The above prints "baz" on Linux and Windows, respectively. -
osDirReturn the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` +#### osDir + +Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` returns `C:\\foo\\bar` on Windows. -
osCleanClean up a path. +#### osClean + +Clean up a path. ``` osClean "/foo/bar/../baz" @@ -1563,12 +1329,10 @@ osClean "C:\\foo\\bar\\..\\baz" ``` The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. -
osExtReturn the file extension. +#### osExt + +Return the file extension. ``` osExt "/foo.bar" @@ -1576,48 +1340,32 @@ osExt "C:\\foo.bar" ``` The above returns `.bar` on Linux and Windows, respectively. -
osIsAbsTo check whether a file path is absolute, use `osIsAbs`. -
+#### osIsAbs + +To check whether a file path is absolute, use `osIsAbs`. ## Flow Control Functions - - - - - -
failUnconditionally returns an empty `string` and an `error` with the specified +### fail + +Unconditionally returns an empty `string` and an `error` with the specified text. This is useful in scenarios where other conditionals have determined that template rendering should fail. ``` fail "Please accept the end user license agreement" ``` -
## UUID Functions Sprig can generate UUID v4 universally unique IDs. - - - - - -
uuidv4 ``` uuidv4 ``` The above returns a new UUID of the v4 (randomly generated) type. -
## Reflection Functions @@ -1632,63 +1380,35 @@ Sprig provides a set of functions for each. ### Kind Functions - - - - - - - - - - -
kindOfReturns the kind of an object. +There are two Kind functions: `kindOf` returns the kind of an object. ``` kindOf "hello" ``` -The above would return `string`. -
kindIsFor simple tests (like in `if` blocks), the `kindIs` function will let you verify that a value is a particular kind: +The above would return `string`. For simple tests (like in `if` blocks), the +`kindIs` function will let you verify that a value is a particular kind: ``` kindIs "int" 123 ``` The above will return `true` -
### Type Functions Types are slightly harder to work with, so there are three different functions: - - - - - - - - - - - - - - - -
typeOfReturns the underlying type of a value: `typeOf $foo` -
typeIsLike `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` -
typeIsLikeWorks as `typeIs`, except that it also dereferences pointers. -
+- `typeOf` returns the underlying type of a value: `typeOf $foo` +- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` +- `typeIsLike` works as `typeIs`, except that it also dereferences pointers. **Note:** None of these can test whether or not something implements a given interface, since doing so would require compiling the interface in ahead of time. - - - - - -
deepEqualReturns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) +### deepEqual + +`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) Works for non-primitive types as well (compared to the built-in `eq`). @@ -1697,28 +1417,22 @@ deepEqual (list 1 2 3) (list 1 2 3) ``` The above will return `true` -
## Cryptographic and Security Functions Sprig provides a couple of advanced cryptographic functions. - - - - - - - - - - - - - - - - - -
sha1sumThe `sha1sum` function receives a string, and computes it's SHA1 digest. +### sha1sum + +The `sha1sum` function receives a string, and computes it's SHA1 digest. ``` sha1sum "Hello world!" ``` -
sha256sumThe `sha256sum` function receives a string, and computes it's SHA256 digest. +### sha256sum + +The `sha256sum` function receives a string, and computes it's SHA256 digest. ``` sha256sum "Hello world!" @@ -1726,12 +1440,10 @@ sha256sum "Hello world!" The above will compute the SHA 256 sum in an "ASCII armored" format that is safe to print. -
sha512sumThe `sha512sum` function receives a string, and computes it's SHA512 digest. +### sha512sum + +The `sha512sum` function receives a string, and computes it's SHA512 digest. ``` sha512sum "Hello world!" @@ -1739,26 +1451,19 @@ sha512sum "Hello world!" The above will compute the SHA 512 sum in an "ASCII armored" format that is safe to print. -
adler32sumThe `adler32sum` function receives a string, and computes its Adler-32 checksum. +### adler32sum + +The `adler32sum` function receives a string, and computes its Adler-32 checksum. ``` adler32sum "Hello world!" ``` -
## URL Functions - - - - - - - - - -
urlParseParses string for URL and produces dict with URL parts +### urlParse +Parses string for URL and produces dict with URL parts ``` urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" @@ -1776,12 +1481,9 @@ userinfo: 'admin:secret' ``` For more info, check https://golang.org/pkg/net/url/#URL -
urlJoinJoins map (produced by `urlParse`) to produce URL string +### urlJoin +Joins map (produced by `urlParse`) to produce URL string ``` urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") @@ -1791,6 +1493,3 @@ The above returns the following string: ``` proto://host:80/path?query#fragment ``` -
diff --git a/docs/sprig.md b/docs/sprig.md deleted file mode 100644 index be4e6c9c..00000000 --- a/docs/sprig.md +++ /dev/null @@ -1,24 +0,0 @@ -# Template Functions - -ntfy includes a (reduced) version of [Sprig](https://github.com/Masterminds/sprig) to add functions that can be used -when you are using the [message template](publish.md#message-templating) feature. - -Below are the functions that are available to use inside your message/title templates. - -* [String Functions](./sprig/strings.md): `trim`, `trunc`, `substr`, `plural`, etc. - * [String List Functions](./sprig/string_slice.md): `splitList`, `sortAlpha`, etc. -* [Integer Math Functions](./sprig/math.md): `add`, `max`, `mul`, etc. - * [Integer List Functions](./sprig/integer_slice.md): `until`, `untilStep` -* [Date Functions](./sprig/date.md): `now`, `date`, etc. -* [Defaults Functions](./sprig/defaults.md): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` -* [Encoding Functions](./sprig/encoding.md): `b64enc`, `b64dec`, etc. -* [Lists and List Functions](./sprig/lists.md): `list`, `first`, `uniq`, etc. -* [Dictionaries and Dict Functions](./sprig/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, etc. -* [Type Conversion Functions](./sprig/conversion.md): `atoi`, `int64`, `toString`, etc. -* [Path and Filepath Functions](./sprig/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` -* [Flow Control Functions](./sprig/flow_control.md): `fail` -* Advanced Functions - * [UUID Functions](./sprig/uuid.md): `uuidv4` - * [Reflection](./sprig/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. - * [Cryptographic and Security Functions](./sprig/crypto.md): `sha256sum`, etc. - * [URL](./sprig/url.md): `urlParse`, `urlJoin` diff --git a/docs/sprig/conversion.md b/docs/sprig/conversion.md deleted file mode 100644 index af952682..00000000 --- a/docs/sprig/conversion.md +++ /dev/null @@ -1,36 +0,0 @@ -# Type Conversion Functions - -The following type conversion functions are provided by Sprig: - -- `atoi`: Convert a string to an integer. -- `float64`: Convert to a `float64`. -- `int`: Convert to an `int` at the system's width. -- `int64`: Convert to an `int64`. -- `toDecimal`: Convert a unix octal to a `int64`. -- `toString`: Convert to a string. -- `toStrings`: Convert a list, slice, or array to a list of strings. - -Only `atoi` requires that the input be a specific type. The others will attempt -to convert from any type to the destination type. For example, `int64` can convert -floats to ints, and it can also convert strings to ints. - -## toStrings - -Given a list-like collection, produce a slice of strings. - -``` -list 1 2 3 | toStrings -``` - -The above converts `1` to `"1"`, `2` to `"2"`, and so on, and then returns -them as a list. - -## toDecimal - -Given a unix octal permission, produce a decimal. - -``` -"0777" | toDecimal -``` - -The above converts `0777` to `511` and returns the value as an int64. diff --git a/docs/sprig/crypto.md b/docs/sprig/crypto.md deleted file mode 100644 index c66a269d..00000000 --- a/docs/sprig/crypto.md +++ /dev/null @@ -1,41 +0,0 @@ -# Cryptographic and Security Functions - -Sprig provides a couple of advanced cryptographic functions. - -## sha1sum - -The `sha1sum` function receives a string, and computes it's SHA1 digest. - -``` -sha1sum "Hello world!" -``` - -## sha256sum - -The `sha256sum` function receives a string, and computes it's SHA256 digest. - -``` -sha256sum "Hello world!" -``` - -The above will compute the SHA 256 sum in an "ASCII armored" format that is -safe to print. - -## sha512sum - -The `sha512sum` function receives a string, and computes it's SHA512 digest. - -``` -sha512sum "Hello world!" -``` - -The above will compute the SHA 512 sum in an "ASCII armored" format that is -safe to print. - -## adler32sum - -The `adler32sum` function receives a string, and computes its Adler-32 checksum. - -``` -adler32sum "Hello world!" -``` diff --git a/docs/sprig/date.md b/docs/sprig/date.md deleted file mode 100644 index 7410c08d..00000000 --- a/docs/sprig/date.md +++ /dev/null @@ -1,126 +0,0 @@ -# Date Functions - -## now - -The current date/time. Use this in conjunction with other date functions. - -## ago - -The `ago` function returns duration from time.Now in seconds resolution. - -``` -ago .CreatedAt -``` - -returns in `time.Duration` String() format - -``` -2h34m7s -``` - -## date - -The `date` function formats a date. - -Format the date to YEAR-MONTH-DAY: - -``` -now | date "2006-01-02" -``` - -Date formatting in Go is a [little bit different](https://pauladamsmith.com/blog/2011/05/go_time.html). - -In short, take this as the base date: - -``` -Mon Jan 2 15:04:05 MST 2006 -``` - -Write it in the format you want. Above, `2006-01-02` is the same date, but -in the format we want. - -## dateInZone - -Same as `date`, but with a timezone. - -``` -dateInZone "2006-01-02" (now) "UTC" -``` - -## duration - -Formats a given amount of seconds as a `time.Duration`. - -This returns 1m35s - -``` -duration "95" -``` - -## durationRound - -Rounds a given duration to the most significant unit. Strings and `time.Duration` -gets parsed as a duration, while a `time.Time` is calculated as the duration since. - -This return 2h - -``` -durationRound "2h10m5s" -``` - -This returns 3mo - -``` -durationRound "2400h10m5s" -``` - -## unixEpoch - -Returns the seconds since the unix epoch for a `time.Time`. - -``` -now | unixEpoch -``` - -## dateModify, mustDateModify - -The `dateModify` takes a modification and a date and returns the timestamp. - -Subtract an hour and thirty minutes from the current time: - -``` -now | date_modify "-1.5h" -``` - -If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. - -## htmlDate - -The `htmlDate` function formats a date for inserting into an HTML date picker -input field. - -``` -now | htmlDate -``` - -## htmlDateInZone - -Same as htmlDate, but with a timezone. - -``` -htmlDateInZone (now) "UTC" -``` - -## toDate, mustToDate - -`toDate` converts a string to a date. The first argument is the date layout and -the second the date string. If the string can't be convert it returns the zero -value. -`mustToDate` will return an error in case the string cannot be converted. - -This is useful when you want to convert a string date to another format -(using pipe). The example below converts "2017-12-31" to "31/12/2017". - -``` -toDate "2006-01-02" "2017-12-31" | date "02/01/2006" -``` diff --git a/docs/sprig/defaults.md b/docs/sprig/defaults.md deleted file mode 100644 index b8af1455..00000000 --- a/docs/sprig/defaults.md +++ /dev/null @@ -1,169 +0,0 @@ -# Default Functions - -Sprig provides tools for setting default values for templates. - -## default - -To set a simple default value, use `default`: - -``` -default "foo" .Bar -``` - -In the above, if `.Bar` evaluates to a non-empty value, it will be used. But if -it is empty, `foo` will be returned instead. - -The definition of "empty" depends on type: - -- Numeric: 0 -- String: "" -- Lists: `[]` -- Dicts: `{}` -- Boolean: `false` -- And always `nil` (aka null) - -For structs, there is no definition of empty, so a struct will never return the -default. - -## empty - -The `empty` function returns `true` if the given value is considered empty, and -`false` otherwise. The empty values are listed in the `default` section. - -``` -empty .Foo -``` - -Note that in Go template conditionals, emptiness is calculated for you. Thus, -you rarely need `if empty .Foo`. Instead, just use `if .Foo`. - -## coalesce - -The `coalesce` function takes a list of values and returns the first non-empty -one. - -``` -coalesce 0 1 2 -``` - -The above returns `1`. - -This function is useful for scanning through multiple variables or values: - -``` -coalesce .name .parent.name "Matt" -``` - -The above will first check to see if `.name` is empty. If it is not, it will return -that value. If it _is_ empty, `coalesce` will evaluate `.parent.name` for emptiness. -Finally, if both `.name` and `.parent.name` are empty, it will return `Matt`. - -## all - -The `all` function takes a list of values and returns true if all values are non-empty. - -``` -all 0 1 2 -``` - -The above returns `false`. - -This function is useful for evaluating multiple conditions of variables or values: - -``` -all (eq .Request.TLS.Version 0x0304) (.Request.ProtoAtLeast 2 0) (eq .Request.Method "POST") -``` - -The above will check http.Request is POST with tls 1.3 and http/2. - -## any - -The `any` function takes a list of values and returns true if any value is non-empty. - -``` -any 0 1 2 -``` - -The above returns `true`. - -This function is useful for evaluating multiple conditions of variables or values: - -``` -any (eq .Request.Method "GET") (eq .Request.Method "POST") (eq .Request.Method "OPTIONS") -``` - -The above will check http.Request method is one of GET/POST/OPTIONS. - -## fromJSON, mustFromJSON - -`fromJSON` decodes a JSON document into a structure. If the input cannot be decoded as JSON the function will return an empty string. -`mustFromJSON` will return an error in case the JSON is invalid. - -``` -fromJSON "{\"foo\": 55}" -``` - -## toJSON, mustToJSON - -The `toJSON` function encodes an item into a JSON string. If the item cannot be converted to JSON the function will return an empty string. -`mustToJSON` will return an error in case the item cannot be encoded in JSON. - -``` -toJSON .Item -``` - -The above returns JSON string representation of `.Item`. - -## toPrettyJSON, mustToPrettyJSON - -The `toPrettyJSON` function encodes an item into a pretty (indented) JSON string. - -``` -toPrettyJSON .Item -``` - -The above returns indented JSON string representation of `.Item`. - -## toRawJSON, mustToRawJSON - -The `toRawJSON` function encodes an item into JSON string with HTML characters unescaped. - -``` -toRawJSON .Item -``` - -The above returns unescaped JSON string representation of `.Item`. - -## ternary - -The `ternary` function takes two values, and a test value. If the test value is -true, the first value will be returned. If the test value is empty, the second -value will be returned. This is similar to the c ternary operator. - -### true test value - -``` -ternary "foo" "bar" true -``` - -or - -``` -true | ternary "foo" "bar" -``` - -The above returns `"foo"`. - -### false test value - -``` -ternary "foo" "bar" false -``` - -or - -``` -false | ternary "foo" "bar" -``` - -The above returns `"bar"`. diff --git a/docs/sprig/dicts.md b/docs/sprig/dicts.md deleted file mode 100644 index 5a4490d5..00000000 --- a/docs/sprig/dicts.md +++ /dev/null @@ -1,172 +0,0 @@ -# Dictionaries and Dict Functions - -Sprig provides a key/value storage type called a `dict` (short for "dictionary", -as in Python). A `dict` is an _unorder_ type. - -The key to a dictionary **must be a string**. However, the value can be any -type, even another `dict` or `list`. - -Unlike `list`s, `dict`s are not immutable. The `set` and `unset` functions will -modify the contents of a dictionary. - -## dict - -Creating dictionaries is done by calling the `dict` function and passing it a -list of pairs. - -The following creates a dictionary with three items: - -``` -$myDict := dict "name1" "value1" "name2" "value2" "name3" "value 3" -``` - -## get - -Given a map and a key, get the value from the map. - -``` -get $myDict "name1" -``` - -The above returns `"value1"` - -Note that if the key is not found, this operation will simply return `""`. No error -will be generated. - -## set - -Use `set` to add a new key/value pair to a dictionary. - -``` -$_ := set $myDict "name4" "value4" -``` - -Note that `set` _returns the dictionary_ (a requirement of Go template functions), -so you may need to trap the value as done above with the `$_` assignment. - -## unset - -Given a map and a key, delete the key from the map. - -``` -$_ := unset $myDict "name4" -``` - -As with `set`, this returns the dictionary. - -Note that if the key is not found, this operation will simply return. No error -will be generated. - -## hasKey - -The `hasKey` function returns `true` if the given dict contains the given key. - -``` -hasKey $myDict "name1" -``` - -If the key is not found, this returns `false`. - -## pluck - -The `pluck` function makes it possible to give one key and multiple maps, and -get a list of all of the matches: - -``` -pluck "name1" $myDict $myOtherDict -``` - -The above will return a `list` containing every found value (`[value1 otherValue1]`). - -If the give key is _not found_ in a map, that map will not have an item in the -list (and the length of the returned list will be less than the number of dicts -in the call to `pluck`. - -If the key is _found_ but the value is an empty value, that value will be -inserted. - -A common idiom in Sprig templates is to uses `pluck... | first` to get the first -matching key out of a collection of dictionaries. - -## dig - -The `dig` function traverses a nested set of dicts, selecting keys from a list -of values. It returns a default value if any of the keys are not found at the -associated dict. - -``` -dig "user" "role" "humanName" "guest" $dict -``` - -Given a dict structured like -``` -{ - user: { - role: { - humanName: "curator" - } - } -} -``` - -the above would return `"curator"`. If the dict lacked even a `user` field, -the result would be `"guest"`. - -Dig can be very useful in cases where you'd like to avoid guard clauses, -especially since Go's template package's `and` doesn't shortcut. For instance -`and a.maybeNil a.maybeNil.iNeedThis` will always evaluate -`a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) - -`dig` accepts its dict argument last in order to support pipelining. - -## keys - -The `keys` function will return a `list` of all of the keys in one or more `dict` -types. Since a dictionary is _unordered_, the keys will not be in a predictable order. -They can be sorted with `sortAlpha`. - -``` -keys $myDict | sortAlpha -``` - -When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` -function along with `sortAlpha` to get a unqiue, sorted list of keys. - -``` -keys $myDict $myOtherDict | uniq | sortAlpha -``` - -## pick - -The `pick` function selects just the given keys out of a dictionary, creating a -new `dict`. - -``` -$new := pick $myDict "name1" "name2" -``` - -The above returns `{name1: value1, name2: value2}` - -## omit - -The `omit` function is similar to `pick`, except it returns a new `dict` with all -the keys that _do not_ match the given keys. - -``` -$new := omit $myDict "name1" "name3" -``` - -The above returns `{name2: value2}` - -## values - -The `values` function is similar to `keys`, except it returns a new `list` with -all the values of the source `dict` (only one dictionary is supported). - -``` -$vals := values $myDict -``` - -The above returns `list["value1", "value2", "value 3"]`. Note that the `values` -function gives no guarantees about the result ordering- if you care about this, -then use `sortAlpha`. diff --git a/docs/sprig/encoding.md b/docs/sprig/encoding.md deleted file mode 100644 index 1c7a36f8..00000000 --- a/docs/sprig/encoding.md +++ /dev/null @@ -1,6 +0,0 @@ -# Encoding Functions - -Sprig has the following encoding and decoding functions: - -- `b64enc`/`b64dec`: Encode or decode with Base64 -- `b32enc`/`b32dec`: Encode or decode with Base32 diff --git a/docs/sprig/flow_control.md b/docs/sprig/flow_control.md deleted file mode 100644 index 6414640a..00000000 --- a/docs/sprig/flow_control.md +++ /dev/null @@ -1,11 +0,0 @@ -# Flow Control Functions - -## fail - -Unconditionally returns an empty `string` and an `error` with the specified -text. This is useful in scenarios where other conditionals have determined that -template rendering should fail. - -``` -fail "Please accept the end user license agreement" -``` diff --git a/docs/sprig/integer_slice.md b/docs/sprig/integer_slice.md deleted file mode 100644 index ab4bef6d..00000000 --- a/docs/sprig/integer_slice.md +++ /dev/null @@ -1,41 +0,0 @@ -# Integer List Functions - -## until - -The `until` function builds a range of integers. - -``` -until 5 -``` - -The above generates the list `[0, 1, 2, 3, 4]`. - -This is useful for looping with `range $i, $e := until 5`. - -## untilStep - -Like `until`, `untilStep` generates a list of counting integers. But it allows -you to define a start, stop, and step: - -``` -untilStep 3 6 2 -``` - -The above will produce `[3 5]` by starting with 3, and adding 2 until it is equal -or greater than 6. This is similar to Python's `range` function. - -## seq - -Works like the bash `seq` command. -* 1 parameter (end) - will generate all counting integers between 1 and `end` inclusive. -* 2 parameters (start, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by 1. -* 3 parameters (start, step, end) - will generate all counting integers between `start` and `end` inclusive incrementing or decrementing by `step`. - -``` -seq 5 => 1 2 3 4 5 -seq -3 => 1 0 -1 -2 -3 -seq 0 2 => 0 1 2 -seq 2 -2 => 2 1 0 -1 -2 -seq 0 2 10 => 0 2 4 6 8 10 -seq 0 -2 -5 => 0 -2 -4 -``` diff --git a/docs/sprig/lists.md b/docs/sprig/lists.md deleted file mode 100644 index ed8c52b3..00000000 --- a/docs/sprig/lists.md +++ /dev/null @@ -1,188 +0,0 @@ -# Lists and List Functions - -Sprig provides a simple `list` type that can contain arbitrary sequential lists -of data. This is similar to arrays or slices, but lists are designed to be used -as immutable data types. - -Create a list of integers: - -``` -$myList := list 1 2 3 4 5 -``` - -The above creates a list of `[1 2 3 4 5]`. - -## first, mustFirst - -To get the head item on a list, use `first`. - -`first $myList` returns `1` - -`first` panics if there is a problem while `mustFirst` returns an error to the -template engine if there is a problem. - -## rest, mustRest - -To get the tail of the list (everything but the first item), use `rest`. - -`rest $myList` returns `[2 3 4 5]` - -`rest` panics if there is a problem while `mustRest` returns an error to the -template engine if there is a problem. - -## last, mustLast - -To get the last item on a list, use `last`: - -`last $myList` returns `5`. This is roughly analogous to reversing a list and -then calling `first`. - -`last` panics if there is a problem while `mustLast` returns an error to the -template engine if there is a problem. - -## initial, mustInitial - -This compliments `last` by returning all _but_ the last element. -`initial $myList` returns `[1 2 3 4]`. - -`initial` panics if there is a problem while `mustInitial` returns an error to the -template engine if there is a problem. - -## append, mustAppend - -Append a new item to an existing list, creating a new list. - -``` -$new = append $myList 6 -``` - -The above would set `$new` to `[1 2 3 4 5 6]`. `$myList` would remain unaltered. - -`append` panics if there is a problem while `mustAppend` returns an error to the -template engine if there is a problem. - -## prepend, mustPrepend - -Push an element onto the front of a list, creating a new list. - -``` -prepend $myList 0 -``` - -The above would produce `[0 1 2 3 4 5]`. `$myList` would remain unaltered. - -`prepend` panics if there is a problem while `mustPrepend` returns an error to the -template engine if there is a problem. - -## concat - -Concatenate arbitrary number of lists into one. - -``` -concat $myList ( list 6 7 ) ( list 8 ) -``` - -The above would produce `[1 2 3 4 5 6 7 8]`. `$myList` would remain unaltered. - -## reverse, mustReverse - -Produce a new list with the reversed elements of the given list. - -``` -reverse $myList -``` - -The above would generate the list `[5 4 3 2 1]`. - -`reverse` panics if there is a problem while `mustReverse` returns an error to the -template engine if there is a problem. - -## uniq, mustUniq - -Generate a list with all of the duplicates removed. - -``` -list 1 1 1 2 | uniq -``` - -The above would produce `[1 2]` - -`uniq` panics if there is a problem while `mustUniq` returns an error to the -template engine if there is a problem. - -## without, mustWithout - -The `without` function filters items out of a list. - -``` -without $myList 3 -``` - -The above would produce `[1 2 4 5]` - -Without can take more than one filter: - -``` -without $myList 1 3 5 -``` - -That would produce `[2 4]` - -`without` panics if there is a problem while `mustWithout` returns an error to the -template engine if there is a problem. - -## has, mustHas - -Test to see if a list has a particular element. - -``` -has 4 $myList -``` - -The above would return `true`, while `has "hello" $myList` would return false. - -`has` panics if there is a problem while `mustHas` returns an error to the -template engine if there is a problem. - -## compact, mustCompact - -Accepts a list and removes entries with empty values. - -``` -$list := list 1 "a" "foo" "" -$copy := compact $list -``` - -`compact` will return a new list with the empty (i.e., "") item removed. - -`compact` panics if there is a problem and `mustCompact` returns an error to the -template engine if there is a problem. - -## slice, mustSlice - -To get partial elements of a list, use `slice list [n] [m]`. It is -equivalent of `list[n:m]`. - -- `slice $myList` returns `[1 2 3 4 5]`. It is same as `myList[:]`. -- `slice $myList 3` returns `[4 5]`. It is same as `myList[3:]`. -- `slice $myList 1 3` returns `[2 3]`. It is same as `myList[1:3]`. -- `slice $myList 0 3` returns `[1 2 3]`. It is same as `myList[:3]`. - -`slice` panics if there is a problem while `mustSlice` returns an error to the -template engine if there is a problem. - -## chunk - -To split a list into chunks of given size, use `chunk size list`. This is useful for pagination. - -``` -chunk 3 (list 1 2 3 4 5 6 7 8) -``` - -This produces list of lists `[ [ 1 2 3 ] [ 4 5 6 ] [ 7 8 ] ]`. - -## A Note on List Internals - -A list is implemented in Go as a `[]interface{}`. For Go developers embedding -Sprig, you may pass `[]interface{}` items into your template context and be -able to use all of the `list` functions on those items. diff --git a/docs/sprig/math.md b/docs/sprig/math.md deleted file mode 100644 index b08d0a2f..00000000 --- a/docs/sprig/math.md +++ /dev/null @@ -1,78 +0,0 @@ -# Integer Math Functions - -The following math functions operate on `int64` values. - -## add - -Sum numbers with `add`. Accepts two or more inputs. - -``` -add 1 2 3 -``` - -## add1 - -To increment by 1, use `add1` - -## sub - -To subtract, use `sub` - -## div - -Perform integer division with `div` - -## mod - -Modulo with `mod` - -## mul - -Multiply with `mul`. Accepts two or more inputs. - -``` -mul 1 2 3 -``` - -## max - -Return the largest of a series of integers: - -This will return `3`: - -``` -max 1 2 3 -``` - -## min - -Return the smallest of a series of integers. - -`min 1 2 3` will return `1` - -## floor - -Returns the greatest float value less than or equal to input value - -`floor 123.9999` will return `123.0` - -## ceil - -Returns the greatest float value greater than or equal to input value - -`ceil 123.001` will return `124.0` - -## round - -Returns a float value with the remainder rounded to the given number to digits after the decimal point. - -`round 123.555555 3` will return `123.556` - -## randInt -Returns a random integer value from min (inclusive) to max (exclusive). - -``` -randInt 12 30 -``` - -The above will produce a random number in the range [12,30]. diff --git a/docs/sprig/paths.md b/docs/sprig/paths.md deleted file mode 100644 index f847e357..00000000 --- a/docs/sprig/paths.md +++ /dev/null @@ -1,114 +0,0 @@ -# Path and Filepath Functions - -While Sprig does not grant access to the filesystem, it does provide functions -for working with strings that follow file path conventions. - -## Paths - -Paths separated by the slash character (`/`), processed by the `path` package. - -Examples: - -* The [Linux](https://en.wikipedia.org/wiki/Linux) and - [MacOS](https://en.wikipedia.org/wiki/MacOS) - [filesystems](https://en.wikipedia.org/wiki/File_system): - `/home/user/file`, `/etc/config`; -* The path component of - [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier): - `https://example.com/some/content/`, `ftp://example.com/file/`. - -### base - -Return the last element of a path. - -``` -base "foo/bar/baz" -``` - -The above prints "baz". - -### dir - -Return the directory, stripping the last part of the path. So `dir "foo/bar/baz"` -returns `foo/bar`. - -### clean - -Clean up a path. - -``` -clean "foo/bar/../baz" -``` - -The above resolves the `..` and returns `foo/baz`. - -### ext - -Return the file extension. - -``` -ext "foo.bar" -``` - -The above returns `.bar`. - -### isAbs - -To check whether a path is absolute, use `isAbs`. - -## Filepaths - -Paths separated by the `os.PathSeparator` variable, processed by the `path/filepath` package. - -These are the recommended functions to use when parsing paths of local filesystems, usually when dealing with local files, directories, etc. - -Examples: - -* Running on Linux or MacOS the filesystem path is separated by the slash character (`/`): - `/home/user/file`, `/etc/config`; -* Running on [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) - the filesystem path is separated by the backslash character (`\`): - `C:\Users\Username\`, `C:\Program Files\Application\`; - -### osBase - -Return the last element of a filepath. - -``` -osBase "/foo/bar/baz" -osBase "C:\\foo\\bar\\baz" -``` - -The above prints "baz" on Linux and Windows, respectively. - -### osDir - -Return the directory, stripping the last part of the path. So `osDir "/foo/bar/baz"` -returns `/foo/bar` on Linux, and `osDir "C:\\foo\\bar\\baz"` -returns `C:\\foo\\bar` on Windows. - -### osClean - -Clean up a path. - -``` -osClean "/foo/bar/../baz" -osClean "C:\\foo\\bar\\..\\baz" -``` - -The above resolves the `..` and returns `foo/baz` on Linux and `C:\\foo\\baz` on Windows. - -### osExt - -Return the file extension. - -``` -osExt "/foo.bar" -osExt "C:\\foo.bar" -``` - -The above returns `.bar` on Linux and Windows, respectively. - -### osIsAbs - -To check whether a file path is absolute, use `osIsAbs`. diff --git a/docs/sprig/reflection.md b/docs/sprig/reflection.md deleted file mode 100644 index 51e167aa..00000000 --- a/docs/sprig/reflection.md +++ /dev/null @@ -1,50 +0,0 @@ -# Reflection Functions - -Sprig provides rudimentary reflection tools. These help advanced template -developers understand the underlying Go type information for a particular value. - -Go has several primitive _kinds_, like `string`, `slice`, `int64`, and `bool`. - -Go has an open _type_ system that allows developers to create their own types. - -Sprig provides a set of functions for each. - -## Kind Functions - -There are two Kind functions: `kindOf` returns the kind of an object. - -``` -kindOf "hello" -``` - -The above would return `string`. For simple tests (like in `if` blocks), the -`kindIs` function will let you verify that a value is a particular kind: - -``` -kindIs "int" 123 -``` - -The above will return `true` - -## Type Functions - -Types are slightly harder to work with, so there are three different functions: - -- `typeOf` returns the underlying type of a value: `typeOf $foo` -- `typeIs` is like `kindIs`, but for types: `typeIs "*io.Buffer" $myVal` -- `typeIsLike` works as `typeIs`, except that it also dereferences pointers. - -**Note:** None of these can test whether or not something implements a given -interface, since doing so would require compiling the interface in ahead of time. - -## deepEqual - -`deepEqual` returns true if two values are ["deeply equal"](https://golang.org/pkg/reflect/#DeepEqual) - -Works for non-primitive types as well (compared to the built-in `eq`). - -``` -deepEqual (list 1 2 3) (list 1 2 3) -``` - -The above will return `true` diff --git a/docs/sprig/string_slice.md b/docs/sprig/string_slice.md deleted file mode 100644 index 96c0c83b..00000000 --- a/docs/sprig/string_slice.md +++ /dev/null @@ -1,72 +0,0 @@ -# String List Functions - -These function operate on or generate slices of strings. In Go, a slice is a -growable array. In Sprig, it's a special case of a `list`. - -## join - -Join a list of strings into a single string, with the given separator. - -``` -list "hello" "world" | join "_" -``` - -The above will produce `hello_world` - -`join` will try to convert non-strings to a string value: - -``` -list 1 2 3 | join "+" -``` - -The above will produce `1+2+3` - -## splitList and split - -Split a string into a list of strings: - -``` -splitList "$" "foo$bar$baz" -``` - -The above will return `[foo bar baz]` - -The older `split` function splits a string into a `dict`. It is designed to make -it easy to use template dot notation for accessing members: - -``` -$a := split "$" "foo$bar$baz" -``` - -The above produces a map with index keys. `{_0: foo, _1: bar, _2: baz}` - -``` -$a._0 -``` - -The above produces `foo` - -## splitn - -`splitn` function splits a string into a `dict` with `n` keys. It is designed to make -it easy to use template dot notation for accessing members: - -``` -$a := splitn "$" 2 "foo$bar$baz" -``` - -The above produces a map with index keys. `{_0: foo, _1: bar$baz}` - -``` -$a._0 -``` - -The above produces `foo` - -## sortAlpha - -The `sortAlpha` function sorts a list of strings into alphabetical (lexicographical) -order. - -It does _not_ sort in place, but returns a sorted copy of the list, in keeping -with the immutability of lists. diff --git a/docs/sprig/strings.md b/docs/sprig/strings.md deleted file mode 100644 index 784392f1..00000000 --- a/docs/sprig/strings.md +++ /dev/null @@ -1,309 +0,0 @@ -# String Functions - -Sprig has a number of string manipulation functions. - -## trim - -The `trim` function removes space from either side of a string: - -``` -trim " hello " -``` - -The above produces `hello` - -## trimAll - -Remove given characters from the front or back of a string: - -``` -trimAll "$" "$5.00" -``` - -The above returns `5.00` (as a string). - -## trimSuffix - -Trim just the suffix from a string: - -``` -trimSuffix "-" "hello-" -``` - -The above returns `hello` - -## trimPrefix - -Trim just the prefix from a string: - -``` -trimPrefix "-" "-hello" -``` - -The above returns `hello` - -## upper - -Convert the entire string to uppercase: - -``` -upper "hello" -``` - -The above returns `HELLO` - -## lower - -Convert the entire string to lowercase: - -``` -lower "HELLO" -``` - -The above returns `hello` - -## title - -Convert to title case: - -``` -title "hello world" -``` - -The above returns `Hello World` - -## repeat - -Repeat a string multiple times: - -``` -repeat 3 "hello" -``` - -The above returns `hellohellohello` - -## substr - -Get a substring from a string. It takes three parameters: - -- start (int) -- end (int) -- string (string) - -``` -substr 0 5 "hello world" -``` - -The above returns `hello` - -## trunc - -Truncate a string (and add no suffix) - -``` -trunc 5 "hello world" -``` - -The above produces `hello`. - -``` -trunc -5 "hello world" -``` - -The above produces `world`. - -## contains - -Test to see if one string is contained inside of another: - -``` -contains "cat" "catch" -``` - -The above returns `true` because `catch` contains `cat`. - -## hasPrefix and hasSuffix - -The `hasPrefix` and `hasSuffix` functions test whether a string has a given -prefix or suffix: - -``` -hasPrefix "cat" "catch" -``` - -The above returns `true` because `catch` has the prefix `cat`. - -## quote and squote - -These functions wrap a string in double quotes (`quote`) or single quotes -(`squote`). - -## cat - -The `cat` function concatenates multiple strings together into one, separating -them with spaces: - -``` -cat "hello" "beautiful" "world" -``` - -The above produces `hello beautiful world` - -## indent - -The `indent` function indents every line in a given string to the specified -indent width. This is useful when aligning multi-line strings: - -``` -indent 4 $lots_of_text -``` - -The above will indent every line of text by 4 space characters. - -## nindent - -The `nindent` function is the same as the indent function, but prepends a new -line to the beginning of the string. - -``` -nindent 4 $lots_of_text -``` - -The above will indent every line of text by 4 space characters and add a new -line to the beginning. - -## replace - -Perform simple string replacement. - -It takes three arguments: - -- string to replace -- string to replace with -- source string - -``` -"I Am Henry VIII" | replace " " "-" -``` - -The above will produce `I-Am-Henry-VIII` - -## plural - -Pluralize a string. - -``` -len $fish | plural "one anchovy" "many anchovies" -``` - -In the above, if the length of the string is 1, the first argument will be -printed (`one anchovy`). Otherwise, the second argument will be printed -(`many anchovies`). - -The arguments are: - -- singular string -- plural string -- length integer - -NOTE: Sprig does not currently support languages with more complex pluralization -rules. And `0` is considered a plural because the English language treats it -as such (`zero anchovies`). The Sprig developers are working on a solution for -better internationalization. - -## regexMatch, mustRegexMatch - -Returns true if the input string contains any match of the regular expression. - -``` -regexMatch "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" "test@acme.com" -``` - -The above produces `true` - -`regexMatch` panics if there is a problem and `mustRegexMatch` returns an error to the -template engine if there is a problem. - -## regexFindAll, mustRegexFindAll - -Returns a slice of all matches of the regular expression in the input string. -The last parameter n determines the number of substrings to return, where -1 means return all matches - -``` -regexFindAll "[2,4,6,8]" "123456789" -1 -``` - -The above produces `[2 4 6 8]` - -`regexFindAll` panics if there is a problem and `mustRegexFindAll` returns an error to the -template engine if there is a problem. - -## regexFind, mustRegexFind - -Return the first (left most) match of the regular expression in the input string - -``` -regexFind "[a-zA-Z][1-9]" "abcd1234" -``` - -The above produces `d1` - -`regexFind` panics if there is a problem and `mustRegexFind` returns an error to the -template engine if there is a problem. - -## regexReplaceAll, mustRegexReplaceAll - -Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement. -Inside string replacement, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch - -``` -regexReplaceAll "a(x*)b" "-ab-axxb-" "${1}W" -``` - -The above produces `-W-xxW-` - -`regexReplaceAll` panics if there is a problem and `mustRegexReplaceAll` returns an error to the -template engine if there is a problem. - -## regexReplaceAllLiteral, mustRegexReplaceAllLiteral - -Returns a copy of the input string, replacing matches of the Regexp with the replacement string replacement -The replacement string is substituted directly, without using Expand - -``` -regexReplaceAllLiteral "a(x*)b" "-ab-axxb-" "${1}" -``` - -The above produces `-${1}-${1}-` - -`regexReplaceAllLiteral` panics if there is a problem and `mustRegexReplaceAllLiteral` returns an error to the -template engine if there is a problem. - -## regexSplit, mustRegexSplit - -Slices the input string into substrings separated by the expression and returns a slice of the substrings between those expression matches. The last parameter `n` determines the number of substrings to return, where `-1` means return all matches - -``` -regexSplit "z+" "pizza" -1 -``` - -The above produces `[pi a]` - -`regexSplit` panics if there is a problem and `mustRegexSplit` returns an error to the -template engine if there is a problem. - -## regexQuoteMeta - -Returns a string that escapes all regular expression metacharacters inside the argument text; -the returned string is a regular expression matching the literal text. - -``` -regexQuoteMeta "1.2.3" -``` - -The above produces `1\.2\.3` - -## See Also... - -The [Conversion Functions](conversion.md) contain functions for converting strings. The [String List Functions](string_slice.md) contains -functions for working with an array of strings. diff --git a/docs/sprig/url.md b/docs/sprig/url.md deleted file mode 100644 index 21d54a29..00000000 --- a/docs/sprig/url.md +++ /dev/null @@ -1,33 +0,0 @@ -# URL Functions - -## urlParse -Parses string for URL and produces dict with URL parts - -``` -urlParse "http://admin:secret@server.com:8080/api?list=false#anchor" -``` - -The above returns a dict, containing URL object: -```yaml -scheme: 'http' -host: 'server.com:8080' -path: '/api' -query: 'list=false' -opaque: nil -fragment: 'anchor' -userinfo: 'admin:secret' -``` - -For more info, check https://golang.org/pkg/net/url/#URL - -## urlJoin -Joins map (produced by `urlParse`) to produce URL string - -``` -urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "http") -``` - -The above returns the following string: -``` -proto://host:80/path?query#fragment -``` diff --git a/docs/sprig/uuid.md b/docs/sprig/uuid.md deleted file mode 100644 index 1b57a330..00000000 --- a/docs/sprig/uuid.md +++ /dev/null @@ -1,9 +0,0 @@ -# UUID Functions - -Sprig can generate UUID v4 universally unique IDs. - -``` -uuidv4 -``` - -The above returns a new UUID of the v4 (randomly generated) type. diff --git a/docs/static/img/android-screenshot-template-custom.png b/docs/static/img/android-screenshot-template-custom.png new file mode 100644 index 0000000000000000000000000000000000000000..8325e9a4a580ee877dc620fcec04ac9bbb47dfc5 GIT binary patch literal 45032 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 literal 0 HcmV?d00001 diff --git a/docs/static/img/android-screenshot-template-predefined.png b/docs/static/img/android-screenshot-template-predefined.png new file mode 100644 index 0000000000000000000000000000000000000000..ba77c31d3ec0f59a790e094192174140b7b34b66 GIT binary patch literal 86828 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 Qhd@cz)78&qol`;+09SQgDF6Tf literal 0 HcmV?d00001 diff --git a/docs/static/img/screenshot-github-webhook-config.png b/docs/static/img/screenshot-github-webhook-config.png new file mode 100644 index 0000000000000000000000000000000000000000..46b784b3dee587b776b67a3be8be45ff2d8c6d89 GIT binary patch literal 98734 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

I{*Wzx$vp2<6+N za4E9Jcp{H}93NIyA8cZyh&SC1U?il<6kp__>qxiKirOM(t92MejTx>eQS?$LTPI2i zq$yfG)(wj`yP9$;!AKqM10+G?7Le+E4V*a6gG!3m85ifUnB6AeV8{GIzJFh^#nBiy zk&qtiX~^5bDL`m8w?2ugO8yj^xFrvnfO_aIm!_Z|RZ01M-ETxiOp>Y>dyrq^Sg1?iRq)y@IGN|)?g^(HdEs~(ae#?FV13b*(=diyMU>9JB2+f2S*#|{Pb(CGO7 ziJP#lA>-XZFlj(_V~cLeB*lcx+fKA5)p;lwL_Uo84q$&0gWF87`Ix~a>;T)5PJO3% z`4q#WtTaz7Bhum*BiozBm!f0+=?AV;a%rD0y~z2@ZZ7q|t^!b61R!IJx{hy!%(S=e ztYc8~lcExaxT*4tR=rxvb3j{g5_;b2=fxLM;7Y9ZFjG$`Iof4P_2J@qY6mS&bSg6w zY$bw{{^BL5CiEtyqjK=F?WeznN1PhgG^}Y3WyJ8{hW+OkMrsFDJD-0_a@>v_@ zmWmiVYk@gsZ)djI6MY-2SyEKN((kB#cXv_rs};%9yR6|G{F?3V}KoO#Mzdi!~>u&UhI0t@6szE`YH_qvu zHU3V_IEW8v;_g({E*a?|DdeOQxJbm8vW>WT=R})$5Q&y0)it=TEM$O9bxv z5_5N{J3_3=r0)ZE8(`HopPlYN^5{{7cZ0FXymbn4x^(E zGLiHPfzyjUUXo_rI~sR!6JBu!Zu{RT-x#xI-nC9}r`tJMAk{c@12N-yWF!6U_etct zC?vpYaSn%zaXT7j`w4sO6gA*gZ72U&`$+WN;={e1HwEdM#xoBf)Y&u%kHdOPG5P*N zlB|B~o0lZl#8gOuP|2*HZ)MK=&sIN4Gg?*yV_?m6ob4YKCb}^|uyZ6a8PIE1KpSs5 zx@EULiI@YtmmXP1YPZen#mvJyRXvHb*qY)wFpcVxVXqXms)|XbB&NnzWt%*#m}7FP z8E5l-_~8FRN>u23GE|&xnfMFqIMFmBU!KIF(cy?l%eM>~$EDtof3c5O)bAFOR2Z1s zR!lQX8MxCf=36E$$5Z4y5BL6}5Au`UvUW>fp}vRT&9T1==>6a;^@j|tu~tUHr=O5W zR<|-jDp+(4C^*}uAd#cbb=^B0Ad3b5Mp1IH_TuKg9`wMue?XZ?M z0l(gn-``K1b;akU)|0GPKzQe4!$Ns=(jk%_IzPG$SYMp1X9LlHTqu{rcLJGm)xU=}@3v{?cdaSb53|p^c9eYtCH2#)i z|G81%hgwTvfRQ-qIw5%@e-YDD%st7tl0wuZ@bE(9XR3X!H^9k4QZ4#_CNMw+Jc0ZV zf|f!xr;nP9bb`sK_QwhL!Q|8DOKMiH$C8*>qht3TL>fFk0Ux(&`|;lcXz8U`45YPL zb!%mFozn-X)vHW{M>tDLYyz+I!L+zS=m_|6o0j>=o8eYARpi4Yn|+JhR1tGqvSncxnV)`Bf0%icg)kUc}P6N>QL;wT88tp>hFI?4u|C?7WA|5iL{7S-$6-=d&=UqhdKxT6;1VX5|Ruydci9m zp#Fl3(^(*KZ(?6&ggTxX(YpY@_qp$ZFA(kfkN@Cp{AMMbKy(xB;RabhZ;>|CSU0Wq zS%oj_LxYst91gAE)ywgVSx0lJHXFeu-^pRv$2e}tx@uFk!s>xiy-dB9fzU2^^pEck zthIsRi>DoeFP^4!)Y5Z#TREa_ob0f#!2P-A&aXeS&4QHl-rg~TodGVep7Kd6g=e=s zN`b=r(i-%O4HV;?5yi_V*4j>*xb^hFzF`)-USYGX5kRE&vYzzWgiP` zKGzsd7JhG|kFH8j2(UTKst%gPYGfNTQFft-MW0T)p^VasGsZ_;G_U^5n(`T)q%{?? z^W&Y91MH!wTg4ukzCPI-c%Ri;3)#ZlJDQUCb%(<^XttuQ_Hq8pUbh}zI~O1Bb|nr! zmJ2njx`FEp^9oD9QcG4M6Hk3opK;H(>a*!QEO|PhSDnDi0cqw#fbc@M;RZor&ost@ zCmbbwo?6keI1i_`lE3zTNh-r)J2|0UsFxQdVU-h2h62#$loZTNR9u?Ezcb862=YLHyErEDACdQ*Ka7a~mzKGeoJ>jKilJ<`$!=t!M znu%dgulXyg6H&5nR3zb|f0<8|XbN;s$aBCa72r=p%T916ur@c5sung$Nqd1!0|cjg zHPb@R+*vaFm=X1C&!7O5Vg~SadP%~Ml3#Og{fKhAHl9YOUKPXwe)_Ww)>zMo7od>o zn*u^?RJQMZETcA3qt9v+F6X? zUp~0=E@kUsA=uNx2UCx0iX@f11A6~pwnzC#Ar5^-P20= zyT`%vE#TQ}Y&|O)nKI1gIdX9TjBSi4!ySG>-UuZo2cnmS#me^_gBrL73k^;Jy!(iz z^#?p4fwe96jL1SFCJkcW0ZgM}tA`MMgk!xaP6?Oidfy)a0#J4#ULnpS>CG_7%blGN zYYQc)Uf&`QSL&J!y)?u~GwX&UWbu_R?Ho21LMXOMUG65|2rg^iyWOF5F$K{%iXs=Z z_v?^}_<^JHtwHC@P}`ZmKAKN^@YyC;hzk7;CEpN~JjEg1DYA{an&xl+{PJQHH|FZ} zR;ib8d3nWmqL`%bYMihTCzp_-62*gG`$uT?UJo?>M)!(#dGvwjjST>Mn$ODe+%jcE zT)OTb!1l8NRj2YbA!0o3j766xaw#GayZ*t#fVM}R?qcCB++7OqkxMinZkXnQj}*Bm zLpTUd1Q6aIQMz!=Vqg8L?uR;gjamKunFCiRWktng)fB0&|NO&VH09%7IMwT&IBwns`!L{MNv+jdotgbJFjnO!S)P5XbR5 z^8R2fzpf)M0%AvSKi-u{{A&8u!Pblx%!#BMB| zl0W!U1=k!P7tRH}xbiM&lrUwX$eFxGFO+ek-EdKaJuO94fh;I4Ni}VJ%mNTV6e8b! zsCCb>Ls9{?+uR*yC!?`75RHhXSloHG(c8YSz)&m?7#j=|lK+%OJVLxeIQ(Y#T)C1M z&58cgUB-Gw5cy~Rz{59!T9-0uVZdVL+FicKgL4cV2k?!iT#5enc)0k%xAwEy6`usQ zfTg5bp431TF)ukYkfwv6vERg*=pPsQdM`Z48I1;Ebp98~C@V;=5cNu%?MnL73Jh`i zKoGA6rFlkZDi2FK&}TeO=Gk=dUM=j|3|m8|h&>rKu84Ch$Zgk2(AnD- z;A>yhEm^(tEW}dRuUoFHm9{4K2rzpfLoI_~mvc$FlO1mS>X+^G^KO)hot&oVg<#jP zbq`a^z=anxQ9a(d{#7%%!OymOcHu*&J3{e<#>8-eyFWp6yAEr+?so@pYteDGegVt7 zXlYcj;Kg8TA1#`*L`BeJhtg{;IFDwmj#!PR!|ibN2H>?)40kf< zK-FHPy6-t&;HzVP^_IpQL9Cwk-R-hg>%`X=?eh9pQGI98^+>-`s%BjJ$6zwtC8(?| z{(s-$PI$2Mjt|2cqUD2p)52kvHsVQ zENDDDG3pjOMg%5E|JN_El4+?YMgPl{ipKs=AOfa>|Id@K{})Z^|0bRM|J#$i3W)&d z&MI)D?*$J6W0VfB*kX=9-DC5!vC|APA0)<}xV%32sW+W7{y*%!Ra9Kt(l#6Vl7I%Ac^vR+jFvc z=q;I`=ldHGf{3)hZagy@`6U1DD?nJZldD>}Geyv{0gm78vM&#>=;Vi#loAePOaSKG z`+UVHMbBC_+$P&4EdQxxHQ%@cLH|CWUg?!9M71GEukjIYzY1X50Q5xd>+!8LaS9ad z>IIL1`&F}u`YA-<Mj|PWnl%`H`HH60XA}QSQ-XrihYFCDbSOdya$i znl%kxv+~%OVPaM>=E==B@@%6(A;XF&MCW$Bswik#bpmG&V6x(3a~krf<{^c`M(rt4 z(ogBh35onZpD)ztRX9frtIAY#m6de&(NEDUc5E+r8R^6_Jx=$O=I{j_6C%E$m9`0qT6C|fBeJaKTClZtYYwW z9efnKsQwlI5+R27M;hFruFo0RjQ;gpX2u|y7*m|H?jotZu++@m_#&<`wYHE_da#J z5HLiP3v7wDf8dD)JR;iZa!H+iGx}0X963`$JX8IEHlgnaPiG+72Mr=JSL*dJ^b6&x z)(JmUhFg3MR_1R)&~bYvdJTvgPVga^Mw6M@AB&ZM|6>T?s4yhuv{@9%Hoqub&I4&R z`p|USW7-1pbJF9dn9uf1ngGtx*GSf}hL}?kXD2IK>g3s+gq4!MF(s2aWJQX2){ne# zJB_Kw&7x>%OL!ysbeRf{}zQ?BF_$psa|nB113*$nDS^Gt0WgKlI61ayQo zg>n~1#XZ44)`pWR$DbqGmN$qZ>ay8L@o}K>=BkIYHi$BW*SW=u+^3^b^hqb*{)EnS z3`|*U{sBu_C4tLfr8RQ@XIe)()y;!gsA?Y`1YI0|Zo3%fYBGuB+z?-&DpIbzGqxmS z`HN$bt7@`U%j1Cg*VxC8rA1=Z47;wH|Ezb7z)-v*guIHvK=bDt*~tCzozTGWAW>;o z%nExk?wZ>FGtTF3uR!(dA4zgfI4RIgb*DM1~3J zeM|-A(PB%4X^&CUvljz$3ll_hS*2oJ8vB87e|o$GskEwV-r@^<=!qr@I=^TQ?V%*A zz*#`Wy#Lq%E2`m#niBhmcK5S6YPequIFWLU+3;;mP%il*|2!)6U7o(WM?VNziX<`DnPA(a z%3)ih63YM44-V1RDha=}Wl~~Zd4yw@FB{E^Z_=rEOtizqmS_u|6EbF|UuZ~UIqT}y zG-CB0-UnCv-7-0{pw4N@?{d#(K*dD3m@PMYTc;`g=$E2)LFAjtC>EOP;Ng1=r1P^_ z(VLD~vZ-2RzL)~R+Do8>Dux6av6j9dB+KFOxoWj;t}s< zh*-zFg)-tu9g+OY%HMeD2efZ@+fsvD;!=<5igkE-9d4-WOSPh>LGRN@@ngKu(T_H} z1&VC0e)@joox|(P8;;!Wmb6>9toh9G?2LhB9ej4kK;x@+fAb;tHMFz6SWTV*0W`tQ z+NSNLuN%;Vg+B#KF&bRu{o}IMiljl+nG>@31%7BzZKc7z*b}urjTAkmy|E=V=fP<4)!V0x9X~{f#Xj)IQq(>HtQMym&F#KadiP*_Ff`=m7TwlkahXR3?>eE7^wK`gW zj~k;wLML3Iz8j<6O4Xr{R_Dwk@cc4E_OI($3GE2Hx#pbL z7f5~!VO@TZ9|?0&CAq=Cx1Em5$}{*+uP3`|S9%S%2C)W^dp51v+wWOT5N!~?nzF2d zb#^X+#*ME;Og+rCxatEIKhw4~+mUfq0sQxAqcIE;{wsN)6e0=9a zc0MSGm6B$JUBo&ZPUSZy<7wC>7-SJZ zh|iq#x#w&CJ0Eyh@EFc~T#R!YES9>|Dv5Ena2O~p>z%Zguam#R2n_}yrMI}mEB0mO zhFWcV+#*9lr7Og2u;j5WwO8j4Jo(NjKU_h}m-9Y=)((c~Px3EqcB@_smfWZhKLqP0 zDF({ifA^Wp$Cd8oB=PcnX>tq^`VH%Uv@&sZqulgth2OBra+`?wAL^$8E5yyP2&mck zqdbn@@ZS9u-=pESSl4*0C#cVEEcoMeykbU)H*^;vFHQvfaRiCJ7%`M8-gEW$&c*!i z@rpC165BXt>&GpxP;lQPjhgLnj3#=bta4i!067_e0W-RVh7uG1+E_E;+F5tTb6zT| zrwXK_OQ+OLfZuqF$@SgbDJ^)LepR?pk4Joe`-ZXgLOhs)v#&rBqkZ5UYQ5 zPY^}a{M3JGtR$oC=yQE~-jo7X-By=ImhY{iKMvTO{P!ku|O&UgJ(`qta;+A&hLb`zB z4jcLwb7}SqVI@bJ#~rxsM?$O^6c_w8g3&V z5{|g(%e3EYg}>J7Jwc4n$Xcg&>JTYCDo7lMlPenBWLse$uXPc^PRE)`(#up-x%CV; z__3-Vk;QDi{38#_&OhwkXE84>Q9doQb1pK~fL)~IELWChleafC~Rn`ogNVBX|#QS^=M?cub@~Dir zVqql7%37LTZw;qaHg@+Iv2e9CEq_#$9b7Hkn2eU7o&_#@$Rw+hq%RV3X5UvE@FR;D zqbT!};raJ5Dbt+pYIroGBeymA3+N=p&M^qK1h85^!X>{CXvqb-4?a#V@7|ViXF}q~ zDk5WEw--9kRb6OJZ}vF92u9{FHXvy7GxU{ueS5et#FLU~}1TupY~(^WxY8V5lf6UXc2r(RBr10ANTcZ?6I zyZ+?aXV}1UEn&NNI<2;WJMqd3mw={f?JF%IiL*aZ)U z126fTcH~=PKv#;ss*!euo2zX_V&z%`m<7c6*fT9VZdPP+l#D{RR0p(1FkfU>6X;cx z4{`?=o6-WS?73N>!i>eN6_y4|KXWJ}wg z5;v3vdQ-ao%|^I7=bt9x;>@2<=Qvkfp1E8lkr;<`h&CG*+&3QYEOt}MSGDAv(Yc?I z9@iy{-sgK=SSeczjLgcYi8M|*^ajkf*;jJS!UsZDL z&pdUGa(kD58#P(jJt+6nB58%c8FJBT@)f+#RuE5UY7sX^Y+;PU+-Co9f~pVPVzPor zy3@&oe^of{w6HaKO3iuyuX+YIQBEUruYq&xGS?ygVsd5Gu9M+N`2aNv%l5JPH%XZ-3HdXjh`b}1tE0q@8rC^i& z9} z4ns6GJF+CEtk(Z=hNWSvCQfmEB;%M!AVw33#>J<$)FgX;1TR^fPtF|5Hc0V? z%A@~#h3Ld)y_M9>bakM5{uK#}n&$reZ__EA(l$__pY>-!bg@rnc@H1AzovCB<>E-FPV zp|h3^uX-7c4C)!c|9$=TqF1dkYt}L5(p6S9X_~}Qke{jpx~ZctKz0+2f>W~Oj0_ZH zZZ21_KmP4!QFy0FJEh*r^Vx}zVq2*MMHPhY`^dHd} zGOE|Ii}Ok}Pm+aMCXs@GhI8S(R@j=LKCcYlO-$X`l>dBHn_TdKiI<7cBXN$@W@won z2~_lpRi-MwDe`0=_2M7xPT@@=4YPdR#J9e`&GBC1Jd^ZaNTg)q>V4jiDm|23vHzrc z1mHcJ1lx_-$ZF((OW-uj$g}$_`|rfD5hHKUGyl13xPjIj$8zTw$LN>z^GuR|jYTDi zS+gnE!bI|SSPGJWBSk(*pNylHJXZLxDSl@8M-*pC5&jES|Nlt=fnavGN!rRqKSSxY z5z^&R+f{+`&8Rq);FEtI@QueHFJAHUbGkwrcK{R@ac6`{6xjWm*M{w!L?=RI`&K+L z6h*mFgVlQ34!EVZMA7;PDsc_UM=SNH!2-)6qcTLFXMzyuekPmB^{ZB``MrNKf2h8t z4e9UH!o~-VE`k$uAIo}#4atHK$`3tEonEZi$XPb{d#4x`A%Hr&W6$a-*V8y4Nh~+%=L%M_bgy=N% zKlooPBP?1k_0_^5Uh#Ad#k)ek{&}Didh^DMEzgmZ6RI7YeoW!DG~kB4(-WyK5M`QI|uncImSu0fS;n(>@}#cp?a+h-eP zJ8OJUVb5cGo>~V6!JV5!e+Mv&j(hi6UT(4KEeK^#CHd}oaNGbsk1E;Zj9nQ2wGX`A z*@|3PHzK7nC5h{H93vlkhw}y}rq>G3;t)o?_#W)zM0)SkgOx{wk#og!?)-&l`|b^e zKHq&?O7kRJ_1holGz7zc$kGuaxr2t8En4^7puIGxEG-@r8;p)5a5tftCb>w?gx>@ZsKM%ff-z22^ zL77F}v;d--2%r7d54f0h7XV|qF2pn!>i#DiY?>_8l_K$gPtwCCCIey{En#G7^FpDB zLNh?#iX3QhkZ)XV)b!-UtolRfaXcvV%1>o{drQN*a0FjC3(kr(5yz(FR^*!@S;wT% z#f=4vy1I0!j8M>@=Bi)hzxq;wnD)gJCRP~MI`m`#f1uVL)(Cudndy9{LCLkE|SpbP>1#s?%-i|3Iv2VR{+lo{$BV&+jxbFxW!xo>K(yjL~XFWzHxJP6hVnZ|3F%vYM#X_m2 z?3sKo`JQa?UQd_z9r>H_Bgb`?+Kk2U@g?=8%{D2ny0)_3M)1|HVZQdj^CgmX_|j(2 z$SF!`LLv8nR?+a{D+Al@AMXMuKPMkY$|leoB?VUw1iIf(*^Cd_?l}&(ZYDHUo)E!m z_aG<4tPy_@iOJ}*Bo%_AX&ov^y?qgGy-$&SMcw5&ODf8cb5%B2iz*lE5$nWJ*fr#b z?TcC8zkyP#J3AG$%r1#rTNSq9JpK^{BCI8&`HhJ4x&%1W4L@XT)<213b9_eBlNk$M zE&j8~0$VSdT0D3j(*D?DVX=UMg1ufTM9_Q|dLcPIBBT8~>fOdVABOQ;-x0bKRC}vO zp<&$p<;{b@AA$=&|73LkH8InW@}@u_Z=t{Fx_QhlE?T7bh5Jxk&(Lla1soO5GC<{O z_vd9Tm41}p=-78-wO>59o;}TvKGaxl86N!}rlw8?=`{C*mlt_n?V}^PZtWrPv<6!St>^h9r)I-0r%(q{E;?sFps&SqOMfTgTQwP zgYbPpVon}NI#r*YJUmiWH#V8b&(CW*!G7$SlhmuW@mw6eVE?J7?C#Wr>~{7}!JD%# z7vO%q`^`ulukIxo9}>0#;awxSzPbqgdCqp6wkh0zZkv#K|3yN2GhqixTBM++LH=hS zJDUVgiwEva_A8dR{(>tQ;6-%TjJZbL#R}&9GSL7yDTtcrOeLyw+%vqEj1 ztV&E%+(Vu90el-$t||Qss@u^V;D#$#LR^wu=$rt7@=8bmT))enyt=~PkNe@E-(ja) z>e!&OnAf4+*3%;`lk*SCr@GS!Og59a{ZR1ymIoin$B%n!ft+|m8}CzaG+3LRLmgIN zZF?@U+J7tTH;rw0P8=^neW2c2A;|~(l_xryBAhCGtjqOnkpVA+xwa%Kl5;ab!)Z=` z%awX{1N7Fl>t3-^6DIR5XaA*I`~`cyN*@XvarAe1_jkKEAQQ8{$D~n`>>=4CV^8f} zVTP}KQ~a!d$c9}|b3XX}No~i@0!6GG#yb{utT+`@huC61?r&bkd8eq|~KvfRG2Tc#*TYN5(eQNp7J<0*2A63)V-1~mO zJSgjJUXoRe`b9jwcESUu-7nACVE0QV`jg7l*ld;Y1hR>D=N#Ayu8;c}%!b8~@10bI zwRG+bzO`)}a80Xoo=c39(|t zZsw0*F>174A=(N#Bdz12hNfS}ule=4VS!|fulMZ9Ce$BoZFaN1>mJz@RER8`y}~;+ z2G?(3iv$mw%-yc0aX3LghkocE?#BphWT8-U=~#=OTKNET$O!Cd<@WVK8Ay|vXS1J> ze)Qo%1Z+6B-J_1o-+nL2RaYX?AhJa&L^b9CmtWQGb;M82(4$3ZOoik&HhOOt(#Q|j z1^Vq6QDsGy3w~hc3(97o>Fe8q+3c5uKNFDA+QMv6ML_! zT6!c$Gbx@8yJ{m_P-yQPC7b&m7BcA^X$@+NrNb_etx}P){^$Aw=Qigq#D6pAd-k{q zF-;Z1`{a!=98DZ^-c7>FmeAL+jz{F5v0COab12G3g-B6jWSw@Y&zjuV-%RL@N)#&+ z3`aRri+Q>k}-Ce)aBErN=!#6j|}%Cy428a3jyIr+Nuf z>|fdHuDd%96e*-Osd79IB+fSe%9!P|<4W}B1UB@&YlvE^LI1_f*Jyk}X#GtfcuWhm zwNAn_ag#$?EsV3j9jvGwybANObGk_kBc(Xl=46V#HVIc^t~$C2Hd$Ycv&_o$XD*8B z>4s)@CfvpIHW^*9O5<|j8jitxM}vePUFFOAI(pL4E3F_AU#&!8U`I$qG!vnRK?KwB*iI}@Cx`BW7q*iFdxWNJd7x@b~X9K+>;I4_8`Dc!xvU5DuxE0xlik4GW&K zb-G9$Tn1-acHf#@@xP%{f_t|p(OxCx2Sa|UvH2>qto;0F$>Pf`W!Jv^OZZ!pq_oAd z!w2fSbJ$eHd^|pPdTPat@b12DR@~Z7DZCcLC`;$fzv56w%Qq(w_Y4CseZ64(7A9PF zTEtj}Zfs08As0Pr^Cs-G34NeT-1b#{(>a1%@s~WlL;PWPcXfcrrZXivq`hbvwp{*~ z2k7!@x}h_D7h^Mf)Y=5Nc$I2x<|I}1*4&PGMQ9jyIUZMnSauf71Wm1=xjBqqhednm z_Yv{5YKZm`>OV_5I&hzw1(Ec6m)$KHYMf*|AO4;seUdvHX?d3}{rc&>l0LR0=uNDe5uybV`M`SQl20`98xP18ONpvV>XeDXi^Kh9CKx} zgRc&YQ%A=b78s}df(u=CEtxk)rqnJ)69+2jg^WYvh(c5PR=_Q9**14yqyrq^ZQZ9F zqa8ZC@+S5B=_lt#V^emN`5-ZO=$CX+HzNY?eN{>9C1?nd@N~kA*1NR6A}n`)5kTF2 zi}H5F65J%3+3LqciW86ka+O)X=r@YB@=Bp4lKU=4%BfH^>6`EISLL7~s_ka*hD{7r zZakytqJolur1g1*xNdm!ZWHtZaf%Z5BIJlj*9qAPp@WaGSk0^k>gwshM!B#7RTHS( zgaC?~Je|iadS%`0Ag17un^Ydt^EWBl%2~kxs!zGupYt-ACWv0@uWBDg(YqUe|NhDbs5p3Es6h2isKIJTc}-3o)=^&DH8J&G z1aoOuK~zJ39SfI&{hmwGXAF1|&v5XPoVQt8_O|);j7r1tq!Sik%C16YtErUxR zl!cBcW=dBjX1=dq$a?NVuJ2`xL9!JP6vA=UT8E5{jZMomHqgl4GoVco7@^4{WNoj1 z)p}`jo*tbFo>`p2kYpZkd{&^pj*#?r5enFuvsA;$oBWUZWo%Uk4BBJ*z{=;{3&B z)&0T3-W9=5BIAnWtqpG;vVk45^C%CcftQ3Qa~9P0T}l{Jwc?K+;g6YF!KD@{eG16O zwjcJb?FU{TFU0s<@G^9J{d!3~={Hy;`kU!7yTiQFuXz4|KjR@SZ+v@#>PrX7fY+bJ!l+RqkgW-ch?*}Pdk%wk2+X28(nCIA48*TXg-_MiyS&hB~-79t7L^HQ=UNM zc3t4AVH+>A(%_}7m>z8{d!OUJH;GjQ4vIsMK*qd;|T;4Oj z=-bY2bx?BY#mwtnq=@ppFQ~N*ZEQyV^x)ryw<ncN{eo5$5~F*%Ghby~D*dMy9(3r_rQUDBl{_M5(xR+M}rL3I#={3V)? zvA5A|4D8vqFz#G)5eGXoW`{OXE_QUH6^hi=Z=Vw6u#xtGnF*9emce6Y39}8i+grzP z_4>G{9jW<;InO$>McL_eO^KJUC-|}9M4R`@WN*W$ZBbo5J=8_>wl|*YEN;>d#U69` zW1IEAei16foqSM#lF?%A)n5r~o)P-J*3&8}y*#D&_%IDV=qgkpj=5yka9e&gFF05I z3oLpDz63#{LR*E)l+|rd!QUxBIHH2Y#^+ey_8karh4gGKj5tkSMm!XpF+Z&4+0&^9yA-CAcUMGsktY*_R#LRMX-}LAl>021Y5Mh5H zlUo;427XV~-8G>WC<~h!3bWmWyOXq*b+mdFADt|AM9vAxJ|yMv^2n(f76f_M`pt6O z&Pn^UVs4cju;SMdqQvdHjqN*-O^l;|Y8B$yw$*a-lePf9@o#hV~c^=_+e4pdr& zE0?y=Vvh%$z`~gFSNX18`i;hQ(&-s8;JM>P@ULxI^?T5;;M_bz)@cxB{Nm6?_(Co) zs^Ss{vN|EJD80#T&s*6F<&h*t@zq$N?yrt)E&kb|7doX*nehg5FP$Y7vV*y=zB7ui z=PiV^YNYl^eWj4#n*- z{3P>syEM@(rq>&C-FPf_ivjBFu~;@d8aag%$;e!a$Xt)5Z zVfiKN0eY;`osv~^v8T6f`ZKZ98C6T&!{takzKzqX*a5dtbou4^4$r83HF`^P@v2&- zle;+$Ub@@h9DhUyQZsoro=J{xWhr-J1y(AzeCWmskw20Hr~J+(_Swy9ykZ_>TUejF zYMN*4;O18a7HM@h<|z=A7^dz^%Q1AM(7K=^FfvQp&64uIo2}V7sM5i3V$Bf|9iM7) zt>NmlMs50#qvrsvcG=$*L#)G}f5tKcD*WQY+o{T4tA2UZ&pQ;`MwAazGsI9~MxS;g zyj~0ANFA3s%La>ifu#l@8*Nwj;xsUo{Cjmps1P?grkeHobF_PBG&gu=NYs-5&u?kV zBrbnCM-}nY&_qZ7gFtq)Rn@k_vA&9P97~Hee4E^2>$jCl{gcMa@U061 zmLInt5f@7Hg^SeG4@{;vSa*MXJi0o(Iq1TD0nx6 zM$i7$&o*Yfzl2Y+H96iljBytI_0sT8+6S-*bj4^x)dl`Cmr-Kc$}xBkhvZatVclh{ zyLX1|h~-10*ptUy#YWfwU>X`{JG3bG@fedLo&QmLc|)Gv&;p%HVmO>JSKt;@bJfy2HP_K|2B@;qD!!=@R>R3FVALqo5oFD*O zQz54OX{X7<>#ey+rhjv|k1NJ}=v`=~8eCe2tdaFa7-+qQqPb1S$Br<}b$3}V)6k`f zXf9$Pl3M|UO1Z0b2VkfHK$^ea+J8Uq4~?Ii!>nZSw{|yB5bcnP=>ar9I@&X3F`7A| z>c=V_Z++)%YyTpE!;ifEr{-t9FP&q#Uzhe~<2(fiQT=2n<`Tou0jpdS`<1mTGs}O`aP=Px>rVv+%ta4Feoy8O#^}L!k-p|Nse>2_hEbkeY zGUOs%tGNfpEBb1#83EGyq)%sf-(|n*RFFllZL$)z2^HNCbfH=ebx5s4sLJi!?Oq5&a;6BsGPKXz8B(4feTj}|@NiTd1! z6gK0P_~}a1Y_Tuxf#<)*ULO6bEv@EQtY&%RdHW)W{6gB5SVK(^1s!q`zJQGs_rsFw z^ov452xpgHx14k-cuM??;16|C=Ns7KUpF7f?K*GyFWQ6*nt{+!1Me&atn>NB*E02w zXip3M4u`^*!9=B>4d&qV1b$%e54;$eP=v`{IQ|&5or{wLU?Y-L0}IQEh)1fVD$d7$ zEmVHlf30C-Woz#1EiP5gJ1lRt6XPShIYWS5`$=|oMf2U-<_dHz$)o#o2kV(%=s5HK zjvY&VDBRBv0A6Aem8QXM;;xrGIvtQ+LWZ#?nw#vrvLn~`e^I#gXbh1#^m?4}6(`CF zf%Gn5FT$)*8ODshatSOqhx8)ECp+8CvLReAiiy4#(S}X7F3XRAC;d8-)s%r7M*Vm(sK1rThw5(SxT*jB1ctv0`(gcF z)PdiBIv%wGQ9w`d-I)S0tNs4AbXx=br~H@>h1J05hLY;%;@3fC*6^+ z{JWYN;m=}pP(jGV)VshOYTP*Rn1JD(8({Nsuku3e-*)U1YfKO`{hyM4^I&|^8P6U0=|Grh_ybRN`2_D0<)HvvahY9_T6oifGe5D#ReP0 zJa4&3hLSS`%ah7*&WVe=;}iqFkrQW!oRE;|J#^$JKnAK*9KP7AGZ}f~sm+!aS@u8% z4gRJ(L^mP_g6kZs2==1xow;B9_uLvuEXN=v@Cixt{M&LpKPf0>F7K_)p`~SL!7txn zAiCL$o4KLe*AkKWmVQ0rF^8FJ667E>_uy*}$fQ7& zDxye|^p7q=19u!&59GnEFsrh~_IUc;XL`a{1$Eun1*KZYi(SGZ++%q>6{X}Tn8H^h zql55>wVw-3y%a{DNNA2H$QWO6h7C3mTOBes z)g!nw;mWs*S4_{LesTEB>dQS?6msf7#5?M^yD?Pq6Y~0#$f0mjnR5CKo~hmtc@GoB z(T;}XWr#LR^#GD+l{y0hQB`+4z@{fr0dFie|CYKdwjwo_(Y|~qC|1^b(dC43ryDn9 z49tCW^vobcx0Mp{>ukYGEHPN{yBDVyArlNtbV6ca(rJebLeqdg6VdL@uQ;#usXb69 z?cm7B?&3gCy|pNPhgF!;sA^T0=rnT5O(AQ+uffiD!H8X4w;xXeFe6u8WN=P zbfVYgo*7|RWXQ1xb_V;ZOViNCbcJ^3pt3feAQQ^%3JkDtpSjUyUEE+Y%!n`tN9b$X zcH!4>#j>3`XT zVuPbs^8&Q$*B&6{HclL!GiqlfnH^Jf`{#V{>^~lZyw%l0<{6~KOeg}?E}q&TXio&f z)g#su!c#_O9_t&IBwSlT*8-BM266w{At`!leE4Xql9TJafaj{tzy>+Iu7fzYH;dN5 zX^puvKhHx(tY7N+{$?r9;D~>JmmD1Hk52CBXfyaUZ)N_$M;C~i`-sj@6V^W}j=CIQ zzv8?{pVRg9?crj3?`qSA#}%T_e8=VuN`oG7X9osQLl0u{T0sP|w+4{}KeqVv{D`LV z)f}Jud_Gbm9NyEB0F(DV&|?j64Ly=^8web zEygwBFlPWHCk&e1Mry@_DEe-sX5S4`9DLv8@^Q^c{%ljFt(0+>+aL3LBtiGlvnZds zHgd`>`Vx1eNZij8#|Jw`)y3_wL!M3#&S$r)pzL73#@!l2m+4eiQ>+|r;GO1Tu5dp`xU1Uuktmd-`RyY1I~3&&uq2^`6M zbiP6EhkIH?(IshxiioM<|BIdU>Ww#7UatId#S>&(%*xRi)SQT`x6w6gH7NV>*P?{V zSi?tK!hHbDF8TPx7=q$C@$Lp2UOi(a@BrS?Lj_waZU0uPK zd31prg~G7{3L-Hrow3ljLpF2 z!p_SlAxF%$xu2`=G0KU|YhWBNy>N~lq5`ny=x!Wc_YT9bm!GB}C1&rQ6dy;2axyU< zCp*GLyHF~`r`BEI-5POYB~az00C#?8^L`)gX%_ABRF}xEKr*)vPG7dc8vT4z{d~k( zA?}Hzvs+wf3@lQ4O+O#X-#X2spyqK*Ww)nT8(o8fD{-?n1&rBz=7wzLf-(^n_X#^@W zNu7slsJuNwqT>D)eYIBS^ab%;yZl;UHoG@`4^(WO2m1N~5*JsCs1MS*Yc7z#9;o2` z0zv`}+9ch1;`&w(ZFAVH!1q|~IYd4B#p`$(k4;ss5DCGXoAZ%xYHqSGN_+Z3zw#qK zw9S}f*zB%#E;OfSi^1JJOb%PXp10oC^g6_odXOfH3N9{7S|4gWbnYEP+33>T=G4Qh zy$be_7h%SB(PQ#Uf_tSOvp=;u0UM>Hc-n!+(_ncgkj1&u{) zU<&ch(Q);g&M6YI5zyp!C^_o#PDmQ51Nn}vZ$Eo&dD}0`Ky%C^EGiBX2SX{)o#V7 z9|8@~eqiLjHFg#<5*?WX{s}@1E<%H+_Iq@njF&DKDCmw4CVOuH$cfrNV86AEWJKR> z)U|Cg_!~6UpD(Ji0TU8O5FR)W2~n8)eJK+c8G|EQZ^1APLlKRiIrk3~oPB65*J?lf zgn^hD8aviNbZYoXr_Hd#bllLLP)2& z%uS|>wpy%5tCGp?0vkL6POULZNK95l%*Z)ewTcI?x)lcMix9dXoZ=8~evF%NdHr>k z^rE-4?HX~1?^TTsb^x}7={iT+Ad`hFNC53QEp#VyGvC?sy8;rTS%1unjY zF$?~gMHVzM98z%&tH#Q(vJ;=#QXjGp^1LA&n?pfOrJXaax@6BaOA}u!R7mGmcG2kPYJchjD9CF;UCK$1ScJF=VoJNM z#Wu>M$hmKDQ$0Vw$*M@#g$cu&)hsB{eMw)A---44#DUj{cjpF3b?>S{*{ZDO{*ZucB=b>YR`G$)wKi z{#{z;*o@mAdC{M&)_#vXadnCI8Oh(YbQ66irY^=zO$l~!yq>w$4m`&X!|~@nM$)B~ z&@8nE{h4W4Uwkc%tUq0t|I zo?Ff+J&+@8ZgPAJ!dn<8{p7++Bx=q--!#>s5|lYEX4QejwV-&Q~LL9B>Y zz>^^T`Y!`h=?6j9qWOIK!RIZF_%)w_o@JvKF85E=is`Xerc!-9g-Fo~j5}x?=%w4g zUCJ2#ZC7AGW)TBY1xMaXQJ^2(J}~$NRXdFN(XLPNhb;4sn1|_@Em8U!oN768NlMKT zpCwnUu;OQJ1>I%{Y}P-K|7JObulZ))Diwbv4c$U6Slai(eGf zylUAb|K!S{za9@xS+~EZdc;c&`rd%JIrOhF`BeN>%Ada>h6FTl|EGrlL(W!b^XfDe{xpCk4j6GD;D~)c#OqWB~h^ui_ht|ix$QB>U9kj{n07*)Z&wJ z3Q>>W6f}kZ5)H^l0cix@Xt~eKP$>l$FAR3_Vn?NKqXqyZZlc+9`a-e$c^)k}Ep{Ww z1|w(SKO;!o>B5S&wh+bJwy(Uw>6deUGg5vx)hpcnbl36I9E;Ok0vqJ<^A| z&C1{(*9746zK3uHzSe6o;zSLZ{PV{@Lw0gZ%-1@l<3UeeHMAo*gGX4*=EqeYVoUS? zEO=q}KUz7c5dM!|3=6~m?Z+)^m2HMwRDs&PK|h zf@dsdFa($u_19X)ZcOUzhaGQ0+^}*i)c!6$bcmGo=FdF0AYtgrSYh$i*7F@2wU+}9 zKHk;d&wJ7Tf-SF?6{-FrI)sNG;a=*Cb`<;y!y`S4ydRE%<~61J$Td~h)DW2e{}uPu zZ&7etxJt^9Lw8C@cMnRBbax{l4FW@#G?LOWAl)rUcPZUDw3MWz6-m~}GvEKErcWob|B#Y1AGfTRkJ!X|6$TE87wt19q-#ZnY49K~CJgx}IkfQyZ zhJWuaX_!)llkGthao>kMCOx<~pEoL!`J<9Y8u*%xKpFC2~!@3<`#k!$H<>A(!3Cf!^aAtqO4#HPkI z`*OoLmqk_?cLx~H=^3bHMBA^W@-4SMHL8uNXjl-`e#D)=jZ_?!(5%3fldiwH>pko{ z`=Bf*t>yQ^#%)Zo;FYd8DJ1q{{BsdL%}^z+oQs_^+6$)pgLwl2X7q$=$1auBjAu(^ z`gzBhP>+iehuhR(-b77pTE^FQA|qmWAssWkt5&)W05jeny}gV%)e?3|q;D8!?%9O- z3Fy1CHM;9ra25-lTRup-W*nXR50j$jwjf>>A&!ASIQf&h!8Yi!$0iui1_VZ`-W==l zB8h!C1Xi7Zo^2I)R0pBkKeWFSvJyoQ_LgL?3y5#RJOBl8y7rSA+@^lGVX7!(C4-lA zmBw~tB;F;7VbK~mWsIv*bW0pm_KEiOAC=X)a;UCr5ch_Asasf1@G4rWE_3Tu+SbK3 zu1ck~*r^LMhHV@PG3F;s47e!}_JKGf@#-)21f$AWX>CMzs0#gsb;|MD9W}YQz$+!Q zn>Z72Z-C-h%~zQBhFqpYuuc`BH3z%dL*qkrT^!$%7OUhAO=DVII9-D9%mppJ+{Pqg z(Fy@S=8OitAn&L;S*xB0MsLzQ_c}2iS4W-YY9X}x(2f6y(l}O}#;-b2zZ-0ZC{kq- zWz>i`h6_j!F)v+|lM)mBAgwOjHMWGvT|yqpAmKgl*;#eP#JcE3sQPOpNYk(+_6zMG zjAE7_!pSugToFB_15j_GC?^N4k@*3QSLps65dLLulERuK4_Q=;I38(UruQEvlf6~< zRFb4ke|3(=$>om(5xULO2K9Ckp^x_R0`^J6T({#IA>sikWKpD;_4ci8E`u7P3S~>p zoZq=Zy=JazCyk>en`;lSbvOr!EDPDFvK5Qq&(G(IOShHIa3aYti8$!qbfNoKn((!G zk8%7y&TGxAAc{J#y_eXEzWn*>No(B)M~fzk#wC)?y*|%hE}%FnDodP#9O|w>64INW zxNeyx*!Fuk(1ex6B1F0N?8qlcBJ4$;RL&-kASffMv$&G9P(}#1G2k6LhzHa`A=h;d zB)ufXrtV_PNWUzI?$w1F{`$AvcFdfQC78hBT!4hbSx<}wP!AhW-np|1{UY25AKkAd7j5@LBV$D3 z^{Ld=W)*{=?QqP_RHS@$3;a}yJC}U%zJx*LV>6O@SIn8mlQ9@WTZ4n|>1)QomxsoO(+($3*^nJo0+xZzf$gGV}pA_!tOSKcr9#RDP ziTT2Z(SAT^q~_jj^C`ESeq^Ri+|PY>CCZg(v6b0$NX`4l!9GE+N{3tYxoqmPa?&Kv z)6}^!!zmPQ*Q{!iy@HD_9X2y;rMJ(}6%A|lonOOp8EBx5{v_p~X7ls%J{?YXW`tHl zUVGYxGSuxyiZYO}^aEbhyD2+cyQgDfJWeOaVICJh(PHcYVX;B=!f$!vn$1?aii)QX zLyKZ_G29!X0}CQNpuTH|)^rDw2tQWJfV#a8F8_8$q|P^OX5y669udr28f_%WHmeWE zdxeZODBUT1MalV=zO4!S@X(^~2N57|934qkCltD(tPQB8i8*v zxO7TWzPOV_W<((G(~rQZ7(~OO4_Nf+MB|!o{G~izIv(!mU+IQ7E`PhBkWBJNhSm3k zb@*A*S?w$JF}P^u)}U)J)% z)n+MzYIelm!CEdf>cPGc4Dr#1MSFW$DsjU?ognO%fli$|XwDha@;AF@6B;uif)@i! zV8y=g+}DaLp-~))uggkbZS`rMs9jyxy((JRIo+64YYYb=MYBzqA8|d8;28OnYI^gu z-T|C)hTkL7^j4BALEJlM$KKV83Fz&HZcxtG^GdO;=D@)$fe*jWS_jlz20=iMjkk?c zdtT@Yk7^@(*?X-#A7mbi>`tUs)OlkUBtE2^r;>r@3+FOQJk(2bmKn=l`;aO7yI!J? zC&zwaR2bnMa6F2IQgQ!im`2-M^d9XW=#N%Y4~LTtdkm)36>)jdCl0-~<%SNs+~`!j z@`b~lmo$c-{RQHLyUJAcA(v-i9}@B`Atq=C-Gp@TJT`YV`%Bwlj0?5wq8xi=a?k9L z(pDW-avh?8bYUd3Iat1BLcrgr+cQ1Be074huII)A_Sa_QwO9+yo+)CbHli<^@gWz- z(slbG30y*o+9lrxxag<}T(a=`l; zdfv{QuTJ}J?%8470!MNQ2Y;KAy5jm276$ zsj55dz~0jUzXVmVm*WogqSnyUlNeB<@5v4Z_z@_cPL85ND?jz6fWVnM_ z#ohxA%cmb#$GZ`SlsdGP8*>WqCY#Q6LgxF=c$n&{Z5yi~O*uVpT4#p=EzK#hm&g5q zWpR~nS3++V6aFlL_VhOz3KcvzB(a}23vwf-hdh83u{XK7g*190$rER$ z>18Z{DLA{9MGHzXl^-fU6*v2l5xmPgHhWtXsw;QPE*dF%aXa?l3%)YFtq#p8w*)x} zx2aaKaXrU<$O#vTjG4TtRzT=TD~i{g5Irm{VU>2a4&evi&e0gIYX5CzzP< z5LpjPy&`B}!z!pFP5S0*lj_|~w)~Ts`sVSBR-JMn(;y+%yW4I>p6QuIE5W1u-Ql=W zyvf&wUK?9W2#RnSV7bjoy5TxMZXC2Xh>%?N$MRknfiIcmGT&i%eQaG$+wz45Ad!eD zxBMrI_r&iP3h+K(50oENibRxr8Kl5tR|~Bn$1?2ZG~e^GTWzCifgE->&#-XX-+`~M zQUf}0f9~^_jrLBEfa*bu7+VxvmJ3+qFTH_9F-KkJ;(m)cl?)B-^FAOY9=r3^P*>(R znHk7X7S4JK;|@nRCO}FZkAr7+^8wM&3Zx5stnyw!W5==K(dCt*)fx z3wfa<{5h}CJrBRZucvzN1lge3gA(;JvwJAB@a&kdNUU#=-OaQlutdQH^!!=YqX587 zk{22`+Bpp{L>cXDkSucEzZM@RnZU#4Q8cmLk2)P#v6bwKOs#w7_I7@@d5W`K+D6ql@|eTB|Bl>A#$Z_R8T)q_M-^f21!Zsu;?tdbgz8HY`CPLPSSn0 zV+m)hkBNg$S@mf0C6SdBFRY1UY*|A}mI>9+$p)b<>uXcXV<9p`%Wzvr~G?mB$GE`tq*Nd zxLd`{-kWz9Tj3Mq(BxamG(r&ga05wsHei9hgym3uyauq<_r9=el7T>LHkB5w#^V3% zJF6==5~S*8gaH|&LRJTWl77som-&n)wQ|j&Jp#NAIXmMF?CmTpAN%|a>^@S9sKZ4A6t%O6S>-) z|MkOzA|c2I8mpm6us-#{ThQeIr@>(%@`CJ(*^f#@LBDkrBk6*i)`kg`nT19_kE-oD z0rR=~;zHA;Qh;h~mB~=nAIh1(U4L4QvKsL0!uN2~47G@@MB*@5A9SuK*H|n{pB2Td$4f?y{mMt#l4S>{OMd_Ni}B`S*XpC2ykpFJv2-2U#kdrX~(3Z{}^lr zys#^i&9}IuteEr98lo+&1TbNpVo3o+butVC2mpCs*Y(2WuORU>GaN5`x4~hWn$6ML z)<$Y+PVlXCi1_Y;tLpoD;eYUGJ$MOP39a+j*S1!=6&Xkpv(~Jmts^oQI}!r_sU2JK zo`xca=^oe}b{UfG|4af^u`{wwJ5EonqD9Z;o2wluWz?wO(|yv01LjCQVv>I|x8YBD zw*NN(=B=`XaFQHyh(yGGx}JvS+^3KK3&lp1mKGB7)dQH4m|?qRnm$)?mAR#)LcjXJ z3Pb%x!Mv@pulrkepZ^p5g=4rsib}#8@p~DpsM%Pkr0KsGYcjc-SNnf&32_Cizp6X? zfCEjf6pLs~WPP)RPu)>XEZtY|UjVWW>#VZ+f@cVLbWqb>0~S8> zhwrWQf05R7U5vHE9j~ntRa*4barktMr>$ z>X_N5fzAghVeHaws)rzsO6z(emzLR1`x6ZuM52!THymwtuRy=8Cs%-5Uk}jG(khev z(Er?`xTksc**^U0#S3eU1nu?)z^7&3n2P$(wLB3k71h$~K1o2BLd(Eq2>Pgjar_Vs z=ScUyrtNhfk3iK@r9p9Rw*B*x2Humxas3-U$Bl5cn$5+I2^;tT&6P6yG<}?n#8tn# zXZ%AbN0z32o9zV~nQFw|qfP%DT#T)Ei&IsQ+L>_9f2l3@x7~;DT2Vs`hFDltJ02=N zH7dBDYTMSwRW=uD-RHKJFC(;v{jF;J;FzebQ4-c#_~!fGzXKtfnsaNu3fz%H-`t4) z{3qajZLeviDw=h;;^?&D`3zi;xvadh3%>jpTfpD`rJd-V#+$%x1Q z8;bu|SmMUN`#(ZW#nJz$wrumyn#}(l=J|(I{D1xA21TwwO}`?92IjA*89GOVZ3^3l zHJ&mc*DrR=!5JO|2a7KF92;j7UMYs?_+uI#>f*EDzl=Hw32frAJoXNjOWHox zbk>;|Ad-QznCc!l@NK7B8j8S8vEuEOoVDlOwdz%>6)h5ErE)$2r-x+ab8t=XQ}Y<9y-KA&E_-YQXvYEO8?e4vXuekbf|%Eanee{mDM3f>P;ZfoX>#j!kX@ zymQK?U&zfFK6t-77gux^7W0de5%2RS4=Ry!ev^s%26h1_8;IwzvQScq(dU&nOg#O< zC9O-?pm6xqJOS(3qlmhQRAZ?iVs@~1N%{6mCE-69c#4L){s&uu<5^_ao^%vfvkNLN zk}`ffStW<|qe5eNeE|PM9vVaa5{0&?m02~gKE|G(0`hBj&hZh1!?v9@$v*7yxIa@j z1Y8w&zv!pHn3@Vw5-862;))q5FX!rhs_9n~S4pT_Q|0VB1TRcY@Jz&(j5tHT>stj` z=*U92NPJqcDnF*nN6PZRx->clxT2@C41P>GG`cctl#}mEaD7HcHH=U+d{iRD0h?PI z!_xEX8m9@`2KT#@hgo3MF%Z@PhKr|uV~Br0`<`D ziXb|Lf?0UZd(5f))w+e57(N>$RDB(`juw}plXF#Q6&+JTC_CU55E#LjydrP&+0PKW z<<`pG^dNxJ=`-*cQ+|+KnD*_ORJoW3#a@BpYD1C3+1!_Eui3ZPjY4Qw!N~cLm0cm5 zr!lLZv3CF{5o&4a$w{ZgJa{YBi5vM>36+y;kHq=z23p!MF0Q_Ea8zm--qp*T#J0QN zrb4A@uI5jhjze%kY{ttO|CHu9CR>UWx`I?YwE6=CYvf$ z?h7~)ya@T4*q&=??jvlmpCxl4`^S%#YviDy0d-}WxIDw ztNG&SSU;To(e}8#BfXVKFy`@v#q(FV&qSC$*-3{A5=ZJV&IZ;+dcpMdD`r6~^>fT_ zrr!G66?s^t(#~rpj&X|cvT-hF!35Mby9GoA*>ggxn?lN2!CM)38@E2$J0kPqx9M2K zq@t9n>bYE|K7yhKFiyw<7yg&k-pZ0EVY&CF?3$hK!*7Q58ZqR}QcN~dpXlgXzx*JJ zuSCDScf>NMrX3@{df0Vip>E39@P3f~*#|AV&KpJj&*o$?u;JOhsxQ*b+XAsQ=0-?% z1+#J36cJ2^%Of!nUBTmUUes5UV-a9WRqN4mA0ae;wAYL{`CvKfNo>vu1qhADjBull zWQubcM#4aA+Za;y$}BMcW;rujkp_#?oh0^*L;jqS6L&BDr?V(TbW2U2>283=>j9Vb z&WB4ZgCDI21f#x63tX)|_aj>rdIUL8gxc;bFE3Jv$7h=sUbaMM2?HqsBH?x*E;mWh zcjwwCZc@T%qE-$)%g%Ib2RwRL-niS(-<>;wdvSG`-q(EJnWn7yg9(VSUR+?#*ZN|c zY1?-%&-1mB{^*#G&Tz-gIciWjsKO`*qhdy88xBOk>Afe zUCoHx^Ccb5VJrqhrR#&19TfBApAYPUdV_0xE{yM5>_Th>C;8|p0_qCz**CRmPu+l} zw)-kY_x2jvNqL+C@G|jrDyuckia|dcgov7gH#%n+J=Bc~ZXL>4hTYhsmv^E4{YM-ZDJVMmJnTPiEScaIF?L6J4;L<7`%VU@=k;nN`Mlr8;fr-Hg^7V*=+u8w$;lk zyA(|i;nH`#9DvZ0e7DNxm6%bdeD#Osr^&P=cRPpTN-4!w0gzF5bQg#9fD+^9pGt32 z$|fGP#vBKzn=l|N6OHk!koQLUcp|84^U!We7qz5}@RF?A>t1FIGsKJoJ{$3ZV{qtZfX=C@Jo zTfg{9lGmsDYtIupg-nDfwMhUU;g53 z4L)YsXKL=qYmTxdu7cvFc_ZPfm9|uT<{{sk4(Twfv-Ba$fdI4_dU8Jl4H! zvoMrR4U_P$knB&1IyZq2MektX*E0TX#XHb3^O zAHn1ee)Uvk{D-?U^WIxXM+^@oORgJ+7@6QA2dSgBW1J%g)c9ev$Q-{DOyX$Obvzt? zF#t_hd$pH*eu7N)f#+A7jHn^o^w?w&zSWd{KM!1_wNYevMm;rJ z;;tb2E&A?=Il*riNOD03i!9?i#^2MZm!7Y4!XNGflsBu{tToBdLSsPNUUN?u{kSS1+7m7G36el`>HdQdoS))IEXBZ3 zfqaJZ3-#_TEu(s$aj`#sZ0VRbCgqG*N`p`&_!^oxY=)+CjICKito2>3HBYWp2EHeR z(n6Wcs4xtm#b+DSqOg$%3Zl#(tuOVw^Akd-*$Z?Y&#_3qV@7>xcwaJ9)R?Ke;x%p( z#pBnyKcRb#RU7oiiE~tJW~s3|p!p*{YwSrt+B``@LoeNnHeyURXhHG!E_Rsj%FXjh z>wV|@lXvradZvTH_k(=%8N7oXZsd{__9mOah%(&@#y#2Qq`4Kg$t0`0NVKDO;rCh_ zx?^7=zsNQ#)zgz)v#ld(4Mzqns{xv>@H2f^NcC;AGQ(C)yum2Qo5Kn0Kdyz}?q`|n zcB&j+9miZ0QPubB--88`;(vRTZ>e$kKQ8-k=}RdR(Jn7Z8V+yFTyKuOfra87+XP58 z6Nj=W&h{HyiV%qh9uAqnRZu+@5^~yS1`XvFTuh zdf+7ACFVQ^W7S`EyoU|~v;wm?JyrEhcyKqPD73$?b|Y~Rr7n1_mp$fh*y|QCeP1PJ zPv{)><)WOqWR=oPnR~UYozh~n`n4?k%A~pi|FWIOxoEyqWQuoVC8EAM1-yp)8D-2y zKl|51yi(9s<3gP7Jk8He$)KyqANKiE6U7rM_b5cOu_55+vb#s9%~!eXucba$u_%1c znt#}o4{|Yjyup=NhEl&H56%06zSK=m_nQ|u@#Pai#`?_z7;Vuq?@~5vYHca3OE~(zO8as02~h5d0fo%I{$l0bf2o=Go}2EeIb3>b z0cqXjuvUGkq`+Pj`Dt{{RE((W*CERCc zP26Y+g~$s-R@fA8S|nB2hjVyKvWDgXNP4ct4j`2?Hs9p+PqkjXy@p0mT-|vegXa>1 zF1p^OvM^B7R46>Ug5XC{VG|H@1f?|XvZ|x88UtcRB4gJCYj`Tf=VwIm)aCFNYhu*1 zvi3&|?LHxWCB?LuENvjIAR+e9qEX6UQ|iRM89G87;N{#v0xcX$jPw(e8(Msers`HBt+rh&||`qKpf$UtF!oGZ3~3fqGZ^Y zM`S$+R_<$KDXL*wCSx!Yx--9|Zjr0qVIc|9AV zGH7qIhaVL*H#JccUtB|MbLgbqhBu-;$0)0A2aWc%Z$W>jaDVsWG;`VEN*IwnXPL~SGBmN)yhe@w@-B+K%kGkF+Ux{?xL9xEekO5n&uD*!qH-VuJp!Qvo6)Nevis>~ujI70LGMoqOb zR_3yK;gGK>QSoxlK7MrC4Izfo0FlaDWDpu=GLFO1i198YNeTt8gi-6^1ZhUmtG-_c ze(f53vX;0x#Cvw-`aWVFgyCj@`PBTDk=$+36zF4|-kr_*K76{+c1!UhjdtxeACX%Y zsRtTi@#E-LiJCZAqc`Oqwon|P zq-mg__krycJ)-pPwFdv(J-lmZBJF->FzbUwfBY6KpPhBN_}1M_%Qu=O)@6L+sb)R634$wcrQA2T zruqyk<9(n0=82$kiEoHZrann18cS?$^%8w%dNPV=2e8W9=wy)VSq3kW@xyto8@=HO ziyf2;*dtlAaqEo?%1f6C*_gYk@!#Z@3XSdyUsH+7s6lE`UX>F6(m|&(mrU)@qkwTn z6#~2RT0vA+`4}It7bs2)yhRM=(dfelidcxgr!#l@KC{F>&Pl%asj{bTb<||4gV4-C zhv^OxzBKS!6lYb3g8l*&ZZclF>YD6+LN(|vg;F$Nk!-pF#eqqVm`!oZraM_`dmtptH2yn!YxnwERx3Grv5ol!gsTCDszAw+-E8(Y7_T4MnCG{yx_8Ub)CrROj<+G zUnH4yH@MHHfIv=Ec`T&1q5Za}ZPqGYNySJr&s&mU(Z4DEFR+awyuLqKekTAPV!z4u z(#e)-F5D)}-yz)7n&RW-KU?`*3zXNSWQG)$TThhstGKxG@Zm&cZwkyvZ7KyZMblt*yHa0yoejS#b&X$p5~y*%i#JrsMR$7~ zQu_}fA3FkP%avu9`)6FTdiYIf;Zad*omi|Zf@#TW#!u!$vXOzZa)d-Jjl7(i*SAkK zFd%=YZvv<_iguM){J{o&utC2la-kNd??Q5 zn*1|ozzZIkzo{3we*OP2qp^RQ5y_ut6A-BS(){o4pxw8M`ny_E_fh{}JE?m_c(qb4 U37iC>J%P8^(n?a55+?8e53_P`4gdfE literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 8006eac4..ef746518 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,7 +94,6 @@ nav: - "Integrations + projects": integrations.md - "Release notes": releases.md - "Emojis 🥳 🎉": emojis.md - - "Template Functions": sprig.md - "Troubleshooting": troubleshooting.md - "Known issues": known-issues.md - "Deprecation notices": deprecations.md diff --git a/server/config.go b/server/config.go index c5560010..9c1c4e10 100644 --- a/server/config.go +++ b/server/config.go @@ -11,6 +11,8 @@ import ( // Defines default config settings (excluding limits, see below) const ( DefaultListenHTTP = ":80" + DefaultConfigFile = "/etc/ntfy/server.yml" + DefaultTemplateDir = "/etc/ntfy/templates" DefaultCacheDuration = 12 * time.Hour DefaultCacheBatchTimeout = time.Duration(0) DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) @@ -173,7 +175,7 @@ type Config struct { // NewConfig instantiates a default new server config func NewConfig() *Config { return &Config{ - File: "", // Only used for testing + File: DefaultConfigFile, // Only used for testing BaseURL: "", ListenHTTP: DefaultListenHTTP, ListenHTTPS: "", @@ -196,6 +198,7 @@ func NewConfig() *Config { AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit, AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit, AttachmentExpiryDuration: DefaultAttachmentExpiryDuration, + TemplateDir: DefaultTemplateDir, KeepaliveInterval: DefaultKeepaliveInterval, ManagerInterval: DefaultManagerInterval, DisallowedTopics: DefaultDisallowedTopics, @@ -258,6 +261,5 @@ func NewConfig() *Config { WebPushEmailAddress: "", WebPushExpiryDuration: DefaultWebPushExpiryDuration, WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration, - TemplateDir: "", } } diff --git a/server/server.yml b/server/server.yml index e1a58232..db968498 100644 --- a/server/server.yml +++ b/server/server.yml @@ -126,6 +126,26 @@ # attachment-file-size-limit: "15M" # attachment-expiry-duration: "3h" +# Template directory for message templates. +# +# When "X-Template: " (aliases: "Template: ", "Tpl: ") or "?template=" is set, transform the message +# based on one of the built-in pre-defined templates, or on a template defined in the "template-dir" directory. +# +# Template files must have the ".yml" extension and must be formatted as YAML. They may contain "title" and "message" keys, +# which are interpreted as Go templates. +# +# Example template file (e.g. /etc/ntfy/templates/grafana.yml): +# title: | +# {{- if eq .status "firing" }} +# {{ .title | default "Alert firing" }} +# {{- else if eq .status "resolved" }} +# {{ .title | default "Alert resolved" }} +# {{- end }} +# message: | +# {{ .message | trunc 2000 }} +# +# template-dir: "/etc/ntfy/templates" + # If enabled, allow outgoing e-mail notifications via the 'X-Email' header. If this header is set, # messages will additionally be sent out as e-mail using an external SMTP server. # diff --git a/server/server_test.go b/server/server_test.go index a783dbd2..c904cbb7 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2918,7 +2918,7 @@ func TestServer_MessageTemplate_Range(t *testing.T) { require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) - require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com\n", m.Message) + require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com", m.Message) } func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) { @@ -2971,8 +2971,7 @@ Labels: Annotations: - summary = 15m load average too high Source: localhost:3000/alerting/grafana/NW9oDw-4z/view -Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter -`, m.Message) +Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter`, m.Message) } func TestServer_MessageTemplate_GitHub(t *testing.T) { @@ -3073,18 +3072,75 @@ func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) { var ( //go:embed testdata/webhook_github_comment_created.json githubCommentCreatedJSON string + + //go:embed testdata/webhook_github_issue_opened.json + githubIssueOpenedJSON string ) -func TestServer_MessageTemplate_FromNamedTemplate(t *testing.T) { +func TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", githubCommentCreatedJSON, map[string]string{ - "Template": "github", - }) + response := request(t, s, "POST", "/mytopic?template=github", githubCommentCreatedJSON, nil) require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) - require.Equal(t, "💬 New comment on issue #1389 — instant alerts without Pull to refresh", m.Title) - require.Equal(t, "💬 New comment on issue #1389 — instant alerts without Pull to refresh", m.Message) + require.Equal(t, "💬 [ntfy] New comment on issue #1389 instant alerts without Pull to refresh", m.Title) + require.Equal(t, `Commenter: https://github.com/wunter8 +Repository: https://github.com/binwiederhier/ntfy +Comment link: https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289 + +Comment: +These are the things you need to do to get iOS push notifications to work: +1. open a browser to the web app of your ntfy instance and copy the URL (including "http://" or "https://", your domain or IP address, and any ports, and excluding any trailing slashes) +2. put the URL you copied in the ntfy `+"`"+`base-url`+"`"+` config in server.yml or NTFY_BASE_URL in env variables +3. put the URL you copied in the default server URL setting in the iOS ntfy app +4. set `+"`"+`upstream-base-url`+"`"+` in server.yml or NTFY_UPSTREAM_BASE_URL in env variables to "https://ntfy.sh" (without a trailing slash)`, m.Message) +} + +func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "🐛 [ntfy] Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title) + require.Equal(t, `Opened by: https://github.com/TheUser-dev +Repository: https://github.com/binwiederhier/ntfy +Issue link: https://github.com/binwiederhier/ntfy/issues/1391 +Labels: 🪲 bug + +Description: +:lady_beetle: **Describe the bug** +When sending a notification (especially when it happens with multiple requests) this error occurs + +:computer: **Components impacted** +ntfy server 2.13.0 in docker, debian 12 arm64 + +:bulb: **Screenshots and/or logs** +`+"```"+` +closed with HTTP 500 (ntfy error 50001) (error=database table is locked, http_method=POST, http_path=/_matrix/push/v1/notify, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=30, visitor_id=ip:, visitor_ip=, visitor_messages=448, visitor_messages_limit=17280, visitor_messages_remaining=16832, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=57.049697891799994, visitor_seen=2025-07-16T15:06:35.429Z) +`+"```"+` + +:crystal_ball: **Additional context** +Looks like this has already been fixed by #498, regression?`, m.Message) +} + +func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened_OverrideConfigTemplate(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.TemplateDir = t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "github.yml"), []byte(` +title: | + Custom title: action={{ .action }} trunctitle={{ .issue.title | trunc 10 }} +message: | + Custom message {{ .issue.number }} +`), 0644)) + s := newTestServer(t, c) + response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) + fmt.Println(response.Body.String()) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Custom title: action=opened trunctitle=http 500 e", m.Title) + require.Equal(t, "Custom message 1391", m.Message) } func newTestConfig(t *testing.T) *Config { @@ -3093,6 +3149,7 @@ func newTestConfig(t *testing.T) *Config { conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") conf.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" conf.AttachmentCacheDir = t.TempDir() + conf.TemplateDir = t.TempDir() return conf } diff --git a/server/templates/alertmanager.yml b/server/templates/alertmanager.yml new file mode 100644 index 00000000..803bbfcb --- /dev/null +++ b/server/templates/alertmanager.yml @@ -0,0 +1,29 @@ +title: | + {{- if eq .status "firing" }} + 🚨 Alert: {{ (first .alerts).labels.alertname }} + {{- else if eq .status "resolved" }} + ✅ Resolved: {{ (first .alerts).labels.alertname }} + {{- else }} + {{ fail "Unsupported Alertmanager status." }} + {{- end }} +message: | + Status: {{ .status | title }} + Receiver: {{ .receiver }} + + {{- range .alerts }} + Alert: {{ .labels.alertname }} + Instance: {{ .labels.instance }} + Severity: {{ .labels.severity }} + Starts at: {{ .startsAt }} + {{- if .endsAt }}Ends at: {{ .endsAt }}{{ end }} + {{- if .annotations.summary }} + Summary: {{ .annotations.summary }} + {{- end }} + {{- if .annotations.description }} + Description: {{ .annotations.description }} + {{- end }} + Source: {{ .generatorURL }} + + {{ end }} + + diff --git a/server/templates/github.yml b/server/templates/github.yml index 5d1b0b46..2c2922a2 100644 --- a/server/templates/github.yml +++ b/server/templates/github.yml @@ -6,7 +6,7 @@ title: | 👀 {{ .sender.login }} started watching {{ .repository.name }} {{- else if and .comment (eq .action "created") }} - 💬 New comment on #{{ .issue.number }}: {{ .issue.title }} + 💬 New comment on issue #{{ .issue.number }} {{ .issue.title }} {{- else if .pull_request }} 🔀 Pull request {{ .action }}: #{{ .pull_request.number }} {{ .pull_request.title }} @@ -47,6 +47,7 @@ message: | {{ .action | title }} by: {{ .issue.user.html_url }} Repository: {{ .repository.html_url }} Issue link: {{ .issue.html_url }} + {{ if .issue.labels }}Labels: {{ range .issue.labels }}{{ .name }} {{ end }}{{ end }} {{ if .issue.body }} Description: {{ .issue.body | trunc 2000 }}{{ end }} diff --git a/server/templates/grafana.yml b/server/templates/grafana.yml index 42a16deb..658aa550 100644 --- a/server/templates/grafana.yml +++ b/server/templates/grafana.yml @@ -1,9 +1,11 @@ -message: | - {{if .alerts}} - {{.alerts | len}} alert(s) triggered - {{else}} - No alerts triggered. - {{end}} title: | - ⚠️ Grafana alert: {{.title}} + {{- if eq .status "firing" }} + 🚨 {{ .title | default "Alert firing" }} + {{- else if eq .status "resolved" }} + ✅ {{ .title | default "Alert resolved" }} + {{- else }} + ⚠️ Unknown alert: {{ .title | default "Alert" }} + {{- end }} +message: | + {{ .message | trunc 2000 }} diff --git a/server/testdata/webhook_alertmanager_firing.json b/server/testdata/webhook_alertmanager_firing.json new file mode 100644 index 00000000..9155bd9e --- /dev/null +++ b/server/testdata/webhook_alertmanager_firing.json @@ -0,0 +1,33 @@ +{ + "version": "4", + "groupKey": "...", + "status": "firing", + "receiver": "webhook-receiver", + "groupLabels": { + "alertname": "HighCPUUsage" + }, + "commonLabels": { + "alertname": "HighCPUUsage", + "instance": "server01", + "severity": "critical" + }, + "commonAnnotations": { + "summary": "High CPU usage detected" + }, + "alerts": [ + { + "status": "firing", + "labels": { + "alertname": "HighCPUUsage", + "instance": "server01", + "severity": "critical" + }, + "annotations": { + "summary": "High CPU usage detected" + }, + "startsAt": "2025-07-17T07:00:00Z", + "endsAt": "0001-01-01T00:00:00Z", + "generatorURL": "http://prometheus.local/graph?g0.expr=..." + } + ] +} diff --git a/server/testdata/webhook_grafana_resolved.json b/server/testdata/webhook_grafana_resolved.json new file mode 100644 index 00000000..41494578 --- /dev/null +++ b/server/testdata/webhook_grafana_resolved.json @@ -0,0 +1,51 @@ +{ + "receiver": "ntfy\\.example\\.com/alerts", + "status": "resolved", + "alerts": [ + { + "status": "resolved", + "labels": { + "alertname": "Load avg 15m too high", + "grafana_folder": "Node alerts", + "instance": "10.108.0.2:9100", + "job": "node-exporter" + }, + "annotations": { + "summary": "15m load average too high" + }, + "startsAt": "2024-03-15T02:28:00Z", + "endsAt": "2024-03-15T02:42:00Z", + "generatorURL": "localhost:3000/alerting/grafana/NW9oDw-4z/view", + "fingerprint": "becbfb94bd81ef48", + "silenceURL": "localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter", + "dashboardURL": "", + "panelURL": "", + "values": { + "B": 18.98211314475876, + "C": 0 + }, + "valueString": "[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]" + } + ], + "groupLabels": { + "alertname": "Load avg 15m too high", + "grafana_folder": "Node alerts" + }, + "commonLabels": { + "alertname": "Load avg 15m too high", + "grafana_folder": "Node alerts", + "instance": "10.108.0.2:9100", + "job": "node-exporter" + }, + "commonAnnotations": { + "summary": "15m load average too high" + }, + "externalURL": "localhost:3000/", + "version": "1", + "groupKey": "{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}", + "truncatedAlerts": 0, + "orgId": 1, + "title": "[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", + "state": "ok", + "message": "**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\n" +} From 57df16dd62304adc725138df1e844bfb7201231a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 15:44:49 +0200 Subject: [PATCH 177/378] Remove UUID --- docs/publish.md | 1 - docs/publish/template-functions.md | 11 ----------- go.mod | 2 +- server/server_test.go | 4 ++-- util/sprig/crypto.go | 7 ------- util/sprig/crypto_test.go | 21 --------------------- util/sprig/functions.go | 8 +------- 7 files changed, 4 insertions(+), 50 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 6410bece..745c99c4 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1248,7 +1248,6 @@ Below are the functions that are available to use inside your message/title temp * [Path and Filepath Functions](publish/template-functions.md#path-and-filepath-functions): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs` * [Flow Control Functions](publish/template-functions.md#flow-control-functions): `fail` * Advanced Functions - * [UUID Functions](publish/template-functions.md#uuid-functions): `uuidv4` * [Reflection](publish/template-functions.md#reflection-functions): `typeOf`, `kindIs`, `typeIsLike`, etc. * [Cryptographic and Security Functions](publish/template-functions.md#cryptographic-and-security-functions): `sha256sum`, etc. * [URL](publish/template-functions.md#url-functions): `urlParse`, `urlJoin` diff --git a/docs/publish/template-functions.md b/docs/publish/template-functions.md index 238bddd9..4c6b6dfe 100644 --- a/docs/publish/template-functions.md +++ b/docs/publish/template-functions.md @@ -18,7 +18,6 @@ The original set of template functions is based on the [Sprig library](https://m - [Type Conversion Functions](#type-conversion-functions) - [Path and Filepath Functions](#path-and-filepath-functions) - [Flow Control Functions](#flow-control-functions) -- [UUID Functions](#uuid-functions) - [Reflection Functions](#reflection-functions) - [Cryptographic and Security Functions](#cryptographic-and-security-functions) - [URL Functions](#url-functions) @@ -1357,16 +1356,6 @@ template rendering should fail. fail "Please accept the end user license agreement" ``` -## UUID Functions - -Sprig can generate UUID v4 universally unique IDs. - -``` -uuidv4 -``` - -The above returns a new UUID of the v4 (randomly generated) type. - ## Reflection Functions Sprig provides rudimentary reflection tools. These help advanced template diff --git a/go.mod b/go.mod index 88b88463..dc35ae8b 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require github.com/pkg/errors v0.9.1 // indirect require ( firebase.google.com/go/v4 v4.16.1 github.com/SherClockHolmes/webpush-go v1.4.0 - github.com/google/uuid v1.6.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.22.0 github.com/stripe/stripe-go/v74 v74.30.0 @@ -69,6 +68,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/css v1.0.1 // indirect diff --git a/server/server_test.go b/server/server_test.go index c904cbb7..ad2bb8fd 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3083,7 +3083,7 @@ func TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated(t *testin response := request(t, s, "POST", "/mytopic?template=github", githubCommentCreatedJSON, nil) require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) - require.Equal(t, "💬 [ntfy] New comment on issue #1389 instant alerts without Pull to refresh", m.Title) + require.Equal(t, "💬 New comment on issue #1389 instant alerts without Pull to refresh", m.Title) require.Equal(t, `Commenter: https://github.com/wunter8 Repository: https://github.com/binwiederhier/ntfy Comment link: https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289 @@ -3102,7 +3102,7 @@ func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened(t *testing.T response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) - require.Equal(t, "🐛 [ntfy] Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title) + require.Equal(t, "🐛 Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title) require.Equal(t, `Opened by: https://github.com/TheUser-dev Repository: https://github.com/binwiederhier/ntfy Issue link: https://github.com/binwiederhier/ntfy/issues/1391 diff --git a/util/sprig/crypto.go b/util/sprig/crypto.go index 4d027781..db8a6814 100644 --- a/util/sprig/crypto.go +++ b/util/sprig/crypto.go @@ -7,8 +7,6 @@ import ( "encoding/hex" "fmt" "hash/adler32" - - "github.com/google/uuid" ) func sha512sum(input string) string { @@ -30,8 +28,3 @@ func adler32sum(input string) string { hash := adler32.Checksum([]byte(input)) return fmt.Sprintf("%d", hash) } - -// uuidv4 provides a safe and secure UUID v4 implementation -func uuidv4() string { - return uuid.New().String() -} diff --git a/util/sprig/crypto_test.go b/util/sprig/crypto_test.go index bad809a5..d6fb1736 100644 --- a/util/sprig/crypto_test.go +++ b/util/sprig/crypto_test.go @@ -31,24 +31,3 @@ func TestAdler32Sum(t *testing.T) { t.Error(err) } } - -func TestUUIDGeneration(t *testing.T) { - tpl := `{{uuidv4}}` - out, err := runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - - if len(out) != 36 { - t.Error("Expected UUID of length 36") - } - - out2, err := runRaw(tpl, nil) - if err != nil { - t.Error(err) - } - - if out == out2 { - t.Error("Expected subsequent UUID generations to be different") - } -} diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 1cd026c6..10ededd6 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -58,10 +58,7 @@ var genericMap = map[string]any{ }, "substr": substring, // Switch order so that "foo" | repeat 5 - "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, - // Deprecated: Use trimAll. - "trimall": func(a, b string) string { return strings.Trim(b, a) }, - // Switch order so that "$foo" | trimall "$" + "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, "trimAll": func(a, b string) string { return strings.Trim(b, a) }, "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, @@ -220,9 +217,6 @@ var genericMap = map[string]any{ "chunk": chunk, "mustChunk": mustChunk, - // UUIDs: - "uuidv4": uuidv4, - // Flow Control: "fail": func(msg string) (string, error) { return "", errors.New(msg) }, From dde07adbdc9c020a6c163a420f68121688d970a0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 16:46:53 +0200 Subject: [PATCH 178/378] Add some limits --- docs/publish.md | 15 ++++++++++----- server/errors.go | 1 - server/server.go | 6 ++++-- server/server_test.go | 36 ++++++++++++++++++++++++++++++++++++ util/sprig/defaults.go | 2 +- util/sprig/functions.go | 7 ++++++- util/sprig/numeric.go | 10 +++++++++- util/sprig/strings.go | 9 +++++++++ util/timeout_writer.go | 4 ++-- 9 files changed, 77 insertions(+), 13 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 745c99c4..dc124dbc 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -969,15 +969,20 @@ To learn the basics of Go's templating language, please see [template syntax](#t ### Pre-defined templates When `X-Template: ` (aliases: `Template: `, `Tpl: `) or `?template=` is set, ntfy will transform the -message and/or title based on one of the built-in pre-defined templates +message and/or title based on one of the built-in pre-defined templates. The following **pre-defined templates** are available: -* `github`: Formats a subset of [GitHub webhook](https://docs.github.com/en/webhooks/about-webhooks) payloads (PRs, issues, new star, new watcher, new comment) -* `grafana`: Formats [Grafana webhook](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/) payloads (firing/resolved alerts) -* `alertmanager`: Formats [Alertmanager webhook](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) payloads (firing/resolved alerts) +* `github`: Formats a subset of [GitHub webhook](https://docs.github.com/en/webhooks/about-webhooks) payloads (PRs, issues, new star, new watcher, new comment). See [github.yml](https://github.com/binwiederhier/ntfy/blob/main/server/templates/github.yml). +* `grafana`: Formats [Grafana webhook](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/) payloads (firing/resolved alerts). See [grafana.yml](https://github.com/binwiederhier/ntfy/blob/main/server/templates/grafana.yml). +* `alertmanager`: Formats [Alertmanager webhook](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) payloads (firing/resolved alerts). See [alertmanager.yml](https://github.com/binwiederhier/ntfy/blob/main/server/templates/alertmanager.yml). -Here's an example of how to use the pre-defined `github` template: First, configure the webhook in GitHub to send a webhook to your ntfy topic, e.g. `https://ntfy.sh/mytopic?template=github`. +To override the pre-defined templates, you can place a file with the same name in the template directory (defaults to `/etc/ntfy/templates`, +can be overridden with `template-dir`). See [custom templates](#custom-templates) for more details. + +Here's an example of how to use the **pre-defined `github` template**: + +First, configure the webhook in GitHub to send a webhook to your ntfy topic, e.g. `https://ntfy.sh/mytopic?template=github`.

![GitHub webhook config](static/img/screenshot-github-webhook-config.png){ width=600 }
GitHub webhook configuration
diff --git a/server/errors.go b/server/errors.go index fa504410..c6745779 100644 --- a/server/errors.go +++ b/server/errors.go @@ -123,7 +123,6 @@ var ( errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} - errHTTPBadRequestTemplateDirectoryNotConfigured = &errHTTP{40046, http.StatusBadRequest, "invalid request: template directory not configured", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} diff --git a/server/server.go b/server/server.go index 7bad3fde..0d69e068 100644 --- a/server/server.go +++ b/server/server.go @@ -145,6 +145,7 @@ const ( unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part messagesHistoryMax = 10 // Number of message count values to keep in memory templateMaxExecutionTime = 100 * time.Millisecond // Maximum time a template can take to execute, used to prevent DoS attacks + templateMaxOutputBytes = 1024 * 1024 // Maximum number of bytes a template can output, used to prevent DoS attacks templateFileExtension = ".yml" // Template files must end with this extension ) @@ -1127,7 +1128,7 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM return err } } - if len(m.Message) > s.config.MessageSizeLimit { + if len(m.Title) > s.config.MessageSizeLimit || len(m.Message) > s.config.MessageSizeLimit { return errHTTPBadRequestTemplateMessageTooLarge } return nil @@ -1188,7 +1189,8 @@ func (s *Server) replaceTemplate(tpl string, source string) (string, error) { return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error()) } var buf bytes.Buffer - if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil { + limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes)) + if err := t.Execute(limitWriter, data); err != nil { return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error()) } return strings.TrimSpace(buf.String()), nil diff --git a/server/server_test.go b/server/server_test.go index ad2bb8fd..36bbae3f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3143,6 +3143,42 @@ message: | require.Equal(t, "Custom message 1391", m.Message) } +func TestServer_MessageTemplate_Repeat9999_TooLarge(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ repeat 9999 "mystring" }}`, + "X-Template": "1", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) + require.Contains(t, toHTTPError(t, response.Body.String()).Message, "message or title is too large after replacing template") +} + +func TestServer_MessageTemplate_Repeat10001_TooLarge(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ repeat 10001 "mystring" }}`, + "X-Template": "1", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code) + require.Contains(t, toHTTPError(t, response.Body.String()).Message, "repeat count 10001 exceeds limit of 10000") +} + +func TestServer_MessageTemplate_Until100_000(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ range $i, $e := until 100_000 }}{{end}}`, + "X-Template": "1", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code) + require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations") +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 7dcf7450..71c3e61b 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -132,7 +132,7 @@ func toRawJSON(v any) string { if err != nil { panic(err) } - return string(output) + return output } // mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters. diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 10ededd6..c9b9f86b 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -14,6 +14,11 @@ import ( "time" ) +const ( + loopExecutionLimit = 10_000 // Limit the number of loop executions to prevent execution from taking too long + stringLengthLimit = 100_000 // Limit the length of strings to prevent memory issues +) + // TxtFuncMap produces the function map. // // Use this to pass the functions into the template engine: @@ -58,7 +63,7 @@ var genericMap = map[string]any{ }, "substr": substring, // Switch order so that "foo" | repeat 5 - "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, + "repeat": repeat, "trimAll": func(a, b string) string { return strings.Trim(b, a) }, "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go index e41f61f5..901fe3f3 100644 --- a/util/sprig/numeric.go +++ b/util/sprig/numeric.go @@ -127,7 +127,15 @@ func until(count int) []int { } func untilStep(start, stop, step int) []int { - v := []int{} + var v []int + if step == 0 { + return v + } + + iterations := math.Abs(float64(stop)-float64(start)) / float64(step) + if iterations > loopExecutionLimit { + panic(fmt.Sprintf("too many iterations in untilStep; max allowed is %d, got %f", loopExecutionLimit, iterations)) + } if stop < start { if step >= 0 { diff --git a/util/sprig/strings.go b/util/sprig/strings.go index 911aa6f4..11459a4b 100644 --- a/util/sprig/strings.go +++ b/util/sprig/strings.go @@ -187,3 +187,12 @@ func substring(start, end int, s string) string { } return s[start:end] } + +func repeat(count int, str string) string { + if count > loopExecutionLimit { + panic(fmt.Sprintf("repeat count %d exceeds limit of %d", count, loopExecutionLimit)) + } else if count*len(str) >= stringLengthLimit { + panic(fmt.Sprintf("repeat count %d with string length %d exceeds limit of %d", count, len(str), stringLengthLimit)) + } + return strings.Repeat(str, count) +} diff --git a/util/timeout_writer.go b/util/timeout_writer.go index 370068c4..d531916d 100644 --- a/util/timeout_writer.go +++ b/util/timeout_writer.go @@ -7,7 +7,7 @@ import ( ) // ErrWriteTimeout is returned when a write timed out -var ErrWriteTimeout = errors.New("write operation failed due to timeout since creation") +var ErrWriteTimeout = errors.New("write operation failed due to timeout") // TimeoutWriter wraps an io.Writer that will time out after the given timeout type TimeoutWriter struct { @@ -28,7 +28,7 @@ func NewTimeoutWriter(w io.Writer, timeout time.Duration) *TimeoutWriter { // Write implements the io.Writer interface, failing if called after the timeout period from creation. func (tw *TimeoutWriter) Write(p []byte) (n int, err error) { if time.Since(tw.start) > tw.timeout { - return 0, errors.New("write operation failed due to timeout since creation") + return 0, ErrWriteTimeout } return tw.writer.Write(p) } From f0d5392e9e7c365d27217e9a6b4f8a6a4b09f8e0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 21:32:05 +0200 Subject: [PATCH 179/378] Self-review --- docs/releases.md | 3 ++- server/server.go | 25 +++++++++++++++---------- server/templates/alertmanager.yml | 2 -- server/templates/github.yml | 1 - server/templates/grafana.yml | 1 - 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index fe91f580..6171dcff 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1456,7 +1456,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* You can now use a subset of [Sprig](https://github.com/Masterminds/sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) +* Enhanced JSON webhook support via [pre-defined](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) ([#1390](https://github.com/binwiederhier/ntfy/pull/1390)) +* Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work) ### ntfy Android app v1.16.1 (UNRELEASED) diff --git a/server/server.go b/server/server.go index 0d69e068..f3d2ac51 100644 --- a/server/server.go +++ b/server/server.go @@ -57,7 +57,7 @@ type Server struct { userManager *user.Manager // Might be nil! messageCache *messageCache // Database that stores the messages webPush *webPushStore // Database that stores web push subscriptions - fileCache *fileCache // Name system based cache that stores attachments + fileCache *fileCache // File system based cache that stores attachments stripe stripeAPI // Stripe API, can be replaced with a mock priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!) metricsHandler http.Handler // Handles /metrics if enable-metrics set, and listen-metrics-http not set @@ -1120,11 +1120,11 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM } peekedBody := strings.TrimSpace(string(body.PeekedBytes)) if templateName := template.Name(); templateName != "" { - if err := s.replaceTemplateFromFile(m, templateName, peekedBody); err != nil { + if err := s.renderTemplateFromFile(m, templateName, peekedBody); err != nil { return err } } else { - if err := s.replaceTemplateFromParams(m, peekedBody); err != nil { + if err := s.renderTemplateFromParams(m, peekedBody); err != nil { return err } } @@ -1134,7 +1134,9 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM return nil } -func (s *Server) replaceTemplateFromFile(m *message, templateName, peekedBody string) error { +// renderTemplateFromFile transforms the JSON message body according to a template from the filesystem. +// The template file must be in the templates directory, or in the configured template directory. +func (s *Server) renderTemplateFromFile(m *message, templateName, peekedBody string) error { if !templateNameRegex.MatchString(templateName) { return errHTTPBadRequestTemplateFileNotFound } @@ -1153,30 +1155,33 @@ func (s *Server) replaceTemplateFromFile(m *message, templateName, peekedBody st } var err error if tpl.Message != nil { - if m.Message, err = s.replaceTemplate(*tpl.Message, peekedBody); err != nil { + if m.Message, err = s.renderTemplate(*tpl.Message, peekedBody); err != nil { return err } } if tpl.Title != nil { - if m.Title, err = s.replaceTemplate(*tpl.Title, peekedBody); err != nil { + if m.Title, err = s.renderTemplate(*tpl.Title, peekedBody); err != nil { return err } } return nil } -func (s *Server) replaceTemplateFromParams(m *message, peekedBody string) error { +// renderTemplateFromParams transforms the JSON message body according to the inline template in the +// message and title parameters. +func (s *Server) renderTemplateFromParams(m *message, peekedBody string) error { var err error - if m.Message, err = s.replaceTemplate(m.Message, peekedBody); err != nil { + if m.Message, err = s.renderTemplate(m.Message, peekedBody); err != nil { return err } - if m.Title, err = s.replaceTemplate(m.Title, peekedBody); err != nil { + if m.Title, err = s.renderTemplate(m.Title, peekedBody); err != nil { return err } return nil } -func (s *Server) replaceTemplate(tpl string, source string) (string, error) { +// renderTemplate renders a template with the given JSON source data. +func (s *Server) renderTemplate(tpl string, source string) (string, error) { if templateDisallowedRegex.MatchString(tpl) { return "", errHTTPBadRequestTemplateDisallowedFunctionCalls } diff --git a/server/templates/alertmanager.yml b/server/templates/alertmanager.yml index 803bbfcb..a63a756c 100644 --- a/server/templates/alertmanager.yml +++ b/server/templates/alertmanager.yml @@ -25,5 +25,3 @@ message: | Source: {{ .generatorURL }} {{ end }} - - diff --git a/server/templates/github.yml b/server/templates/github.yml index 2c2922a2..aee95b42 100644 --- a/server/templates/github.yml +++ b/server/templates/github.yml @@ -55,4 +55,3 @@ message: | {{- else }} {{ fail "Unsupported GitHub event type or action." }} {{- end }} - diff --git a/server/templates/grafana.yml b/server/templates/grafana.yml index 658aa550..bdb64e45 100644 --- a/server/templates/grafana.yml +++ b/server/templates/grafana.yml @@ -8,4 +8,3 @@ title: | {{- end }} message: | {{ .message | trunc 2000 }} - From 8b4834929d3566960e8041eb8ec0592b5b974968 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 22:30:07 +0200 Subject: [PATCH 180/378] Clean code --- util/sprig/flow_control.go | 7 + util/sprig/functions.go | 410 +++++++++++++-------------- util/sprig/functions_windows_test.go | 28 -- util/sprig/numeric.go | 77 +++-- util/sprig/strings.go | 34 +++ 5 files changed, 287 insertions(+), 269 deletions(-) create mode 100644 util/sprig/flow_control.go delete mode 100644 util/sprig/functions_windows_test.go diff --git a/util/sprig/flow_control.go b/util/sprig/flow_control.go new file mode 100644 index 00000000..2bdf382c --- /dev/null +++ b/util/sprig/flow_control.go @@ -0,0 +1,7 @@ +package sprig + +import "errors" + +func fail(msg string) (string, error) { + return "", errors.New(msg) +} diff --git a/util/sprig/functions.go b/util/sprig/functions.go index c9b9f86b..27d52524 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -1,14 +1,9 @@ package sprig import ( - "errors" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "math/rand" "path" "path/filepath" "reflect" - "strconv" "strings" "text/template" "time" @@ -27,220 +22,195 @@ const ( // // TxtFuncMap returns a 'text/template'.FuncMap func TxtFuncMap() template.FuncMap { - gfm := make(map[string]any, len(genericMap)) - for k, v := range genericMap { - gfm[k] = v + return map[string]any{ + // Date functions + "ago": dateAgo, + "date": date, + "date_in_zone": dateInZone, + "date_modify": dateModify, + "dateInZone": dateInZone, + "dateModify": dateModify, + "duration": duration, + "durationRound": durationRound, + "htmlDate": htmlDate, + "htmlDateInZone": htmlDateInZone, + "must_date_modify": mustDateModify, + "mustDateModify": mustDateModify, + "mustToDate": mustToDate, + "now": time.Now, + "toDate": toDate, + "unixEpoch": unixEpoch, + + // Strings + "trunc": trunc, + "trim": strings.TrimSpace, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": title, + "substr": substring, + "repeat": repeat, + "trimAll": trimAll, + "trimPrefix": trimPrefix, + "trimSuffix": trimSuffix, + "contains": contains, + "hasPrefix": hasPrefix, + "hasSuffix": hasSuffix, + "quote": quote, + "squote": squote, + "cat": cat, + "indent": indent, + "nindent": nindent, + "replace": replace, + "plural": plural, + "sha1sum": sha1sum, + "sha256sum": sha256sum, + "sha512sum": sha512sum, + "adler32sum": adler32sum, + "toString": strval, + + // Wrap Atoi to stop errors. + "atoi": atoi, + "seq": seq, + "toDecimal": toDecimal, + "split": split, + "splitList": splitList, + "splitn": splitn, + "toStrings": strslice, + + "until": until, + "untilStep": untilStep, + + // Basic arithmetic + "add1": add1, + "add": add, + "sub": sub, + "div": div, + "mod": mod, + "mul": mul, + "randInt": randInt, + "biggest": maxAsInt64, + "max": maxAsInt64, + "min": minAsInt64, + "maxf": maxAsFloat64, + "minf": minAsFloat64, + "ceil": ceil, + "floor": floor, + "round": round, + + // string slices. Note that we reverse the order b/c that's better + // for template processing. + "join": join, + "sortAlpha": sortAlpha, + + // Defaults + "default": dfault, + "empty": empty, + "coalesce": coalesce, + "all": all, + "any": anyNonEmpty, + "compact": compact, + "mustCompact": mustCompact, + "fromJSON": fromJSON, + "toJSON": toJSON, + "toPrettyJSON": toPrettyJSON, + "toRawJSON": toRawJSON, + "mustFromJSON": mustFromJSON, + "mustToJSON": mustToJSON, + "mustToPrettyJSON": mustToPrettyJSON, + "mustToRawJSON": mustToRawJSON, + "ternary": ternary, + + // Reflection + "typeOf": typeOf, + "typeIs": typeIs, + "typeIsLike": typeIsLike, + "kindOf": kindOf, + "kindIs": kindIs, + "deepEqual": reflect.DeepEqual, + + // Paths + "base": path.Base, + "dir": path.Dir, + "clean": path.Clean, + "ext": path.Ext, + "isAbs": path.IsAbs, + + // Filepaths + "osBase": filepath.Base, + "osClean": filepath.Clean, + "osDir": filepath.Dir, + "osExt": filepath.Ext, + "osIsAbs": filepath.IsAbs, + + // Encoding + "b64enc": base64encode, + "b64dec": base64decode, + "b32enc": base32encode, + "b32dec": base32decode, + + // Data Structures + "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. + "list": list, + "dict": dict, + "get": get, + "set": set, + "unset": unset, + "hasKey": hasKey, + "pluck": pluck, + "keys": keys, + "pick": pick, + "omit": omit, + "values": values, + + "append": push, + "push": push, + "mustAppend": mustPush, + "mustPush": mustPush, + "prepend": prepend, + "mustPrepend": mustPrepend, + "first": first, + "mustFirst": mustFirst, + "rest": rest, + "mustRest": mustRest, + "last": last, + "mustLast": mustLast, + "initial": initial, + "mustInitial": mustInitial, + "reverse": reverse, + "mustReverse": mustReverse, + "uniq": uniq, + "mustUniq": mustUniq, + "without": without, + "mustWithout": mustWithout, + "has": has, + "mustHas": mustHas, + "slice": slice, + "mustSlice": mustSlice, + "concat": concat, + "dig": dig, + "chunk": chunk, + "mustChunk": mustChunk, + + // Flow Control + "fail": fail, + + // Regex + "regexMatch": regexMatch, + "mustRegexMatch": mustRegexMatch, + "regexFindAll": regexFindAll, + "mustRegexFindAll": mustRegexFindAll, + "regexFind": regexFind, + "mustRegexFind": mustRegexFind, + "regexReplaceAll": regexReplaceAll, + "mustRegexReplaceAll": mustRegexReplaceAll, + "regexReplaceAllLiteral": regexReplaceAllLiteral, + "mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral, + "regexSplit": regexSplit, + "mustRegexSplit": mustRegexSplit, + "regexQuoteMeta": regexQuoteMeta, + + // URLs + "urlParse": urlParse, + "urlJoin": urlJoin, } - return gfm -} - -var genericMap = map[string]any{ - // Date functions - "ago": dateAgo, - "date": date, - "date_in_zone": dateInZone, - "date_modify": dateModify, - "dateInZone": dateInZone, - "dateModify": dateModify, - "duration": duration, - "durationRound": durationRound, - "htmlDate": htmlDate, - "htmlDateInZone": htmlDateInZone, - "must_date_modify": mustDateModify, - "mustDateModify": mustDateModify, - "mustToDate": mustToDate, - "now": time.Now, - "toDate": toDate, - "unixEpoch": unixEpoch, - - // Strings - "trunc": trunc, - "trim": strings.TrimSpace, - "upper": strings.ToUpper, - "lower": strings.ToLower, - "title": func(s string) string { - return cases.Title(language.English).String(s) - }, - "substr": substring, - // Switch order so that "foo" | repeat 5 - "repeat": repeat, - "trimAll": func(a, b string) string { return strings.Trim(b, a) }, - "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, - "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, - // Switch order so that "foobar" | contains "foo" - "contains": func(substr string, str string) bool { return strings.Contains(str, substr) }, - "hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) }, - "hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) }, - "quote": quote, - "squote": squote, - "cat": cat, - "indent": indent, - "nindent": nindent, - "replace": replace, - "plural": plural, - "sha1sum": sha1sum, - "sha256sum": sha256sum, - "sha512sum": sha512sum, - "adler32sum": adler32sum, - "toString": strval, - - // Wrap Atoi to stop errors. - "atoi": func(a string) int { i, _ := strconv.Atoi(a); return i }, - "seq": seq, - "toDecimal": toDecimal, - - // split "/" foo/bar returns map[int]string{0: foo, 1: bar} - "split": split, - "splitList": func(sep, orig string) []string { return strings.Split(orig, sep) }, - // splitn "/" foo/bar/fuu returns map[int]string{0: foo, 1: bar/fuu} - "splitn": splitn, - "toStrings": strslice, - - "until": until, - "untilStep": untilStep, - - // VERY basic arithmetic. - "add1": func(i any) int64 { return toInt64(i) + 1 }, - "add": func(i ...any) int64 { - var a int64 = 0 - for _, b := range i { - a += toInt64(b) - } - return a - }, - "sub": func(a, b any) int64 { return toInt64(a) - toInt64(b) }, - "div": func(a, b any) int64 { return toInt64(a) / toInt64(b) }, - "mod": func(a, b any) int64 { return toInt64(a) % toInt64(b) }, - "mul": func(a any, v ...any) int64 { - val := toInt64(a) - for _, b := range v { - val = val * toInt64(b) - } - return val - }, - "randInt": func(min, max int) int { return rand.Intn(max-min) + min }, - "biggest": max, - "max": max, - "min": min, - "maxf": maxf, - "minf": minf, - "ceil": ceil, - "floor": floor, - "round": round, - - // string slices. Note that we reverse the order b/c that's better - // for template processing. - "join": join, - "sortAlpha": sortAlpha, - - // Defaults - "default": dfault, - "empty": empty, - "coalesce": coalesce, - "all": all, - "any": anyNonEmpty, - "compact": compact, - "mustCompact": mustCompact, - "fromJSON": fromJSON, - "toJSON": toJSON, - "toPrettyJSON": toPrettyJSON, - "toRawJSON": toRawJSON, - "mustFromJSON": mustFromJSON, - "mustToJSON": mustToJSON, - "mustToPrettyJSON": mustToPrettyJSON, - "mustToRawJSON": mustToRawJSON, - "ternary": ternary, - - // Reflection - "typeOf": typeOf, - "typeIs": typeIs, - "typeIsLike": typeIsLike, - "kindOf": kindOf, - "kindIs": kindIs, - "deepEqual": reflect.DeepEqual, - - // Paths: - "base": path.Base, - "dir": path.Dir, - "clean": path.Clean, - "ext": path.Ext, - "isAbs": path.IsAbs, - - // Filepaths: - "osBase": filepath.Base, - "osClean": filepath.Clean, - "osDir": filepath.Dir, - "osExt": filepath.Ext, - "osIsAbs": filepath.IsAbs, - - // Encoding: - "b64enc": base64encode, - "b64dec": base64decode, - "b32enc": base32encode, - "b32dec": base32decode, - - // Data Structures: - "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. - "list": list, - "dict": dict, - "get": get, - "set": set, - "unset": unset, - "hasKey": hasKey, - "pluck": pluck, - "keys": keys, - "pick": pick, - "omit": omit, - "values": values, - - "append": push, - "push": push, - "mustAppend": mustPush, - "mustPush": mustPush, - "prepend": prepend, - "mustPrepend": mustPrepend, - "first": first, - "mustFirst": mustFirst, - "rest": rest, - "mustRest": mustRest, - "last": last, - "mustLast": mustLast, - "initial": initial, - "mustInitial": mustInitial, - "reverse": reverse, - "mustReverse": mustReverse, - "uniq": uniq, - "mustUniq": mustUniq, - "without": without, - "mustWithout": mustWithout, - "has": has, - "mustHas": mustHas, - "slice": slice, - "mustSlice": mustSlice, - "concat": concat, - "dig": dig, - "chunk": chunk, - "mustChunk": mustChunk, - - // Flow Control: - "fail": func(msg string) (string, error) { return "", errors.New(msg) }, - - // Regex - "regexMatch": regexMatch, - "mustRegexMatch": mustRegexMatch, - "regexFindAll": regexFindAll, - "mustRegexFindAll": mustRegexFindAll, - "regexFind": regexFind, - "mustRegexFind": mustRegexFind, - "regexReplaceAll": regexReplaceAll, - "mustRegexReplaceAll": mustRegexReplaceAll, - "regexReplaceAllLiteral": regexReplaceAllLiteral, - "mustRegexReplaceAllLiteral": mustRegexReplaceAllLiteral, - "regexSplit": regexSplit, - "mustRegexSplit": mustRegexSplit, - "regexQuoteMeta": regexQuoteMeta, - - // URLs: - "urlParse": urlParse, - "urlJoin": urlJoin, } diff --git a/util/sprig/functions_windows_test.go b/util/sprig/functions_windows_test.go deleted file mode 100644 index 9d8bd0e5..00000000 --- a/util/sprig/functions_windows_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package sprig - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOsBase(t *testing.T) { - assert.NoError(t, runt(`{{ osBase "C:\\foo\\bar" }}`, "bar")) -} - -func TestOsDir(t *testing.T) { - assert.NoError(t, runt(`{{ osDir "C:\\foo\\bar\\baz" }}`, "C:\\foo\\bar")) -} - -func TestOsIsAbs(t *testing.T) { - assert.NoError(t, runt(`{{ osIsAbs "C:\\foo" }}`, "true")) - assert.NoError(t, runt(`{{ osIsAbs "foo" }}`, "false")) -} - -func TestOsClean(t *testing.T) { - assert.NoError(t, runt(`{{ osClean "C:\\foo\\..\\foo\\..\\bar" }}`, "C:\\bar")) -} - -func TestOsExt(t *testing.T) { - assert.NoError(t, runt(`{{ osExt "C:\\foo\\bar\\baz.txt" }}`, ".txt")) -} diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go index 901fe3f3..32466818 100644 --- a/util/sprig/numeric.go +++ b/util/sprig/numeric.go @@ -3,6 +3,7 @@ package sprig import ( "fmt" "math" + "math/rand" "reflect" "strconv" "strings" @@ -78,7 +79,43 @@ func toInt64(v any) int64 { } } -func max(a any, i ...any) int64 { +func add1(i any) int64 { + return toInt64(i) + 1 +} + +func add(i ...any) int64 { + var a int64 + for _, b := range i { + a += toInt64(b) + } + return a +} + +func sub(a, b any) int64 { + return toInt64(a) - toInt64(b) +} + +func div(a, b any) int64 { + return toInt64(a) / toInt64(b) +} + +func mod(a, b any) int64 { + return toInt64(a) % toInt64(b) +} + +func mul(a any, v ...any) int64 { + val := toInt64(a) + for _, b := range v { + val = val * toInt64(b) + } + return val +} + +func randInt(min, max int) int { + return rand.Intn(max-min) + min +} + +func maxAsInt64(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { bb := toInt64(b) @@ -89,16 +126,15 @@ func max(a any, i ...any) int64 { return aa } -func maxf(a any, i ...any) float64 { - aa := toFloat64(a) +func maxAsFloat64(a any, i ...any) float64 { + m := toFloat64(a) for _, b := range i { - bb := toFloat64(b) - aa = math.Max(aa, bb) + m = math.Max(m, toFloat64(b)) } - return aa + return m } -func min(a any, i ...any) int64 { +func minAsInt64(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { bb := toInt64(b) @@ -109,13 +145,12 @@ func min(a any, i ...any) int64 { return aa } -func minf(a any, i ...any) float64 { - aa := toFloat64(a) +func minAsFloat64(a any, i ...any) float64 { + m := toFloat64(a) for _, b := range i { - bb := toFloat64(b) - aa = math.Min(aa, bb) + m = math.Min(m, toFloat64(b)) } - return aa + return m } func until(count int) []int { @@ -131,12 +166,10 @@ func untilStep(start, stop, step int) []int { if step == 0 { return v } - iterations := math.Abs(float64(stop)-float64(start)) / float64(step) if iterations > loopExecutionLimit { panic(fmt.Sprintf("too many iterations in untilStep; max allowed is %d, got %f", loopExecutionLimit, iterations)) } - if stop < start { if step >= 0 { return v @@ -146,7 +179,6 @@ func untilStep(start, stop, step int) []int { } return v } - if step <= 0 { return v } @@ -157,13 +189,11 @@ func untilStep(start, stop, step int) []int { } func floor(a any) float64 { - aa := toFloat64(a) - return math.Floor(aa) + return math.Floor(toFloat64(a)) } func ceil(a any) float64 { - aa := toFloat64(a) - return math.Ceil(aa) + return math.Ceil(toFloat64(a)) } func round(a any, p int, rOpt ...float64) float64 { @@ -195,6 +225,11 @@ func toDecimal(v any) int64 { return result } +func atoi(a string) int { + i, _ := strconv.Atoi(a) + return i +} + func seq(params ...int) string { increment := 1 switch len(params) { @@ -231,6 +266,6 @@ func seq(params ...int) string { } } -func intArrayToString(slice []int, delimeter string) string { - return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") +func intArrayToString(slice []int, delimiter string) string { + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimiter), "[]") } diff --git a/util/sprig/strings.go b/util/sprig/strings.go index 11459a4b..8a1bdc1b 100644 --- a/util/sprig/strings.go +++ b/util/sprig/strings.go @@ -4,6 +4,8 @@ import ( "encoding/base32" "encoding/base64" "fmt" + "golang.org/x/text/cases" + "golang.org/x/text/language" "reflect" "strconv" "strings" @@ -149,6 +151,10 @@ func trunc(c int, s string) string { return s } +func title(s string) string { + return cases.Title(language.English).String(s) +} + func join(sep string, v any) string { return strings.Join(strslice(v), sep) } @@ -162,6 +168,10 @@ func split(sep, orig string) map[string]string { return res } +func splitList(sep, orig string) []string { + return strings.Split(orig, sep) +} + func splitn(sep string, n int, orig string) map[string]string { parts := strings.SplitN(orig, sep, n) res := make(map[string]string, len(parts)) @@ -196,3 +206,27 @@ func repeat(count int, str string) string { } return strings.Repeat(str, count) } + +func trimAll(a, b string) string { + return strings.Trim(b, a) +} + +func trimPrefix(a, b string) string { + return strings.TrimPrefix(b, a) +} + +func trimSuffix(a, b string) string { + return strings.TrimSuffix(b, a) +} + +func contains(substr string, str string) bool { + return strings.Contains(str, substr) +} + +func hasPrefix(substr string, str string) bool { + return strings.HasPrefix(str, substr) +} + +func hasSuffix(substr string, str string) bool { + return strings.HasSuffix(str, substr) +} From 892e82ceb8363076a69b9809445ab4c9b712801a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 22:41:53 +0200 Subject: [PATCH 181/378] Remove underscore functions --- util/sprig/date_test.go | 4 ++-- util/sprig/functions.go | 29 +++++++++++++---------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/util/sprig/date_test.go b/util/sprig/date_test.go index 3ebfa2be..496822cf 100644 --- a/util/sprig/date_test.go +++ b/util/sprig/date_test.go @@ -52,7 +52,7 @@ func TestDateInZone(t *testing.T) { if err != nil { t.Error(err) } - tpl := `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "UTC" }}` + tpl := `{{ dateInZone "02 Jan 06 15:04 -0700" .Time "UTC" }}` // Test time.Time input if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": tm}); err != nil { @@ -86,7 +86,7 @@ func TestDateInZone(t *testing.T) { } // Test case of invalid timezone - tpl = `{{ date_in_zone "02 Jan 06 15:04 -0700" .Time "foobar" }}` + tpl = `{{ dateInZone "02 Jan 06 15:04 -0700" .Time "foobar" }}` if err = runtv(tpl, "13 Jun 19 20:39 +0000", map[string]any{"Time": tm}); err != nil { t.Error(err) } diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 27d52524..f7aabfc5 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -24,22 +24,19 @@ const ( func TxtFuncMap() template.FuncMap { return map[string]any{ // Date functions - "ago": dateAgo, - "date": date, - "date_in_zone": dateInZone, - "date_modify": dateModify, - "dateInZone": dateInZone, - "dateModify": dateModify, - "duration": duration, - "durationRound": durationRound, - "htmlDate": htmlDate, - "htmlDateInZone": htmlDateInZone, - "must_date_modify": mustDateModify, - "mustDateModify": mustDateModify, - "mustToDate": mustToDate, - "now": time.Now, - "toDate": toDate, - "unixEpoch": unixEpoch, + "ago": dateAgo, + "date": date, + "dateInZone": dateInZone, + "dateModify": dateModify, + "duration": duration, + "durationRound": durationRound, + "htmlDate": htmlDate, + "htmlDateInZone": htmlDateInZone, + "mustDateModify": mustDateModify, + "mustToDate": mustToDate, + "now": time.Now, + "toDate": toDate, + "unixEpoch": unixEpoch, // Strings "trunc": trunc, From 8783c86cd6efb2b4d4c8789013ea09fad209d439 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 22:45:41 +0200 Subject: [PATCH 182/378] Fix docs --- docs/publish/template-functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/publish/template-functions.md b/docs/publish/template-functions.md index 4c6b6dfe..f20ae1fd 100644 --- a/docs/publish/template-functions.md +++ b/docs/publish/template-functions.md @@ -617,7 +617,7 @@ The `dateModify` takes a modification and a date and returns the timestamp. Subtract an hour and thirty minutes from the current time: ``` -now | date_modify "-1.5h" +now | dateModify "-1.5h" ``` If the modification format is wrong `dateModify` will return the date unmodified. `mustDateModify` will return an error otherwise. From 1f34c39eb04c5e60c30a88c44705cc41c37d2f2f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 19 Jul 2025 22:52:08 +0200 Subject: [PATCH 183/378] Refactor a little --- util/sprig/date.go | 14 +++----------- util/sprig/defaults.go | 9 +++------ util/sprig/dict.go | 4 +--- util/sprig/functions.go | 2 +- util/sprig/list.go | 1 - 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/util/sprig/date.go b/util/sprig/date.go index 3fed04e9..f01dcf0b 100644 --- a/util/sprig/date.go +++ b/util/sprig/date.go @@ -38,12 +38,10 @@ func dateInZone(fmt string, date any, zone string) string { case int32: t = time.Unix(int64(date), 0) } - loc, err := time.LoadLocation(zone) if err != nil { loc, _ = time.LoadLocation("UTC") } - return t.In(loc).Format(fmt) } @@ -65,7 +63,6 @@ func mustDateModify(fmt string, date time.Time) (time.Time, error) { func dateAgo(date any) string { var t time.Time - switch date := date.(type) { default: t = time.Now() @@ -76,9 +73,7 @@ func dateAgo(date any) string { case int: t = time.Unix(int64(date), 0) } - // Drop resolution to seconds - duration := time.Since(t).Round(time.Second) - return duration.String() + return time.Since(t).Round(time.Second).String() } func duration(sec any) string { @@ -106,13 +101,10 @@ func durationRound(duration any) string { case time.Time: d = time.Since(duration) } - - u := uint64(d) - neg := d < 0 - if neg { + var u uint64 + if d < 0 { u = -u } - var ( year = uint64(time.Hour) * 24 * 365 month = uint64(time.Hour) * 24 * 30 diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 71c3e61b..948747b9 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -7,7 +7,7 @@ import ( "strings" ) -// dfault checks whether `given` is set, and returns default if not set. +// defaultValue checks whether `given` is set, and returns default if not set. // // This returns `d` if `given` appears not to be set, and `given` otherwise. // @@ -17,8 +17,7 @@ import ( // Structs are never considered unset. // // For everything else, including pointers, a nil value is unset. -func dfault(d any, given ...any) any { - +func defaultValue(d any, given ...any) any { if empty(given) || empty(given[0]) { return d } @@ -31,7 +30,6 @@ func empty(given any) bool { if !g.IsValid() { return true } - // Basically adapted from text/template.isTrue switch g.Kind() { default: @@ -140,8 +138,7 @@ func mustToRawJSON(v any) (string, error) { buf := new(bytes.Buffer) enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) - err := enc.Encode(&v) - if err != nil { + if err := enc.Encode(&v); err != nil { return "", err } return strings.TrimSuffix(buf.String(), "\n"), nil diff --git a/util/sprig/dict.go b/util/sprig/dict.go index 97182a97..6485763e 100644 --- a/util/sprig/dict.go +++ b/util/sprig/dict.go @@ -33,7 +33,7 @@ func pluck(key string, d ...map[string]any) []any { } func keys(dicts ...map[string]any) []string { - k := []string{} + var k []string for _, dict := range dicts { for key := range dict { k = append(k, key) @@ -54,12 +54,10 @@ func pick(dict map[string]any, keys ...string) map[string]any { func omit(dict map[string]any, keys ...string) map[string]any { res := map[string]any{} - omit := make(map[string]bool, len(keys)) for _, k := range keys { omit[k] = true } - for k, v := range dict { if _, ok := omit[k]; !ok { res[k] = v diff --git a/util/sprig/functions.go b/util/sprig/functions.go index f7aabfc5..f0232a5b 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -100,7 +100,7 @@ func TxtFuncMap() template.FuncMap { "sortAlpha": sortAlpha, // Defaults - "default": dfault, + "default": defaultValue, "empty": empty, "coalesce": coalesce, "all": all, diff --git a/util/sprig/list.go b/util/sprig/list.go index 138ecfa5..d8882af0 100644 --- a/util/sprig/list.go +++ b/util/sprig/list.go @@ -20,7 +20,6 @@ func push(list any, v any) []any { if err != nil { panic(err) } - return l } From f4a74dac57d0a55e1ae28660cfac3997c20500db Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sat, 19 Jul 2025 21:51:00 -0600 Subject: [PATCH 184/378] doc corrections --- docs/publish.md | 5 +++-- docs/publish/template-functions.md | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index dc124dbc..2d347f22 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -954,8 +954,8 @@ is valid JSON). You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`, or the query parameter `?template=...`): -* **Pre-defined template files**: Setting the `X-Template` header or query parameter to a template name (e.g. `?template=github`) - to a pre-defined template name (e.g. `github`, `grafana`, or `alertmanager`) will use the template with that name. +* **Pre-defined template files**: Setting the `X-Template` header or query parameter to a pre-defined template name (one of `github`, + `grafana`, or `alertmanager`, such as `?template=github`) will use the built-in template with that name. See [pre-defined templates](#pre-defined-templates) for more details. * **Custom template files**: Setting the `X-Template` header or query parameter to a custom template name (e.g. `?template=myapp`) will use a custom template file from the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`). @@ -1244,6 +1244,7 @@ Below are the functions that are available to use inside your message/title temp * [String List Functions](publish/template-functions.md#string-list-functions): `splitList`, `sortAlpha`, etc. * [Integer Math Functions](publish/template-functions.md#integer-math-functions): `add`, `max`, `mul`, etc. * [Integer List Functions](publish/template-functions.md#integer-list-functions): `until`, `untilStep` +* [Float Math Functions](publish/template-functions.md#float-math-functions): `maxf`, `minf` * [Date Functions](publish/template-functions.md#date-functions): `now`, `date`, etc. * [Defaults Functions](publish/template-functions.md#default-functions): `default`, `empty`, `coalesce`, `fromJSON`, `toJSON`, `toPrettyJSON`, `toRawJSON`, `ternary` * [Encoding Functions](publish/template-functions.md#encoding-functions): `b64enc`, `b64dec`, etc. diff --git a/docs/publish/template-functions.md b/docs/publish/template-functions.md index f20ae1fd..a08e4717 100644 --- a/docs/publish/template-functions.md +++ b/docs/publish/template-functions.md @@ -10,6 +10,7 @@ The original set of template functions is based on the [Sprig library](https://m - [String List Functions](#string-list-functions) - [Integer Math Functions](#integer-math-functions) - [Integer List Functions](#integer-list-functions) +- [Float Math Functions](#float-math-functions) - [Date Functions](#date-functions) - [Default Functions](#default-functions) - [Encoding Functions](#encoding-functions) @@ -526,6 +527,28 @@ seq 0 2 10 => 0 2 4 6 8 10 seq 0 -2 -5 => 0 -2 -4 ``` +## Float Math Functions + +### maxf + +Return the largest of a series of floats: + +This will return `3`: + +``` +maxf 1 2.5 3 +``` + +### minf + +Return the smallest of a series of floats. + +This will return `1.5`: + +``` +minf 1.5 2 3 +``` + ## Date Functions ### now From 006f73af7d6af3c077eab926e6ef2e0e542e4803 Mon Sep 17 00:00:00 2001 From: timof <54764164+timofej673@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:02:06 +0400 Subject: [PATCH 185/378] Update message_cache.go Added lock in add_messages to avoid "database is locked" error Small code reformatting --- server/message_cache.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index e314ace3..a73650ad 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -8,6 +8,7 @@ import ( "net/netip" "strings" "time" + "sync" _ "github.com/mattn/go-sqlite3" // SQLite driver "heckel.io/ntfy/v2/log" @@ -35,7 +36,7 @@ const ( priority INT NOT NULL, tags TEXT NOT NULL, click TEXT NOT NULL, - icon TEXT NOT NULL, + icon TEXT NOT NULL, actions TEXT NOT NULL, attachment_name TEXT NOT NULL, attachment_type TEXT NOT NULL, @@ -72,30 +73,30 @@ const ( selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding - FROM messages + FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding - FROM messages + FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding - FROM messages + FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding - FROM messages + FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding - FROM messages + FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` @@ -105,10 +106,10 @@ const ( WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 - ` + ` selectMessagesDueQuery = ` SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding - FROM messages + FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id ` @@ -281,6 +282,7 @@ var ( type messageCache struct { db *sql.DB queue *util.BatchingQueue[*message] + mu sync.Mutex nop bool } @@ -340,6 +342,8 @@ func (c *messageCache) AddMessage(m *message) error { // addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until // SQLite's busy_timeout is exceeded before erroring out. func (c *messageCache) addMessages(ms []*message) error { + c.mu.Lock() + defer c.mu.Unlock() if c.nop { return nil } From 4d1baae6d0210bd6a5b60d8d0e7481a198c4a387 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 21 Jul 2025 10:28:26 +0200 Subject: [PATCH 186/378] Refine --- docs/publish.md | 4 ++-- docs/publish/template-functions.md | 2 +- mkdocs.yml | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index dc124dbc..4e95932f 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1235,8 +1235,8 @@ A good way to experiment with Go templates is the **[Go Template Playground](htt your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). ### Template functions -ntfy supports a subset of the [Sprig](https://github.com/Masterminds/sprig) template functions. This is useful for advanced -message templating and for transforming the data provided through the JSON payload. +ntfy supports a subset of the **[Sprig template functions](publish/template-functions.md)** (originally copied from [Sprig](https://github.com/Masterminds/sprig), +thank you to the Sprig developers 🙏). This is useful for advanced message templating and for transforming the data provided through the JSON payload. Below are the functions that are available to use inside your message/title templates. diff --git a/docs/publish/template-functions.md b/docs/publish/template-functions.md index f20ae1fd..68892a5f 100644 --- a/docs/publish/template-functions.md +++ b/docs/publish/template-functions.md @@ -1,6 +1,6 @@ # Template Functions -These template functions may be used in the [message template](../publish.md#message-templating) feature of ntfy. Please refer to the examples in the documentation for how to use them. +These template functions may be used in the **[message template](../publish.md#message-templating)** feature of ntfy. Please refer to the examples in the documentation for how to use them. The original set of template functions is based on the [Sprig library](https://masterminds.github.io/sprig/). This documentation page is a (slightly modified) copy of their docs. **Thank you to the Sprig developers for their work!** 🙏 diff --git a/mkdocs.yml b/mkdocs.yml index ef746518..adaf166b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,7 @@ nav: - "Integrations + projects": integrations.md - "Release notes": releases.md - "Emojis 🥳 🎉": emojis.md + - "Template functions": publish/template-functions.md - "Troubleshooting": troubleshooting.md - "Known issues": known-issues.md - "Deprecation notices": deprecations.md From 50c564d8a2c8a644cb418a522122935faa95506f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 21 Jul 2025 11:24:58 +0200 Subject: [PATCH 187/378] AI docs --- util/sprig/crypto.go | 17 +++ util/sprig/date.go | 112 +++++++++++++-- util/sprig/date_test.go | 3 + util/sprig/defaults.go | 139 +++++++++++++++++-- util/sprig/dict.go | 118 ++++++++++++++++ util/sprig/flow_control.go | 1 + util/sprig/functions.go | 1 + util/sprig/functions_test.go | 2 +- util/sprig/list.go | 138 +++++++++++------- util/sprig/list_test.go | 3 + util/sprig/numeric.go | 238 +++++++++++++++++++++++++++++++- util/sprig/reflect.go | 42 ++++++ util/sprig/regex.go | 134 ++++++++++++++++++ util/sprig/strings.go | 261 ++++++++++++++++++++++++++++++++++- util/sprig/url.go | 1 - 15 files changed, 1132 insertions(+), 78 deletions(-) diff --git a/util/sprig/crypto.go b/util/sprig/crypto.go index db8a6814..da4bfc94 100644 --- a/util/sprig/crypto.go +++ b/util/sprig/crypto.go @@ -9,21 +9,38 @@ import ( "hash/adler32" ) +// sha512sum computes the SHA-512 hash of the input string and returns it as a hex-encoded string. +// This function can be used in templates to generate secure hashes of sensitive data. +// +// Example usage in templates: {{ "hello world" | sha512sum }} func sha512sum(input string) string { hash := sha512.Sum512([]byte(input)) return hex.EncodeToString(hash[:]) } +// sha256sum computes the SHA-256 hash of the input string and returns it as a hex-encoded string. +// This is a commonly used cryptographic hash function that produces a 256-bit (32-byte) hash value. +// +// Example usage in templates: {{ "hello world" | sha256sum }} func sha256sum(input string) string { hash := sha256.Sum256([]byte(input)) return hex.EncodeToString(hash[:]) } +// sha1sum computes the SHA-1 hash of the input string and returns it as a hex-encoded string. +// Note: SHA-1 is no longer considered secure against well-funded attackers for cryptographic purposes. +// Consider using sha256sum or sha512sum for security-critical applications. +// +// Example usage in templates: {{ "hello world" | sha1sum }} func sha1sum(input string) string { hash := sha1.Sum([]byte(input)) return hex.EncodeToString(hash[:]) } +// adler32sum computes the Adler-32 checksum of the input string and returns it as a decimal string. +// This is a non-cryptographic hash function primarily used for error detection. +// +// Example usage in templates: {{ "hello world" | adler32sum }} func adler32sum(input string) string { hash := adler32.Checksum([]byte(input)) return fmt.Sprintf("%d", hash) diff --git a/util/sprig/date.go b/util/sprig/date.go index f01dcf0b..3231e619 100644 --- a/util/sprig/date.go +++ b/util/sprig/date.go @@ -1,27 +1,61 @@ package sprig import ( + "math" "strconv" "time" ) -// Given a format and a date, format the date string. +// date formats a date according to the provided format string. // -// Date can be a `time.Time` or an `int, int32, int64`. -// In the later case, it is treated as seconds since UNIX -// epoch. +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02 15:04:05") +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// +// If date is not one of the recognized types, the current time is used. +// +// Example usage in templates: {{ now | date "2006-01-02" }} func date(fmt string, date any) string { return dateInZone(fmt, date, "Local") } +// htmlDate formats a date in HTML5 date format (YYYY-MM-DD). +// +// Parameters: +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// +// If date is not one of the recognized types, the current time is used. +// +// Example usage in templates: {{ now | htmlDate }} func htmlDate(date any) string { return dateInZone("2006-01-02", date, "Local") } +// htmlDateInZone formats a date in HTML5 date format (YYYY-MM-DD) in the specified timezone. +// +// Parameters: +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// - zone: Timezone name (e.g., "UTC", "America/New_York") +// +// If date is not one of the recognized types, the current time is used. +// If the timezone is invalid, UTC is used. +// +// Example usage in templates: {{ now | htmlDateInZone "UTC" }} func htmlDateInZone(date any, zone string) string { return dateInZone("2006-01-02", date, zone) } +// dateInZone formats a date according to the provided format string in the specified timezone. +// +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02 15:04:05") +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// - zone: Timezone name (e.g., "UTC", "America/New_York") +// +// If date is not one of the recognized types, the current time is used. +// If the timezone is invalid, UTC is used. +// +// Example usage in templates: {{ now | dateInZone "2006-01-02 15:04:05" "UTC" }} func dateInZone(fmt string, date any, zone string) string { var t time.Time switch date := date.(type) { @@ -45,6 +79,15 @@ func dateInZone(fmt string, date any, zone string) string { return t.In(loc).Format(fmt) } +// dateModify modifies a date by adding a duration and returns the resulting time. +// +// Parameters: +// - fmt: A duration string (e.g., "24h", "-12h30m", "1h15m30s") +// - date: The time.Time to modify +// +// If the duration string is invalid, the original date is returned. +// +// Example usage in templates: {{ now | dateModify "-24h" }} func dateModify(fmt string, date time.Time) time.Time { d, err := time.ParseDuration(fmt) if err != nil { @@ -53,6 +96,15 @@ func dateModify(fmt string, date time.Time) time.Time { return date.Add(d) } +// mustDateModify modifies a date by adding a duration and returns the resulting time or an error. +// +// Parameters: +// - fmt: A duration string (e.g., "24h", "-12h30m", "1h15m30s") +// - date: The time.Time to modify +// +// Unlike dateModify, this function returns an error if the duration string is invalid. +// +// Example usage in templates: {{ now | mustDateModify "24h" }} func mustDateModify(fmt string, date time.Time) (time.Time, error) { d, err := time.ParseDuration(fmt) if err != nil { @@ -61,6 +113,14 @@ func mustDateModify(fmt string, date time.Time) (time.Time, error) { return date.Add(d), nil } +// dateAgo returns a string representing the time elapsed since the given date. +// +// Parameters: +// - date: Can be a time.Time, int, or int64 (seconds since UNIX epoch) +// +// If date is not one of the recognized types, the current time is used. +// +// Example usage in templates: {{ "2023-01-01" | toDate "2006-01-02" | dateAgo }} func dateAgo(date any) string { var t time.Time switch date := date.(type) { @@ -76,6 +136,12 @@ func dateAgo(date any) string { return time.Since(t).Round(time.Second).String() } +// duration converts seconds to a duration string. +// +// Parameters: +// - sec: Can be a string (parsed as int64), or int64 representing seconds +// +// Example usage in templates: {{ 3600 | duration }} -> "1h0m0s" func duration(sec any) string { var n int64 switch value := sec.(type) { @@ -89,6 +155,15 @@ func duration(sec any) string { return (time.Duration(n) * time.Second).String() } +// durationRound formats a duration in a human-readable rounded format. +// +// Parameters: +// - duration: Can be a string (parsed as duration), int64 (nanoseconds), +// or time.Time (time since that moment) +// +// Returns a string with the largest appropriate unit (y, mo, d, h, m, s). +// +// Example usage in templates: {{ 3600 | duration | durationRound }} -> "1h" func durationRound(duration any) string { var d time.Duration switch duration := duration.(type) { @@ -101,10 +176,7 @@ func durationRound(duration any) string { case time.Time: d = time.Since(duration) } - var u uint64 - if d < 0 { - u = -u - } + u := uint64(math.Abs(float64(d))) var ( year = uint64(time.Hour) * 24 * 365 month = uint64(time.Hour) * 24 * 30 @@ -130,15 +202,39 @@ func durationRound(duration any) string { return "0s" } +// toDate parses a string into a time.Time using the specified format. +// +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02") +// - str: The date string to parse +// +// If parsing fails, returns a zero time.Time. +// +// Example usage in templates: {{ "2023-01-01" | toDate "2006-01-02" }} func toDate(fmt, str string) time.Time { t, _ := time.ParseInLocation(fmt, str, time.Local) return t } +// mustToDate parses a string into a time.Time using the specified format or returns an error. +// +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02") +// - str: The date string to parse +// +// Unlike toDate, this function returns an error if parsing fails. +// +// Example usage in templates: {{ mustToDate "2006-01-02" "2023-01-01" }} func mustToDate(fmt, str string) (time.Time, error) { return time.ParseInLocation(fmt, str, time.Local) } +// unixEpoch returns the Unix timestamp (seconds since January 1, 1970 UTC) for the given time. +// +// Parameters: +// - date: A time.Time value +// +// Example usage in templates: {{ now | unixEpoch }} func unixEpoch(date time.Time) string { return strconv.FormatInt(date.Unix(), 10) } diff --git a/util/sprig/date_test.go b/util/sprig/date_test.go index 496822cf..ee9a9cc6 100644 --- a/util/sprig/date_test.go +++ b/util/sprig/date_test.go @@ -117,4 +117,7 @@ func TestDurationRound(t *testing.T) { if err := runtv(tpl, "3mo", map[string]any{"Time": "2400h5s"}); err != nil { t.Error(err) } + if err := runtv(tpl, "1m", map[string]any{"Time": "-1m1s"}); err != nil { + t.Error(err) + } } diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 948747b9..c5c14308 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -25,6 +25,21 @@ func defaultValue(d any, given ...any) any { } // empty returns true if the given value has the zero value for its type. +// This is a helper function used by defaultValue, coalesce, all, and anyNonEmpty. +// +// The following values are considered empty: +// - Invalid values +// - nil values +// - Zero-length arrays, slices, maps, and strings +// - Boolean false +// - Zero for all numeric types +// - Structs are never considered empty +// +// Parameters: +// - given: The value to check for emptiness +// +// Returns: +// - bool: True if the value is considered empty, false otherwise func empty(given any) bool { g := reflect.ValueOf(given) if !g.IsValid() { @@ -51,7 +66,16 @@ func empty(given any) bool { } } -// coalesce returns the first non-empty value. +// coalesce returns the first non-empty value from a list of values. +// If all values are empty, it returns nil. +// +// This is useful for providing a series of fallback values. +// +// Parameters: +// - v: A variadic list of values to check +// +// Returns: +// - any: The first non-empty value, or nil if all values are empty func coalesce(v ...any) any { for _, val := range v { if !empty(val) { @@ -61,8 +85,15 @@ func coalesce(v ...any) any { return nil } -// all returns true if empty(x) is false for all values x in the list. -// If the list is empty, return true. +// all checks if all values in a list are non-empty. +// Returns true if every value in the list is non-empty. +// If the list is empty, returns true (vacuously true). +// +// Parameters: +// - v: A variadic list of values to check +// +// Returns: +// - bool: True if all values are non-empty, false otherwise func all(v ...any) bool { for _, val := range v { if empty(val) { @@ -72,8 +103,15 @@ func all(v ...any) bool { return true } -// anyNonEmpty returns true if empty(x) is false for anyNonEmpty x in the list. -// If the list is empty, return false. +// anyNonEmpty checks if at least one value in a list is non-empty. +// Returns true if any value in the list is non-empty. +// If the list is empty, returns false. +// +// Parameters: +// - v: A variadic list of values to check +// +// Returns: +// - bool: True if at least one value is non-empty, false otherwise func anyNonEmpty(v ...any) bool { for _, val := range v { if !empty(val) { @@ -83,25 +121,58 @@ func anyNonEmpty(v ...any) bool { return false } -// fromJSON decodes JSON into a structured value, ignoring errors. +// fromJSON decodes a JSON string into a structured value. +// This function ignores any errors that occur during decoding. +// If the JSON is invalid, it returns nil. +// +// Parameters: +// - v: The JSON string to decode +// +// Returns: +// - any: The decoded value, or nil if decoding failed func fromJSON(v string) any { output, _ := mustFromJSON(v) return output } -// mustFromJSON decodes JSON into a structured value, returning errors. +// mustFromJSON decodes a JSON string into a structured value. +// Unlike fromJSON, this function returns any errors that occur during decoding. +// +// Parameters: +// - v: The JSON string to decode +// +// Returns: +// - any: The decoded value +// - error: Any error that occurred during decoding func mustFromJSON(v string) (any, error) { var output any err := json.Unmarshal([]byte(v), &output) return output, err } -// toJSON encodes an item into a JSON string +// toJSON encodes a value into a JSON string. +// This function ignores any errors that occur during encoding. +// If the value cannot be encoded, it returns an empty string. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value func toJSON(v any) string { output, _ := json.Marshal(v) return string(output) } +// mustToJSON encodes a value into a JSON string. +// Unlike toJSON, this function returns any errors that occur during encoding. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value +// - error: Any error that occurred during encoding func mustToJSON(v any) (string, error) { output, err := json.Marshal(v) if err != nil { @@ -110,12 +181,29 @@ func mustToJSON(v any) (string, error) { return string(output), nil } -// toPrettyJSON encodes an item into a pretty (indented) JSON string +// toPrettyJSON encodes a value into a pretty (indented) JSON string. +// This function ignores any errors that occur during encoding. +// If the value cannot be encoded, it returns an empty string. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The indented JSON string representation of the value func toPrettyJSON(v any) string { output, _ := json.MarshalIndent(v, "", " ") return string(output) } +// mustToPrettyJSON encodes a value into a pretty (indented) JSON string. +// Unlike toPrettyJSON, this function returns any errors that occur during encoding. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The indented JSON string representation of the value +// - error: Any error that occurred during encoding func mustToPrettyJSON(v any) (string, error) { output, err := json.MarshalIndent(v, "", " ") if err != nil { @@ -124,7 +212,15 @@ func mustToPrettyJSON(v any) (string, error) { return string(output), nil } -// toRawJSON encodes an item into a JSON string with no escaping of HTML characters. +// toRawJSON encodes a value into a JSON string with no escaping of HTML characters. +// This function panics if an error occurs during encoding. +// Unlike toJSON, HTML characters like <, >, and & are not escaped. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value without HTML escaping func toRawJSON(v any) string { output, err := mustToRawJSON(v) if err != nil { @@ -133,7 +229,16 @@ func toRawJSON(v any) string { return output } -// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters. +// mustToRawJSON encodes a value into a JSON string with no escaping of HTML characters. +// Unlike toRawJSON, this function returns any errors that occur during encoding. +// HTML characters like <, >, and & are not escaped in the output. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value without HTML escaping +// - error: Any error that occurred during encoding func mustToRawJSON(v any) (string, error) { buf := new(bytes.Buffer) enc := json.NewEncoder(buf) @@ -144,7 +249,17 @@ func mustToRawJSON(v any) (string, error) { return strings.TrimSuffix(buf.String(), "\n"), nil } -// ternary returns the first value if the last value is true, otherwise returns the second value. +// ternary implements a conditional (ternary) operator. +// It returns the first value if the condition is true, otherwise returns the second value. +// This is similar to the ?: operator in many programming languages. +// +// Parameters: +// - vt: The value to return if the condition is true +// - vf: The value to return if the condition is false +// - v: The boolean condition to evaluate +// +// Returns: +// - any: Either vt or vf depending on the value of v func ternary(vt any, vf any, v bool) any { if v { return vt diff --git a/util/sprig/dict.go b/util/sprig/dict.go index 6485763e..0a282add 100644 --- a/util/sprig/dict.go +++ b/util/sprig/dict.go @@ -1,5 +1,15 @@ package sprig +// get retrieves a value from a map by its key. +// If the key exists, returns the corresponding value. +// If the key doesn't exist, returns an empty string. +// +// Parameters: +// - d: The map to retrieve the value from +// - key: The key to look up +// +// Returns: +// - any: The value associated with the key, or an empty string if not found func get(d map[string]any, key string) any { if val, ok := d[key]; ok { return val @@ -7,21 +17,58 @@ func get(d map[string]any, key string) any { return "" } +// set adds or updates a key-value pair in a map. +// Modifies the map in place and returns the modified map. +// +// Parameters: +// - d: The map to modify +// - key: The key to set +// - value: The value to associate with the key +// +// Returns: +// - map[string]any: The modified map (same instance as the input map) func set(d map[string]any, key string, value any) map[string]any { d[key] = value return d } +// unset removes a key-value pair from a map. +// If the key doesn't exist, the map remains unchanged. +// Modifies the map in place and returns the modified map. +// +// Parameters: +// - d: The map to modify +// - key: The key to remove +// +// Returns: +// - map[string]any: The modified map (same instance as the input map) func unset(d map[string]any, key string) map[string]any { delete(d, key) return d } +// hasKey checks if a key exists in a map. +// +// Parameters: +// - d: The map to check +// - key: The key to look for +// +// Returns: +// - bool: True if the key exists in the map, false otherwise func hasKey(d map[string]any, key string) bool { _, ok := d[key] return ok } +// pluck extracts values for a specific key from multiple maps. +// Only includes values from maps where the key exists. +// +// Parameters: +// - key: The key to extract values for +// - d: A variadic list of maps to extract values from +// +// Returns: +// - []any: A slice containing all values associated with the key across all maps func pluck(key string, d ...map[string]any) []any { var res []any for _, dict := range d { @@ -32,6 +79,14 @@ func pluck(key string, d ...map[string]any) []any { return res } +// keys collects all keys from one or more maps. +// The returned slice may contain duplicate keys if multiple maps contain the same key. +// +// Parameters: +// - dicts: A variadic list of maps to collect keys from +// +// Returns: +// - []string: A slice containing all keys from all provided maps func keys(dicts ...map[string]any) []string { var k []string for _, dict := range dicts { @@ -42,6 +97,15 @@ func keys(dicts ...map[string]any) []string { return k } +// pick creates a new map containing only the specified keys from the original map. +// If a key doesn't exist in the original map, it won't be included in the result. +// +// Parameters: +// - dict: The source map +// - keys: A variadic list of keys to include in the result +// +// Returns: +// - map[string]any: A new map containing only the specified keys and their values func pick(dict map[string]any, keys ...string) map[string]any { res := map[string]any{} for _, k := range keys { @@ -52,6 +116,15 @@ func pick(dict map[string]any, keys ...string) map[string]any { return res } +// omit creates a new map excluding the specified keys from the original map. +// The original map remains unchanged. +// +// Parameters: +// - dict: The source map +// - keys: A variadic list of keys to exclude from the result +// +// Returns: +// - map[string]any: A new map containing all key-value pairs except those specified func omit(dict map[string]any, keys ...string) map[string]any { res := map[string]any{} omit := make(map[string]bool, len(keys)) @@ -66,6 +139,16 @@ func omit(dict map[string]any, keys ...string) map[string]any { return res } +// dict creates a new map from a list of key-value pairs. +// The arguments are treated as key-value pairs, where even-indexed arguments are keys +// and odd-indexed arguments are values. +// If there's an odd number of arguments, the last key will be assigned an empty string value. +// +// Parameters: +// - v: A variadic list of alternating keys and values +// +// Returns: +// - map[string]any: A new map containing the specified key-value pairs func dict(v ...any) map[string]any { dict := map[string]any{} lenv := len(v) @@ -80,6 +163,14 @@ func dict(v ...any) map[string]any { return dict } +// values collects all values from a map into a slice. +// The order of values in the resulting slice is not guaranteed. +// +// Parameters: +// - dict: The map to collect values from +// +// Returns: +// - []any: A slice containing all values from the map func values(dict map[string]any) []any { var values []any for _, value := range dict { @@ -88,6 +179,22 @@ func values(dict map[string]any) []any { return values } +// dig safely accesses nested values in maps using a sequence of keys. +// If any key in the path doesn't exist, it returns the default value. +// The function expects at least 3 arguments: one or more keys, a default value, and a map. +// +// Parameters: +// - ps: A variadic list where: +// - The first N-2 arguments are string keys forming the path +// - The second-to-last argument is the default value to return if the path doesn't exist +// - The last argument is the map to traverse +// +// Returns: +// - any: The value found at the specified path, or the default value if not found +// - error: Any error that occurred during traversal +// +// Panics: +// - If fewer than 3 arguments are provided func dig(ps ...any) (any, error) { if len(ps) < 3 { panic("dig needs at least three arguments") @@ -102,6 +209,17 @@ func dig(ps ...any) (any, error) { return digFromDict(dict, def, ks) } +// digFromDict is a helper function for dig that recursively traverses a map using a sequence of keys. +// If any key in the path doesn't exist, it returns the default value. +// +// Parameters: +// - dict: The map to traverse +// - d: The default value to return if the path doesn't exist +// - ks: A slice of string keys forming the path to traverse +// +// Returns: +// - any: The value found at the specified path, or the default value if not found +// - error: Any error that occurred during traversal func digFromDict(dict map[string]any, d any, ks []string) (any, error) { k, ns := ks[0], ks[1:] step, has := dict[k] diff --git a/util/sprig/flow_control.go b/util/sprig/flow_control.go index 2bdf382c..cfaa5081 100644 --- a/util/sprig/flow_control.go +++ b/util/sprig/flow_control.go @@ -2,6 +2,7 @@ package sprig import "errors" +// fail is a function that always returns an error with the given message. func fail(msg string) (string, error) { return "", errors.New(msg) } diff --git a/util/sprig/functions.go b/util/sprig/functions.go index f0232a5b..8dbb23f8 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -12,6 +12,7 @@ import ( const ( loopExecutionLimit = 10_000 // Limit the number of loop executions to prevent execution from taking too long stringLengthLimit = 100_000 // Limit the length of strings to prevent memory issues + sliceSizeLimit = 10_000 // Limit the size of slices to prevent memory issues ) // TxtFuncMap produces the function map. diff --git a/util/sprig/functions_test.go b/util/sprig/functions_test.go index e5989b98..4e83e993 100644 --- a/util/sprig/functions_test.go +++ b/util/sprig/functions_test.go @@ -52,7 +52,7 @@ func runtv(tpl, expect string, vars any) error { return err } if expect != b.String() { - return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) + return fmt.Errorf("expected '%s', got '%s'", expect, b.String()) } return nil } diff --git a/util/sprig/list.go b/util/sprig/list.go index d8882af0..fdcbf5e6 100644 --- a/util/sprig/list.go +++ b/util/sprig/list.go @@ -11,10 +11,15 @@ import ( // ints, and other types not implementing []any can be worked with. // For example, this is useful if you need to work on the output of regexs. +// list creates a new list (slice) containing the provided arguments. +// It accepts any number of arguments of any type and returns them as a slice. func list(v ...any) []any { return v } +// push appends an element to the end of a list (slice or array). +// It takes a list and a value, and returns a new list with the value appended. +// This function will panic if the first argument is not a slice or array. func push(list any, v any) []any { l, err := mustPush(list, v) if err != nil { @@ -23,99 +28,103 @@ func push(list any, v any) []any { return l } +// mustPush is the implementation of push that returns an error instead of panicking. +// It converts the input list to a slice of any type, then appends the value. func mustPush(list any, v any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() nl := make([]any, l) for i := 0; i < l; i++ { nl[i] = l2.Index(i).Interface() } - return append(nl, v), nil - default: return nil, fmt.Errorf("cannot push on type %s", tp) } } +// prepend adds an element to the beginning of a list (slice or array). +// It takes a list and a value, and returns a new list with the value at the start. +// This function will panic if the first argument is not a slice or array. func prepend(list any, v any) []any { l, err := mustPrepend(list, v) if err != nil { panic(err) } - return l } +// mustPrepend is the implementation of prepend that returns an error instead of panicking. +// It converts the input list to a slice of any type, then prepends the value. func mustPrepend(list any, v any) ([]any, error) { - //return append([]any{v}, list...) - tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() nl := make([]any, l) for i := 0; i < l; i++ { nl[i] = l2.Index(i).Interface() } - return append([]any{v}, nl...), nil - default: return nil, fmt.Errorf("cannot prepend on type %s", tp) } } +// chunk divides a list into sub-lists of the specified size. +// It takes a size and a list, and returns a list of lists, each containing +// up to 'size' elements from the original list. +// This function will panic if the second argument is not a slice or array. func chunk(size int, list any) [][]any { l, err := mustChunk(size, list) if err != nil { panic(err) } - return l } +// mustChunk is the implementation of chunk that returns an error instead of panicking. +// It divides the input list into chunks of the specified size. func mustChunk(size int, list any) ([][]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() - - cs := int(math.Floor(float64(l-1)/float64(size)) + 1) - nl := make([][]any, cs) - - for i := 0; i < cs; i++ { + numChunks := int(math.Floor(float64(l-1)/float64(size)) + 1) + if numChunks > sliceSizeLimit { + return nil, fmt.Errorf("number of chunks %d exceeds maximum limit of %d", numChunks, sliceSizeLimit) + } + result := make([][]any, numChunks) + for i := 0; i < numChunks; i++ { clen := size - if i == cs-1 { + // Handle the last chunk which might be smaller + if i == numChunks-1 { clen = int(math.Floor(math.Mod(float64(l), float64(size)))) if clen == 0 { clen = size } } - - nl[i] = make([]any, clen) - + result[i] = make([]any, clen) for j := 0; j < clen; j++ { ix := i*size + j - nl[i][j] = l2.Index(ix).Interface() + result[i][j] = l2.Index(ix).Interface() } } - - return nl, nil + return result, nil default: return nil, fmt.Errorf("cannot chunk type %s", tp) } } +// last returns the last element of a list (slice or array). +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func last(list any) any { l, err := mustLast(list) if err != nil { @@ -125,6 +134,8 @@ func last(list any) any { return l } +// mustLast is the implementation of last that returns an error instead of panicking. +// It returns the last element of the list or nil if the list is empty. func mustLast(list any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { @@ -142,6 +153,9 @@ func mustLast(list any) (any, error) { } } +// first returns the first element of a list (slice or array). +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func first(list any) any { l, err := mustFirst(list) if err != nil { @@ -151,6 +165,8 @@ func first(list any) any { return l } +// mustFirst is the implementation of first that returns an error instead of panicking. +// It returns the first element of the list or nil if the list is empty. func mustFirst(list any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { @@ -168,6 +184,9 @@ func mustFirst(list any) (any, error) { } } +// rest returns all elements of a list except the first one. +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func rest(list any) []any { l, err := mustRest(list) if err != nil { @@ -177,28 +196,30 @@ func rest(list any) []any { return l } +// mustRest is the implementation of rest that returns an error instead of panicking. +// It returns all elements of the list except the first one, or nil if the list is empty. func mustRest(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() if l == 0 { return nil, nil } - nl := make([]any, l-1) for i := 1; i < l; i++ { nl[i-1] = l2.Index(i).Interface() } - return nl, nil default: return nil, fmt.Errorf("cannot find rest on type %s", tp) } } +// initial returns all elements of a list except the last one. +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func initial(list any) []any { l, err := mustInitial(list) if err != nil { @@ -208,28 +229,30 @@ func initial(list any) []any { return l } +// mustInitial is the implementation of initial that returns an error instead of panicking. +// It returns all elements of the list except the last one, or nil if the list is empty. func mustInitial(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() if l == 0 { return nil, nil } - nl := make([]any, l-1) for i := 0; i < l-1; i++ { nl[i] = l2.Index(i).Interface() } - return nl, nil default: return nil, fmt.Errorf("cannot find initial on type %s", tp) } } +// sortAlpha sorts a list of strings alphabetically. +// If the input is not a slice or array, it returns a single-element slice +// containing the string representation of the input. func sortAlpha(list any) []string { k := reflect.Indirect(reflect.ValueOf(list)).Kind() switch k { @@ -242,6 +265,8 @@ func sortAlpha(list any) []string { return []string{strval(list)} } +// reverse returns a new list with the elements in reverse order. +// This function will panic if the argument is not a slice or array. func reverse(v any) []any { l, err := mustReverse(v) if err != nil { @@ -251,42 +276,45 @@ func reverse(v any) []any { return l } +// mustReverse is the implementation of reverse that returns an error instead of panicking. +// It returns a new list with the elements in reverse order. func mustReverse(v any) ([]any, error) { tp := reflect.TypeOf(v).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(v) - l := l2.Len() // We do not sort in place because the incoming array should not be altered. nl := make([]any, l) for i := 0; i < l; i++ { nl[l-i-1] = l2.Index(i).Interface() } - return nl, nil default: return nil, fmt.Errorf("cannot find reverse on type %s", tp) } } +// compact returns a new list with all "empty" elements removed. +// An element is considered empty if it's nil, zero, an empty string, or an empty collection. +// This function will panic if the argument is not a slice or array. func compact(list any) []any { l, err := mustCompact(list) if err != nil { panic(err) } - return l } +// mustCompact is the implementation of compact that returns an error instead of panicking. +// It returns a new list with all "empty" elements removed. func mustCompact(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() - nl := []any{} + var nl []any var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() @@ -294,30 +322,32 @@ func mustCompact(list any) ([]any, error) { nl = append(nl, item) } } - return nl, nil default: return nil, fmt.Errorf("cannot compact on type %s", tp) } } +// uniq returns a new list with duplicate elements removed. +// The first occurrence of each element is kept. +// This function will panic if the argument is not a slice or array. func uniq(list any) []any { l, err := mustUniq(list) if err != nil { panic(err) } - return l } +// mustUniq is the implementation of uniq that returns an error instead of panicking. +// It returns a new list with duplicate elements removed. func mustUniq(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() - dest := []any{} + var dest []any var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() @@ -325,13 +355,15 @@ func mustUniq(list any) ([]any, error) { dest = append(dest, item) } } - return dest, nil default: return nil, fmt.Errorf("cannot find uniq on type %s", tp) } } +// inList checks if a value is present in a list. +// It uses deep equality comparison to check for matches. +// Returns true if the value is found, false otherwise. func inList(haystack []any, needle any) bool { for _, h := range haystack { if reflect.DeepEqual(needle, h) { @@ -341,21 +373,23 @@ func inList(haystack []any, needle any) bool { return false } +// without returns a new list with all occurrences of the specified values removed. +// This function will panic if the first argument is not a slice or array. func without(list any, omit ...any) []any { l, err := mustWithout(list, omit...) if err != nil { panic(err) } - return l } +// mustWithout is the implementation of without that returns an error instead of panicking. +// It returns a new list with all occurrences of the specified values removed. func mustWithout(list any, omit ...any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() res := []any{} var item any @@ -365,22 +399,25 @@ func mustWithout(list any, omit ...any) ([]any, error) { res = append(res, item) } } - return res, nil default: return nil, fmt.Errorf("cannot find without on type %s", tp) } } +// has checks if a value is present in a list. +// Returns true if the value is found, false otherwise. +// This function will panic if the second argument is not a slice or array. func has(needle any, haystack any) bool { l, err := mustHas(needle, haystack) if err != nil { panic(err) } - return l } +// mustHas is the implementation of has that returns an error instead of panicking. +// It checks if a value is present in a list. func mustHas(needle any, haystack any) (bool, error) { if haystack == nil { return false, nil @@ -397,38 +434,41 @@ func mustHas(needle any, haystack any) (bool, error) { return true, nil } } - return false, nil default: return false, fmt.Errorf("cannot find has on type %s", tp) } } +// slice extracts a portion of a list based on the provided indices. +// Usage examples: // $list := [1, 2, 3, 4, 5] // slice $list -> list[0:5] = list[:] // slice $list 0 3 -> list[0:3] = list[:3] // slice $list 3 5 -> list[3:5] // slice $list 3 -> list[3:5] = list[3:] +// +// This function will panic if the first argument is not a slice or array. func slice(list any, indices ...any) any { l, err := mustSlice(list, indices...) if err != nil { panic(err) } - return l } +// mustSlice is the implementation of slice that returns an error instead of panicking. +// It extracts a portion of a list based on the provided indices. func mustSlice(list any, indices ...any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() if l == 0 { return nil, nil } - + // Determine start and end indices var start, end int if len(indices) > 0 { start = toInt(indices[0]) @@ -438,13 +478,15 @@ func mustSlice(list any, indices ...any) (any, error) { } else { end = toInt(indices[1]) } - return l2.Slice(start, end).Interface(), nil default: return nil, fmt.Errorf("list should be type of slice or array but %s", tp) } } +// concat combines multiple lists into a single list. +// It takes any number of lists and returns a new list containing all elements. +// This function will panic if any argument is not a slice or array. func concat(lists ...any) any { var res []any for _, list := range lists { diff --git a/util/sprig/list_test.go b/util/sprig/list_test.go index ec4c4c14..e6693b2f 100644 --- a/util/sprig/list_test.go +++ b/util/sprig/list_test.go @@ -1,6 +1,7 @@ package sprig import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -68,6 +69,8 @@ func TestMustChunk(t *testing.T) { for tpl, expect := range tests { assert.NoError(t, runt(tpl, expect)) } + err := runt(`{{ tuple `+strings.Repeat(" 0", 10001)+` | mustChunk 1 }}`, "a") + assert.ErrorContains(t, err, "number of chunks 10001 exceeds maximum limit of 10000") } func TestPrepend(t *testing.T) { diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go index 32466818..7ee3616d 100644 --- a/util/sprig/numeric.go +++ b/util/sprig/numeric.go @@ -9,7 +9,20 @@ import ( "strings" ) -// toFloat64 converts 64-bit floats +// toFloat64 converts a value to a 64-bit float. +// It handles various input types: +// - string: parsed as a float, returns 0 if parsing fails +// - integer types: converted to float64 +// - unsigned integer types: converted to float64 +// - float types: returned as is +// - bool: true becomes 1.0, false becomes 0.0 +// - other types: returns 0.0 +// +// Parameters: +// - v: The value to convert to float64 +// +// Returns: +// - float64: The converted value func toFloat64(v any) float64 { if str, ok := v.(string); ok { iv, err := strconv.ParseFloat(str, 64) @@ -39,12 +52,27 @@ func toFloat64(v any) float64 { } } +// toInt converts a value to a 32-bit integer. +// This is a wrapper around toInt64 that casts the result to int. +// +// Parameters: +// - v: The value to convert to int +// +// Returns: +// - int: The converted value func toInt(v any) int { // It's not optimal. But I don't want duplicate toInt64 code. return int(toInt64(v)) } -// toInt64 converts integer types to 64-bit integers +// toInt64 converts a value to a 64-bit integer. +// It handles various input types: +// - string: parsed as an integer, returns 0 if parsing fails +// - integer types: converted to int64 +// - unsigned integer types: converted to int64 (values > MaxInt64 become MaxInt64) +// - float types: truncated to int64 +// - bool: true becomes 1, false becomes 0 +// - other types: returns 0 func toInt64(v any) int64 { if str, ok := v.(string); ok { iv, err := strconv.ParseInt(str, 10, 64) @@ -53,7 +81,6 @@ func toInt64(v any) int64 { } return iv } - val := reflect.Indirect(reflect.ValueOf(v)) switch val.Kind() { case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: @@ -79,10 +106,26 @@ func toInt64(v any) int64 { } } +// add1 increments a value by 1. +// The input is first converted to int64 using toInt64. +// +// Parameters: +// - i: The value to increment +// +// Returns: +// - int64: The incremented value func add1(i any) int64 { return toInt64(i) + 1 } +// add sums all the provided values. +// All inputs are converted to int64 using toInt64 before addition. +// +// Parameters: +// - i: A variadic list of values to sum +// +// Returns: +// - int64: The sum of all values func add(i ...any) int64 { var a int64 for _, b := range i { @@ -91,18 +134,61 @@ func add(i ...any) int64 { return a } +// sub subtracts the second value from the first. +// Both inputs are converted to int64 using toInt64 before subtraction. +// +// Parameters: +// - a: The value to subtract from +// - b: The value to subtract +// +// Returns: +// - int64: The result of a - b func sub(a, b any) int64 { return toInt64(a) - toInt64(b) } +// div divides the first value by the second. +// Both inputs are converted to int64 using toInt64 before division. +// Note: This performs integer division, so the result is truncated. +// +// Parameters: +// - a: The dividend +// - b: The divisor +// +// Returns: +// - int64: The result of a / b +// +// Panics: +// - If b evaluates to 0 (division by zero) func div(a, b any) int64 { return toInt64(a) / toInt64(b) } +// mod returns the remainder of dividing the first value by the second. +// Both inputs are converted to int64 using toInt64 before the modulo operation. +// +// Parameters: +// - a: The dividend +// - b: The divisor +// +// Returns: +// - int64: The remainder of a / b +// +// Panics: +// - If b evaluates to 0 (modulo by zero) func mod(a, b any) int64 { return toInt64(a) % toInt64(b) } +// mul multiplies all the provided values. +// All inputs are converted to int64 using toInt64 before multiplication. +// +// Parameters: +// - a: The first value to multiply +// - v: Additional values to multiply with a +// +// Returns: +// - int64: The product of all values func mul(a any, v ...any) int64 { val := toInt64(a) for _, b := range v { @@ -111,10 +197,30 @@ func mul(a any, v ...any) int64 { return val } +// randInt generates a random integer between min (inclusive) and max (exclusive). +// +// Parameters: +// - min: The lower bound (inclusive) +// - max: The upper bound (exclusive) +// +// Returns: +// - int: A random integer in the range [min, max) +// +// Panics: +// - If max <= min (via rand.Intn) func randInt(min, max int) int { return rand.Intn(max-min) + min } +// maxAsInt64 returns the maximum value from a list of values as an int64. +// All inputs are converted to int64 using toInt64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - int64: The maximum value from all inputs func maxAsInt64(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { @@ -126,6 +232,15 @@ func maxAsInt64(a any, i ...any) int64 { return aa } +// maxAsFloat64 returns the maximum value from a list of values as a float64. +// All inputs are converted to float64 using toFloat64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - float64: The maximum value from all inputs func maxAsFloat64(a any, i ...any) float64 { m := toFloat64(a) for _, b := range i { @@ -134,6 +249,15 @@ func maxAsFloat64(a any, i ...any) float64 { return m } +// minAsInt64 returns the minimum value from a list of values as an int64. +// All inputs are converted to int64 using toInt64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - int64: The minimum value from all inputs func minAsInt64(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { @@ -145,6 +269,15 @@ func minAsInt64(a any, i ...any) int64 { return aa } +// minAsFloat64 returns the minimum value from a list of values as a float64. +// All inputs are converted to float64 using toFloat64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - float64: The minimum value from all inputs func minAsFloat64(a any, i ...any) float64 { m := toFloat64(a) for _, b := range i { @@ -153,6 +286,14 @@ func minAsFloat64(a any, i ...any) float64 { return m } +// until generates a sequence of integers from 0 to count (exclusive). +// If count is negative, it generates a sequence from 0 to count (inclusive) with step -1. +// +// Parameters: +// - count: The end value (exclusive if positive, inclusive if negative) +// +// Returns: +// - []int: A slice containing the generated sequence func until(count int) []int { step := 1 if count < 0 { @@ -161,6 +302,23 @@ func until(count int) []int { return untilStep(0, count, step) } +// untilStep generates a sequence of integers from start to stop with the specified step. +// The sequence is generated as follows: +// - If step is 0, returns an empty slice +// - If stop < start and step < 0, generates a decreasing sequence from start to stop (exclusive) +// - If stop > start and step > 0, generates an increasing sequence from start to stop (exclusive) +// - Otherwise, returns an empty slice +// +// Parameters: +// - start: The starting value (inclusive) +// - stop: The ending value (exclusive) +// - step: The increment between values +// +// Returns: +// - []int: A slice containing the generated sequence +// +// Panics: +// - If the number of iterations would exceed loopExecutionLimit func untilStep(start, stop, step int) []int { var v []int if step == 0 { @@ -188,14 +346,44 @@ func untilStep(start, stop, step int) []int { return v } +// floor returns the greatest integer value less than or equal to the input. +// The input is first converted to float64 using toFloat64. +// +// Parameters: +// - a: The value to floor +// +// Returns: +// - float64: The greatest integer value less than or equal to a func floor(a any) float64 { return math.Floor(toFloat64(a)) } +// ceil returns the least integer value greater than or equal to the input. +// The input is first converted to float64 using toFloat64. +// +// Parameters: +// - a: The value to ceil +// +// Returns: +// - float64: The least integer value greater than or equal to a func ceil(a any) float64 { return math.Ceil(toFloat64(a)) } +// round rounds a number to a specified number of decimal places. +// The input is first converted to float64 using toFloat64. +// +// Parameters: +// - a: The value to round +// - p: The number of decimal places to round to +// - rOpt: Optional rounding threshold (default is 0.5) +// +// Returns: +// - float64: The rounded value +// +// Examples: +// - round(3.14159, 2) returns 3.14 +// - round(3.14159, 2, 0.6) returns 3.14 (only rounds up if fraction ≥ 0.6) func round(a any, p int, rOpt ...float64) float64 { roundOn := .5 if len(rOpt) > 0 { @@ -203,7 +391,6 @@ func round(a any, p int, rOpt ...float64) float64 { } val := toFloat64(a) places := toFloat64(p) - var round float64 pow := math.Pow(10, places) digit := pow * val @@ -216,7 +403,15 @@ func round(a any, p int, rOpt ...float64) float64 { return round / pow } -// converts unix octal to decimal +// toDecimal converts a value from octal to decimal. +// The input is first converted to a string using fmt.Sprint, then parsed as an octal number. +// If the parsing fails, it returns 0. +// +// Parameters: +// - v: The octal value to convert +// +// Returns: +// - int64: The decimal representation of the octal value func toDecimal(v any) int64 { result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) if err != nil { @@ -225,11 +420,34 @@ func toDecimal(v any) int64 { return result } +// atoi converts a string to an integer. +// If the conversion fails, it returns 0. +// +// Parameters: +// - a: The string to convert +// +// Returns: +// - int: The integer value of the string func atoi(a string) int { i, _ := strconv.Atoi(a) return i } +// seq generates a sequence of integers and returns them as a space-delimited string. +// The behavior depends on the number of parameters: +// - 0 params: Returns an empty string +// - 1 param: Generates sequence from 1 to param[0] +// - 2 params: Generates sequence from param[0] to param[1] +// - 3 params: Generates sequence from param[0] to param[2] with step param[1] +// +// If the end is less than the start, the sequence will be decreasing unless +// a positive step is explicitly provided (which would result in an empty string). +// +// Parameters: +// - params: Variable number of integers defining the sequence +// +// Returns: +// - string: A space-delimited string of the generated sequence func seq(params ...int) string { increment := 1 switch len(params) { @@ -266,6 +484,16 @@ func seq(params ...int) string { } } +// intArrayToString converts a slice of integers to a space-delimited string. +// The function removes the square brackets that would normally appear when +// converting a slice to a string. +// +// Parameters: +// - slice: The slice of integers to convert +// - delimiter: The delimiter to use between elements +// +// Returns: +// - string: A delimited string representation of the integer slice func intArrayToString(slice []int, delimiter string) string { return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimiter), "[]") } diff --git a/util/sprig/reflect.go b/util/sprig/reflect.go index 5e37f64f..6315a780 100644 --- a/util/sprig/reflect.go +++ b/util/sprig/reflect.go @@ -6,23 +6,65 @@ import ( ) // typeIs returns true if the src is the type named in target. +// It compares the type name of src with the target string. +// +// Parameters: +// - target: The type name to check against +// - src: The value whose type will be checked +// +// Returns: +// - bool: True if the type name of src matches target, false otherwise func typeIs(target string, src any) bool { return target == typeOf(src) } +// typeIsLike returns true if the src is the type named in target or a pointer to that type. +// This is useful when you need to check for both a type and a pointer to that type. +// +// Parameters: +// - target: The type name to check against +// - src: The value whose type will be checked +// +// Returns: +// - bool: True if the type of src matches target or "*"+target, false otherwise func typeIsLike(target string, src any) bool { t := typeOf(src) return target == t || "*"+target == t } +// typeOf returns the type of a value as a string. +// It uses fmt.Sprintf with the %T format verb to get the type name. +// +// Parameters: +// - src: The value whose type name will be returned +// +// Returns: +// - string: The type name of src func typeOf(src any) string { return fmt.Sprintf("%T", src) } +// kindIs returns true if the kind of src matches the target kind. +// This checks the underlying kind (e.g., "string", "int", "map") rather than the specific type. +// +// Parameters: +// - target: The kind name to check against +// - src: The value whose kind will be checked +// +// Returns: +// - bool: True if the kind of src matches target, false otherwise func kindIs(target string, src any) bool { return target == kindOf(src) } +// kindOf returns the kind of a value as a string. +// The kind represents the specific Go type category (e.g., "string", "int", "map", "slice"). +// +// Parameters: +// - src: The value whose kind will be returned +// +// Returns: +// - string: The kind of src as a string func kindOf(src any) string { return reflect.ValueOf(src).Kind().String() } diff --git a/util/sprig/regex.go b/util/sprig/regex.go index fab55101..9853d2e1 100644 --- a/util/sprig/regex.go +++ b/util/sprig/regex.go @@ -4,20 +4,60 @@ import ( "regexp" ) +// regexMatch checks if a string matches a regular expression pattern. +// It ignores any errors that might occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to match against +// - s: The string to check +// +// Returns: +// - bool: True if the string matches the pattern, false otherwise func regexMatch(regex string, s string) bool { match, _ := regexp.MatchString(regex, s) return match } +// mustRegexMatch checks if a string matches a regular expression pattern. +// Unlike regexMatch, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to match against +// - s: The string to check +// +// Returns: +// - bool: True if the string matches the pattern, false otherwise +// - error: Any error that occurred during regex compilation func mustRegexMatch(regex string, s string) (bool, error) { return regexp.MatchString(regex, s) } +// regexFindAll finds all matches of a regular expression in a string. +// It panics if the regex pattern cannot be compiled. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - n: The maximum number of matches to return (negative means all matches) +// +// Returns: +// - []string: A slice containing all matched substrings func regexFindAll(regex string, s string, n int) []string { r := regexp.MustCompile(regex) return r.FindAllString(s, n) } +// mustRegexFindAll finds all matches of a regular expression in a string. +// Unlike regexFindAll, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - n: The maximum number of matches to return (negative means all matches) +// +// Returns: +// - []string: A slice containing all matched substrings +// - error: Any error that occurred during regex compilation func mustRegexFindAll(regex string, s string, n int) ([]string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -26,11 +66,30 @@ func mustRegexFindAll(regex string, s string, n int) ([]string, error) { return r.FindAllString(s, n), nil } +// regexFind finds the first match of a regular expression in a string. +// It panics if the regex pattern cannot be compiled. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// +// Returns: +// - string: The first matched substring, or an empty string if no match func regexFind(regex string, s string) string { r := regexp.MustCompile(regex) return r.FindString(s) } +// mustRegexFind finds the first match of a regular expression in a string. +// Unlike regexFind, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// +// Returns: +// - string: The first matched substring, or an empty string if no match +// - error: Any error that occurred during regex compilation func mustRegexFind(regex string, s string) (string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -39,11 +98,34 @@ func mustRegexFind(regex string, s string) (string, error) { return r.FindString(s), nil } +// regexReplaceAll replaces all matches of a regular expression with a replacement string. +// It panics if the regex pattern cannot be compiled. +// The replacement string can contain $1, $2, etc. for submatches. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The replacement string (can contain $1, $2, etc. for submatches) +// +// Returns: +// - string: The resulting string after all replacements func regexReplaceAll(regex string, s string, repl string) string { r := regexp.MustCompile(regex) return r.ReplaceAllString(s, repl) } +// mustRegexReplaceAll replaces all matches of a regular expression with a replacement string. +// Unlike regexReplaceAll, this function returns any errors that occur during regex compilation. +// The replacement string can contain $1, $2, etc. for submatches. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The replacement string (can contain $1, $2, etc. for submatches) +// +// Returns: +// - string: The resulting string after all replacements +// - error: Any error that occurred during regex compilation func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -52,11 +134,34 @@ func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { return r.ReplaceAllString(s, repl), nil } +// regexReplaceAllLiteral replaces all matches of a regular expression with a literal replacement string. +// It panics if the regex pattern cannot be compiled. +// Unlike regexReplaceAll, the replacement string is used literally (no $1, $2 processing). +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The literal replacement string +// +// Returns: +// - string: The resulting string after all replacements func regexReplaceAllLiteral(regex string, s string, repl string) string { r := regexp.MustCompile(regex) return r.ReplaceAllLiteralString(s, repl) } +// mustRegexReplaceAllLiteral replaces all matches of a regular expression with a literal replacement string. +// Unlike regexReplaceAllLiteral, this function returns any errors that occur during regex compilation. +// The replacement string is used literally (no $1, $2 processing). +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The literal replacement string +// +// Returns: +// - string: The resulting string after all replacements +// - error: Any error that occurred during regex compilation func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -65,11 +170,32 @@ func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, er return r.ReplaceAllLiteralString(s, repl), nil } +// regexSplit splits a string by a regular expression pattern. +// It panics if the regex pattern cannot be compiled. +// +// Parameters: +// - regex: The regular expression pattern to split on +// - s: The string to split +// - n: The maximum number of substrings to return (negative means all substrings) +// +// Returns: +// - []string: A slice containing the substrings between regex matches func regexSplit(regex string, s string, n int) []string { r := regexp.MustCompile(regex) return r.Split(s, n) } +// mustRegexSplit splits a string by a regular expression pattern. +// Unlike regexSplit, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to split on +// - s: The string to split +// - n: The maximum number of substrings to return (negative means all substrings) +// +// Returns: +// - []string: A slice containing the substrings between regex matches +// - error: Any error that occurred during regex compilation func mustRegexSplit(regex string, s string, n int) ([]string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -78,6 +204,14 @@ func mustRegexSplit(regex string, s string, n int) ([]string, error) { return r.Split(s, n), nil } +// regexQuoteMeta escapes all regular expression metacharacters in a string. +// This is useful when you want to use a string as a literal in a regular expression. +// +// Parameters: +// - s: The string to escape +// +// Returns: +// - string: The escaped string with all regex metacharacters quoted func regexQuoteMeta(s string) string { return regexp.QuoteMeta(s) } diff --git a/util/sprig/strings.go b/util/sprig/strings.go index 8a1bdc1b..e64f82d9 100644 --- a/util/sprig/strings.go +++ b/util/sprig/strings.go @@ -11,10 +11,25 @@ import ( "strings" ) +// base64encode encodes a string to base64 using standard encoding. +// +// Parameters: +// - v: The string to encode +// +// Returns: +// - string: The base64 encoded string func base64encode(v string) string { return base64.StdEncoding.EncodeToString([]byte(v)) } +// base64decode decodes a base64 encoded string. +// If the input is not valid base64, it returns the error message as a string. +// +// Parameters: +// - v: The base64 encoded string to decode +// +// Returns: +// - string: The decoded string, or an error message if decoding fails func base64decode(v string) string { data, err := base64.StdEncoding.DecodeString(v) if err != nil { @@ -23,10 +38,25 @@ func base64decode(v string) string { return string(data) } +// base32encode encodes a string to base32 using standard encoding. +// +// Parameters: +// - v: The string to encode +// +// Returns: +// - string: The base32 encoded string func base32encode(v string) string { return base32.StdEncoding.EncodeToString([]byte(v)) } +// base32decode decodes a base32 encoded string. +// If the input is not valid base32, it returns the error message as a string. +// +// Parameters: +// - v: The base32 encoded string to decode +// +// Returns: +// - string: The decoded string, or an error message if decoding fails func base32decode(v string) string { data, err := base32.StdEncoding.DecodeString(v) if err != nil { @@ -35,6 +65,14 @@ func base32decode(v string) string { return string(data) } +// quote adds double quotes around each non-nil string in the input and joins them with spaces. +// This uses Go's %q formatter which handles escaping special characters. +// +// Parameters: +// - str: A variadic list of values to quote +// +// Returns: +// - string: The quoted strings joined with spaces func quote(str ...any) string { out := make([]string, 0, len(str)) for _, s := range str { @@ -45,6 +83,14 @@ func quote(str ...any) string { return strings.Join(out, " ") } +// squote adds single quotes around each non-nil value in the input and joins them with spaces. +// Unlike quote, this doesn't escape special characters. +// +// Parameters: +// - str: A variadic list of values to quote +// +// Returns: +// - string: The single-quoted values joined with spaces func squote(str ...any) string { out := make([]string, 0, len(str)) for _, s := range str { @@ -55,25 +101,69 @@ func squote(str ...any) string { return strings.Join(out, " ") } +// cat concatenates all non-nil values into a single string. +// Nil values are removed before concatenation. +// +// Parameters: +// - v: A variadic list of values to concatenate +// +// Returns: +// - string: The concatenated string func cat(v ...any) string { v = removeNilElements(v) r := strings.TrimSpace(strings.Repeat("%v ", len(v))) return fmt.Sprintf(r, v...) } +// indent adds a specified number of spaces at the beginning of each line in a string. +// +// Parameters: +// - spaces: The number of spaces to add +// - v: The string to indent +// +// Returns: +// - string: The indented string func indent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) return pad + strings.Replace(v, "\n", "\n"+pad, -1) } +// nindent adds a newline followed by an indented string. +// It's a shorthand for "\n" + indent(spaces, v). +// +// Parameters: +// - spaces: The number of spaces to add +// - v: The string to indent +// +// Returns: +// - string: A newline followed by the indented string func nindent(spaces int, v string) string { return "\n" + indent(spaces, v) } +// replace replaces all occurrences of a substring with another substring. +// +// Parameters: +// - old: The substring to replace +// - new: The replacement substring +// - src: The source string +// +// Returns: +// - string: The resulting string after all replacements func replace(old, new, src string) string { return strings.Replace(src, old, new, -1) } +// plural returns the singular or plural form of a word based on the count. +// If count is 1, it returns the singular form, otherwise it returns the plural form. +// +// Parameters: +// - one: The singular form of the word +// - many: The plural form of the word +// - count: The count to determine which form to use +// +// Returns: +// - string: Either the singular or plural form based on the count func plural(one, many string, count int) string { if count == 1 { return one @@ -81,6 +171,19 @@ func plural(one, many string, count int) string { return many } +// strslice converts a value to a slice of strings. +// It handles various input types: +// - []string: returned as is +// - []any: converted to []string, skipping nil values +// - arrays and slices: converted to []string, skipping nil values +// - nil: returns an empty slice +// - anything else: returns a single-element slice with the string representation +// +// Parameters: +// - v: The value to convert to a string slice +// +// Returns: +// - []string: A slice of strings func strslice(v any) []string { switch v := v.(type) { case []string: @@ -116,6 +219,14 @@ func strslice(v any) []string { } } +// removeNilElements creates a new slice with all nil elements removed. +// This is a helper function used by other functions like cat. +// +// Parameters: +// - v: The slice to process +// +// Returns: +// - []any: A new slice with all nil elements removed func removeNilElements(v []any) []any { newSlice := make([]any, 0, len(v)) for _, i := range v { @@ -126,6 +237,19 @@ func removeNilElements(v []any) []any { return newSlice } +// strval converts any value to a string. +// It handles various types: +// - string: returned as is +// - []byte: converted to string +// - error: returns the error message +// - fmt.Stringer: calls the String() method +// - anything else: uses fmt.Sprintf("%v", v) +// +// Parameters: +// - v: The value to convert to a string +// +// Returns: +// - string: The string representation of the value func strval(v any) string { switch v := v.(type) { case string: @@ -141,6 +265,17 @@ func strval(v any) string { } } +// trunc truncates a string to a specified length. +// If c is positive, it returns the first c characters. +// If c is negative, it returns the last |c| characters. +// If the string is shorter than the requested length, it returns the original string. +// +// Parameters: +// - c: The number of characters to keep (positive from start, negative from end) +// - s: The string to truncate +// +// Returns: +// - string: The truncated string func trunc(c int, s string) string { if c < 0 && len(s)+c > 0 { return s[len(s)+c:] @@ -151,14 +286,40 @@ func trunc(c int, s string) string { return s } +// title converts a string to title case. +// This uses the English language rules for capitalization. +// +// Parameters: +// - s: The string to convert +// +// Returns: +// - string: The string in title case func title(s string) string { return cases.Title(language.English).String(s) } +// join concatenates the elements of a slice with a separator. +// The input is first converted to a string slice using strslice. +// +// Parameters: +// - sep: The separator to use between elements +// - v: The value to join (will be converted to a string slice) +// +// Returns: +// - string: The joined string func join(sep string, v any) string { return strings.Join(strslice(v), sep) } +// split splits a string by a separator and returns a map. +// The keys in the map are "_0", "_1", etc., corresponding to the position of each part. +// +// Parameters: +// - sep: The separator to split on +// - orig: The string to split +// +// Returns: +// - map[string]string: A map with keys "_0", "_1", etc. and values being the split parts func split(sep, orig string) map[string]string { parts := strings.Split(orig, sep) res := make(map[string]string, len(parts)) @@ -168,10 +329,30 @@ func split(sep, orig string) map[string]string { return res } +// splitList splits a string by a separator and returns a slice. +// This is a simple wrapper around strings.Split. +// +// Parameters: +// - sep: The separator to split on +// - orig: The string to split +// +// Returns: +// - []string: A slice containing the split parts func splitList(sep, orig string) []string { return strings.Split(orig, sep) } +// splitn splits a string by a separator with a limit and returns a map. +// The keys in the map are "_0", "_1", etc., corresponding to the position of each part. +// It will split the string into at most n parts. +// +// Parameters: +// - sep: The separator to split on +// - n: The maximum number of parts to return +// - orig: The string to split +// +// Returns: +// - map[string]string: A map with keys "_0", "_1", etc. and values being the split parts func splitn(sep string, n int, orig string) map[string]string { parts := strings.SplitN(orig, sep, n) res := make(map[string]string, len(parts)) @@ -182,12 +363,20 @@ func splitn(sep string, n int, orig string) map[string]string { } // substring creates a substring of the given string. +// It extracts a portion of a string based on start and end indices. // -// If start is < 0, this calls string[:end]. +// Parameters: +// - start: The starting index (inclusive) +// - end: The ending index (exclusive) +// - s: The source string // -// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] +// Behavior: +// - If start < 0, returns s[:end] +// - If start >= 0 and end < 0 or end > len(s), returns s[start:] +// - Otherwise, returns s[start:end] // -// Otherwise, this calls string[start, end]. +// Returns: +// - string: The extracted substring func substring(start, end int, s string) string { if start < 0 { return s[:end] @@ -198,6 +387,19 @@ func substring(start, end int, s string) string { return s[start:end] } +// repeat creates a new string by repeating the input string a specified number of times. +// It has safety limits to prevent excessive memory usage or infinite loops. +// +// Parameters: +// - count: The number of times to repeat the string +// - str: The string to repeat +// +// Returns: +// - string: The repeated string +// +// Panics: +// - If count exceeds loopExecutionLimit +// - If the resulting string length would exceed stringLengthLimit func repeat(count int, str string) string { if count > loopExecutionLimit { panic(fmt.Sprintf("repeat count %d exceeds limit of %d", count, loopExecutionLimit)) @@ -207,26 +409,79 @@ func repeat(count int, str string) string { return strings.Repeat(str, count) } +// trimAll removes all leading and trailing characters contained in the cutset. +// Note that the parameter order is reversed from the standard strings.Trim function. +// +// Parameters: +// - a: The cutset of characters to remove +// - b: The string to trim +// +// Returns: +// - string: The trimmed string func trimAll(a, b string) string { return strings.Trim(b, a) } +// trimPrefix removes the specified prefix from a string. +// If the string doesn't start with the prefix, it returns the original string. +// Note that the parameter order is reversed from the standard strings.TrimPrefix function. +// +// Parameters: +// - a: The prefix to remove +// - b: The string to trim +// +// Returns: +// - string: The string with the prefix removed, or the original string if it doesn't start with the prefix func trimPrefix(a, b string) string { return strings.TrimPrefix(b, a) } +// trimSuffix removes the specified suffix from a string. +// If the string doesn't end with the suffix, it returns the original string. +// Note that the parameter order is reversed from the standard strings.TrimSuffix function. +// +// Parameters: +// - a: The suffix to remove +// - b: The string to trim +// +// Returns: +// - string: The string with the suffix removed, or the original string if it doesn't end with the suffix func trimSuffix(a, b string) string { return strings.TrimSuffix(b, a) } +// contains checks if a string contains a substring. +// +// Parameters: +// - substr: The substring to search for +// - str: The string to search in +// +// Returns: +// - bool: True if str contains substr, false otherwise func contains(substr string, str string) bool { return strings.Contains(str, substr) } +// hasPrefix checks if a string starts with a specified prefix. +// +// Parameters: +// - substr: The prefix to check for +// - str: The string to check +// +// Returns: +// - bool: True if str starts with substr, false otherwise func hasPrefix(substr string, str string) bool { return strings.HasPrefix(str, substr) } +// hasSuffix checks if a string ends with a specified suffix. +// +// Parameters: +// - substr: The suffix to check for +// - str: The string to check +// +// Returns: +// - bool: True if str ends with substr, false otherwise func hasSuffix(substr string, str string) bool { return strings.HasSuffix(str, substr) } diff --git a/util/sprig/url.go b/util/sprig/url.go index 00826706..52dac3bb 100644 --- a/util/sprig/url.go +++ b/util/sprig/url.go @@ -60,7 +60,6 @@ func urlJoin(d map[string]any) string { } user = tempURL.User } - resURL.User = user return resURL.String() } From d87d8a2db4f15ff8e03f8b964daa2ddb8fd4c4da Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 21 Jul 2025 11:31:38 +0200 Subject: [PATCH 188/378] fmt --- util/sprig/dict.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/sprig/dict.go b/util/sprig/dict.go index 0a282add..4bb16d03 100644 --- a/util/sprig/dict.go +++ b/util/sprig/dict.go @@ -185,9 +185,9 @@ func values(dict map[string]any) []any { // // Parameters: // - ps: A variadic list where: -// - The first N-2 arguments are string keys forming the path -// - The second-to-last argument is the default value to return if the path doesn't exist -// - The last argument is the map to traverse +// - The first N-2 arguments are string keys forming the path +// - The second-to-last argument is the default value to return if the path doesn't exist +// - The last argument is the map to traverse // // Returns: // - any: The value found at the specified path, or the default value if not found From f298d947bd12c35cbe9eab5e81f044e465e34b93 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 21 Jul 2025 11:46:22 +0200 Subject: [PATCH 189/378] Bump --- go.mod | 30 +-- go.sum | 31 +++ server/smtp_server.go | 6 +- web/package-lock.json | 475 ++++++++++++++++++++++-------------------- 4 files changed, 296 insertions(+), 246 deletions(-) diff --git a/go.mod b/go.mod index dc35ae8b..4ecc680a 100644 --- a/go.mod +++ b/go.mod @@ -16,12 +16,12 @@ require ( github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.27.7 - golang.org/x/crypto v0.39.0 + golang.org/x/crypto v0.40.0 golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.15.0 - golang.org/x/term v0.32.0 + golang.org/x/sync v0.16.0 + golang.org/x/term v0.33.0 golang.org/x/time v0.12.0 - google.golang.org/api v0.240.0 + google.golang.org/api v0.242.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -30,18 +30,18 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi require github.com/pkg/errors v0.9.1 // indirect require ( - firebase.google.com/go/v4 v4.16.1 + firebase.google.com/go/v4 v4.17.0 github.com/SherClockHolmes/webpush-go v1.4.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.22.0 github.com/stripe/stripe-go/v74 v74.30.0 - golang.org/x/text v0.26.0 + golang.org/x/text v0.27.0 ) require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.3 // indirect - cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go v0.121.4 // indirect + cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect @@ -65,12 +65,12 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -92,12 +92,12 @@ require ( go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 18815b70..1f98da35 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo= cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc= +cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= +cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= @@ -24,6 +28,8 @@ cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4 cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= firebase.google.com/go/v4 v4.16.1 h1:Kl5cgXmM0VOWDGT1UAx6b0T2UFWa14ak0CvYqeI7Py4= firebase.google.com/go/v4 v4.16.1/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= +firebase.google.com/go/v4 v4.17.0 h1:Bih69QV/k0YKPA1qUX04ln0aPT9IERrAo2ezibcngzE= +firebase.google.com/go/v4 v4.17.0/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= @@ -83,6 +89,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -100,6 +108,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -186,6 +196,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -202,6 +214,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -213,6 +227,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -227,6 +243,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -238,6 +256,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -251,6 +271,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -263,14 +285,23 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8= +google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/server/smtp_server.go b/server/smtp_server.go index 6de42e37..ee28efc2 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -192,12 +192,12 @@ func (s *smtpSession) publishMessage(m *message) error { // Call HTTP handler with fake HTTP request url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic) req, err := http.NewRequest("POST", url, strings.NewReader(m.Message)) - req.RequestURI = "/" + m.Topic // just for the logs - req.RemoteAddr = remoteAddr // rate limiting!! - req.Header.Set(s.backend.config.ProxyForwardedHeader, remoteAddr) // Set X-Forwarded-For header if err != nil { return err } + req.RequestURI = "/" + m.Topic // just for the logs + req.RemoteAddr = remoteAddr // rate limiting!! + req.Header.Set(s.backend.config.ProxyForwardedHeader, remoteAddr) // Set X-Forwarded-For header if m.Title != "" { req.Header.Set("Title", m.Title) } diff --git a/web/package-lock.json b/web/package-lock.json index ea4962a4..28e5b6d3 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1261,9 +1261,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz", - "integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", + "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -1599,9 +1599,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1770,9 +1770,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -1787,9 +1787,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -1804,9 +1804,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -1821,9 +1821,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -1838,9 +1838,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -1855,9 +1855,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -1872,9 +1872,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -1889,9 +1889,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -1906,9 +1906,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -1923,9 +1923,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -1940,9 +1940,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -1957,9 +1957,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -1974,9 +1974,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -1991,9 +1991,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -2008,9 +2008,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -2025,9 +2025,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -2042,9 +2042,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -2059,9 +2059,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -2076,9 +2076,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -2093,9 +2093,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -2110,9 +2110,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -2126,10 +2126,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -2144,9 +2161,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -2161,9 +2178,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -2178,9 +2195,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -2354,9 +2371,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz", - "integrity": "sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz", + "integrity": "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==", "license": "MIT", "funding": { "type": "opencollective", @@ -2364,9 +2381,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.17.1.tgz", - "integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", + "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" @@ -2390,14 +2407,14 @@ } }, "node_modules/@mui/material": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", - "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", + "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.17.1", - "@mui/system": "^5.17.1", + "@mui/core-downloads-tracker": "^5.18.0", + "@mui/system": "^5.18.0", "@mui/types": "~7.2.15", "@mui/utils": "^5.17.1", "@popperjs/core": "^2.11.8", @@ -2462,13 +2479,14 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", - "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz", + "integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, @@ -2494,14 +2512,14 @@ } }, "node_modules/@mui/system": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", - "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz", + "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.17.1", - "@mui/styled-engine": "^5.16.14", + "@mui/styled-engine": "^5.18.0", "@mui/types": "~7.2.15", "@mui/utils": "^5.17.1", "clsx": "^2.1.0", @@ -2635,9 +2653,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", - "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, @@ -2713,9 +2731,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", - "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "cpu": [ "arm" ], @@ -2727,9 +2745,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", - "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "cpu": [ "arm64" ], @@ -2741,9 +2759,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", - "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "cpu": [ "arm64" ], @@ -2755,9 +2773,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", - "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "cpu": [ "x64" ], @@ -2769,9 +2787,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", - "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "cpu": [ "arm64" ], @@ -2783,9 +2801,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", - "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "cpu": [ "x64" ], @@ -2797,9 +2815,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", - "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "cpu": [ "arm" ], @@ -2811,9 +2829,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", - "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "cpu": [ "arm" ], @@ -2825,9 +2843,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", - "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "cpu": [ "arm64" ], @@ -2839,9 +2857,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", - "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "cpu": [ "arm64" ], @@ -2853,9 +2871,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", - "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "cpu": [ "loong64" ], @@ -2867,9 +2885,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", - "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "cpu": [ "ppc64" ], @@ -2881,9 +2899,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", - "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "cpu": [ "riscv64" ], @@ -2895,9 +2913,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", - "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "cpu": [ "riscv64" ], @@ -2909,9 +2927,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", - "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "cpu": [ "s390x" ], @@ -2923,9 +2941,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", - "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "cpu": [ "x64" ], @@ -2937,9 +2955,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", - "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "cpu": [ "x64" ], @@ -2951,9 +2969,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", - "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "cpu": [ "arm64" ], @@ -2965,9 +2983,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", - "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "cpu": [ "ia32" ], @@ -2979,9 +2997,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", - "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "cpu": [ "x64" ], @@ -3139,16 +3157,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", - "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", + "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.19", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -3156,7 +3174,7 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { @@ -4094,9 +4112,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.179", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", - "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", + "version": "1.5.187", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", "dev": true, "license": "ISC" }, @@ -4303,9 +4321,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4316,31 +4334,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -4465,9 +4484,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", "dev": true, "license": "MIT", "bin": { @@ -6839,9 +6858,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -7364,9 +7383,9 @@ } }, "node_modules/rollup": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", - "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", "dependencies": { @@ -7380,26 +7399,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.2", - "@rollup/rollup-android-arm64": "4.44.2", - "@rollup/rollup-darwin-arm64": "4.44.2", - "@rollup/rollup-darwin-x64": "4.44.2", - "@rollup/rollup-freebsd-arm64": "4.44.2", - "@rollup/rollup-freebsd-x64": "4.44.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", - "@rollup/rollup-linux-arm-musleabihf": "4.44.2", - "@rollup/rollup-linux-arm64-gnu": "4.44.2", - "@rollup/rollup-linux-arm64-musl": "4.44.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-musl": "4.44.2", - "@rollup/rollup-linux-s390x-gnu": "4.44.2", - "@rollup/rollup-linux-x64-gnu": "4.44.2", - "@rollup/rollup-linux-x64-musl": "4.44.2", - "@rollup/rollup-win32-arm64-msvc": "4.44.2", - "@rollup/rollup-win32-ia32-msvc": "4.44.2", - "@rollup/rollup-win32-x64-msvc": "4.44.2", + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" } }, From f59df0f40ada3221a1857c665c9c82e0fdf63d2e Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 21 Jul 2025 17:44:00 +0200 Subject: [PATCH 190/378] Works --- Makefile | 2 +- cmd/access.go | 30 ++++-- cmd/serve.go | 109 ++++++++++++++----- cmd/user.go | 3 +- go.sum | 31 ------ server/message_cache.go | 7 ++ server/server.go | 5 +- server/server.yml | 6 ++ server/server_admin.go | 2 +- user/manager.go | 230 +++++++++++++++++++++++++++++++--------- user/manager_test.go | 10 +- user/types.go | 26 ++--- util/util.go | 12 +++ 13 files changed, 333 insertions(+), 140 deletions(-) diff --git a/Makefile b/Makefile index 575bb788..df131c7a 100644 --- a/Makefile +++ b/Makefile @@ -232,7 +232,7 @@ cli-deps-update: go get -u go install honnef.co/go/tools/cmd/staticcheck@latest go install golang.org/x/lint/golint@latest - go install github.com/goreleaser/goreleaser@latest + go install github.com/goreleaser/goreleaser/v2@latest cli-build-results: cat dist/config.yaml diff --git a/cmd/access.go b/cmd/access.go index c6be94b5..10247b5f 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -105,8 +105,10 @@ func changeAccess(c *cli.Context, manager *user.Manager, username string, topic return err } u, err := manager.User(username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) + } else if err != nil { + return err } else if u.Role == user.RoleAdmin { return fmt.Errorf("user %s is an admin user, access control entries have no effect", username) } @@ -175,7 +177,7 @@ func showAllAccess(c *cli.Context, manager *user.Manager) error { func showUserAccess(c *cli.Context, manager *user.Manager, username string) error { users, err := manager.User(username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } else if err != nil { return err @@ -193,19 +195,27 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error if u.Tier != nil { tier = u.Tier.Name } - fmt.Fprintf(c.App.ErrWriter, "user %s (role: %s, tier: %s)\n", u.Name, u.Role, tier) + provisioned := "" + if u.Provisioned { + provisioned = ", provisioned user" + } + fmt.Fprintf(c.App.ErrWriter, "user %s (role: %s, tier: %s%s)\n", u.Name, u.Role, tier, provisioned) if u.Role == user.RoleAdmin { fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n") } else if len(grants) > 0 { for _, grant := range grants { - if grant.Allow.IsReadWrite() { - fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern) - } else if grant.Allow.IsRead() { - fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern) - } else if grant.Allow.IsWrite() { - fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern) + grantProvisioned := "" + if grant.Provisioned { + grantProvisioned = ", provisioned access entry" + } + if grant.Permission.IsReadWrite() { + fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s%s\n", grant.TopicPattern, grantProvisioned) + } else if grant.Permission.IsRead() { + fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned) + } else if grant.Permission.IsWrite() { + fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned) } else { - fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern) + fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s%s\n", grant.TopicPattern, grantProvisioned) } } } else { diff --git a/cmd/serve.go b/cmd/serve.go index 50314b88..ef37ee6f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -48,7 +48,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), - altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provisioned-users", Aliases: []string{"auth_provisioned_users"}, EnvVars: []string{"NTFY_AUTH_PROVISIONED_USERS"}, Usage: "pre-provisioned declarative users"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provision-users", Aliases: []string{"auth_provision_users"}, EnvVars: []string{"NTFY_AUTH_PROVISION_USERS"}, Usage: "pre-provisioned declarative users"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provision-access", Aliases: []string{"auth_provision_access"}, EnvVars: []string{"NTFY_AUTH_PROVISION_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), @@ -155,8 +156,8 @@ func execServe(c *cli.Context) error { authFile := c.String("auth-file") authStartupQueries := c.String("auth-startup-queries") authDefaultAccess := c.String("auth-default-access") - authProvisionedUsersRaw := c.StringSlice("auth-provisioned-users") - //authProvisionedAccessRaw := c.StringSlice("auth-provisioned-access") + authProvisionUsersRaw := c.StringSlice("auth-provision-users") + authProvisionAccessRaw := c.StringSlice("auth-provision-access") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -352,27 +353,13 @@ func execServe(c *cli.Context) error { if err != nil { return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'") } - authProvisionedUsers := make([]*user.User, 0) - for _, userLine := range authProvisionedUsersRaw { - parts := strings.Split(userLine, ":") - if len(parts) != 3 { - return fmt.Errorf("invalid provisioned user %s, expected format: 'name:hash:role'", userLine) - } - username := strings.TrimSpace(parts[0]) - passwordHash := strings.TrimSpace(parts[1]) - role := user.Role(strings.TrimSpace(parts[2])) - if !user.AllowedUsername(username) { - return fmt.Errorf("invalid provisioned user %s, username invalid", userLine) - } else if passwordHash == "" { - return fmt.Errorf("invalid provisioned user %s, password hash cannot be empty", userLine) - } else if !user.AllowedRole(role) { - return fmt.Errorf("invalid provisioned user %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) - } - authProvisionedUsers = append(authProvisionedUsers, &user.User{ - Name: username, - Hash: passwordHash, - Role: role, - }) + authProvisionUsers, err := parseProvisionUsers(authProvisionUsersRaw) + if err != nil { + return err + } + authProvisionAccess, err := parseProvisionAccess(authProvisionUsers, authProvisionAccessRaw) + if err != nil { + return err } // Special case: Unset default @@ -429,8 +416,8 @@ func execServe(c *cli.Context) error { conf.AuthFile = authFile conf.AuthStartupQueries = authStartupQueries conf.AuthDefault = authDefault - conf.AuthProvisionedUsers = authProvisionedUsers - conf.AuthProvisionedAccess = nil // FIXME + conf.AuthProvisionedUsers = authProvisionUsers + conf.AuthProvisionedAccess = authProvisionAccess conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit @@ -544,6 +531,76 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) { return } +func parseProvisionUsers(usersRaw []string) ([]*user.User, error) { + provisionUsers := make([]*user.User, 0) + for _, userLine := range usersRaw { + parts := strings.Split(userLine, ":") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid auth-provision-users: %s, expected format: 'name:hash:role'", userLine) + } + username := strings.TrimSpace(parts[0]) + passwordHash := strings.TrimSpace(parts[1]) + role := user.Role(strings.TrimSpace(parts[2])) + if !user.AllowedUsername(username) { + return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine) + } else if passwordHash == "" { + return nil, fmt.Errorf("invalid auth-provision-users: %s, password hash cannot be empty", userLine) + } else if !user.AllowedRole(role) { + return nil, fmt.Errorf("invalid auth-provision-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) + } + provisionUsers = append(provisionUsers, &user.User{ + Name: username, + Hash: passwordHash, + Role: role, + Provisioned: true, + }) + } + return provisionUsers, nil +} + +func parseProvisionAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[string][]*user.Grant, error) { + access := make(map[string][]*user.Grant) + for _, accessLine := range provisionAccessRaw { + parts := strings.Split(accessLine, ":") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid auth-provision-access: %s, expected format: 'user:topic:permission'", accessLine) + } + username := strings.TrimSpace(parts[0]) + if username == userEveryone { + username = user.Everyone + } + provisionUser, exists := util.Find(provisionUsers, func(u *user.User) bool { + return u.Name == username + }) + if username != user.Everyone { + if !exists { + return nil, fmt.Errorf("invalid auth-provision-access: %s, user %s is not provisioned", accessLine, username) + } else if !user.AllowedUsername(username) { + return nil, fmt.Errorf("invalid auth-provision-access: %s, username %s invalid", accessLine, username) + } else if provisionUser.Role != user.RoleUser { + return nil, fmt.Errorf("invalid auth-provision-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username) + } + } + topic := strings.TrimSpace(parts[1]) + if !user.AllowedTopicPattern(topic) { + return nil, fmt.Errorf("invalid auth-provision-access: %s, topic pattern %s invalid", accessLine, topic) + } + permission, err := user.ParsePermission(strings.TrimSpace(parts[2])) + if err != nil { + return nil, fmt.Errorf("invalid auth-provision-access: %s, permission %s invalid, %s", accessLine, parts[2], err.Error()) + } + if _, exists := access[username]; !exists { + access[username] = make([]*user.Grant, 0) + } + access[username] = append(access[username], &user.Grant{ + TopicPattern: topic, + Permission: permission, + Provisioned: true, + }) + } + return access, nil +} + func reloadLogLevel(inputSource altsrc.InputSourceContext) error { newLevelStr, err := inputSource.String("log-level") if err != nil { diff --git a/cmd/user.go b/cmd/user.go index 31f4c31b..0a6e24a1 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -349,8 +349,7 @@ func createUserManager(c *cli.Context) (*user.Manager, error) { Filename: authFile, StartupQueries: authStartupQueries, DefaultAccess: authDefault, - ProvisionedUsers: nil, //FIXME - ProvisionedAccess: nil, //FIXME + ProvisionEnabled: false, // Do not re-provision users on manager initialization BcryptCost: user.DefaultUserPasswordBcryptCost, QueueWriterInterval: user.DefaultUserStatsQueueWriterInterval, } diff --git a/go.sum b/go.sum index 1f98da35..575b5c22 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo= -cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc= cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -26,8 +22,6 @@ cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2zn cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -firebase.google.com/go/v4 v4.16.1 h1:Kl5cgXmM0VOWDGT1UAx6b0T2UFWa14ak0CvYqeI7Py4= -firebase.google.com/go/v4 v4.16.1/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= firebase.google.com/go/v4 v4.17.0 h1:Bih69QV/k0YKPA1qUX04ln0aPT9IERrAo2ezibcngzE= firebase.google.com/go/v4 v4.17.0/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= @@ -87,8 +81,6 @@ github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -106,8 +98,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -194,8 +184,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -212,8 +200,6 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -225,8 +211,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -241,8 +225,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= @@ -254,8 +236,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -269,8 +249,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -283,23 +261,14 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= -google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8= google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10= google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= diff --git a/server/message_cache.go b/server/message_cache.go index e314ace3..03cb4969 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/netip" + "path/filepath" "strings" "time" @@ -286,6 +287,12 @@ type messageCache struct { // newSqliteCache creates a SQLite file-backed cache func newSqliteCache(filename, startupQueries string, cacheDuration time.Duration, batchSize int, batchTimeout time.Duration, nop bool) (*messageCache, error) { + // Check the parent directory of the database file (makes for friendly error messages) + parentDir := filepath.Dir(filename) + if !util.FileExists(parentDir) { + return nil, fmt.Errorf("cache database directory %s does not exist or is not accessible", parentDir) + } + // Open database db, err := sql.Open("sqlite3", filename) if err != nil { return nil, err diff --git a/server/server.go b/server/server.go index d585faa0..d3ef9cbb 100644 --- a/server/server.go +++ b/server/server.go @@ -200,8 +200,9 @@ func New(conf *Config) (*Server, error) { Filename: conf.AuthFile, StartupQueries: conf.AuthStartupQueries, DefaultAccess: conf.AuthDefault, - ProvisionedUsers: conf.AuthProvisionedUsers, - ProvisionedAccess: conf.AuthProvisionedAccess, + ProvisionEnabled: true, // Enable provisioning of users and access + ProvisionUsers: conf.AuthProvisionedUsers, + ProvisionAccess: conf.AuthProvisionedAccess, BcryptCost: conf.AuthBcryptCost, QueueWriterInterval: conf.AuthStatsQueueWriterInterval, } diff --git a/server/server.yml b/server/server.yml index db968498..02af7383 100644 --- a/server/server.yml +++ b/server/server.yml @@ -82,6 +82,10 @@ # set to "read-write" (default), "read-only", "write-only" or "deny-all". # - auth-startup-queries allows you to run commands when the database is initialized, e.g. to enable # WAL mode. This is similar to cache-startup-queries. See above for details. +# - auth-provision-users is a list of users that are automatically created when the server starts. +# Each entry is in the format "::", e.g. "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user" +# - auth-provision-access is a list of access control entries that are automatically created when the server starts. +# Each entry is in the format "::", e.g. "phil:mytopic:rw" or "phil:phil-*:rw". # # Debian/RPM package users: # Use /var/lib/ntfy/user.db as user database to avoid permission issues. The package @@ -94,6 +98,8 @@ # auth-file: # auth-default-access: "read-write" # auth-startup-queries: +# auth-provision-users: +# auth-provision-access: # If set, the X-Forwarded-For header (or whatever is configured in proxy-forwarded-header) is used to determine # the visitor IP address instead of the remote address of the connection. diff --git a/server/server_admin.go b/server/server_admin.go index eb362956..b724d4b7 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -25,7 +25,7 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit for i, g := range grants[u.ID] { userGrants[i] = &apiUserGrantResponse{ Topic: g.TopicPattern, - Permission: g.Allow.String(), + Permission: g.Permission.String(), } } usersResponse[i] = &apiUserResponse{ diff --git a/user/manager.go b/user/manager.go index 8932f34a..f2f4875d 100644 --- a/user/manager.go +++ b/user/manager.go @@ -12,6 +12,7 @@ import ( "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/util" "net/netip" + "path/filepath" "strings" "sync" "time" @@ -75,6 +76,7 @@ const ( role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL, prefs JSON NOT NULL DEFAULT '{}', sync_topic TEXT NOT NULL, + provisioned INT NOT NULL, stats_messages INT NOT NULL DEFAULT (0), stats_emails INT NOT NULL DEFAULT (0), stats_calls INT NOT NULL DEFAULT (0), @@ -97,6 +99,7 @@ const ( read INT NOT NULL, write INT NOT NULL, owner_user_id INT, + provisioned INT NOT NULL, PRIMARY KEY (user_id, topic), FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE @@ -121,8 +124,8 @@ const ( id INT PRIMARY KEY, version INT NOT NULL ); - INSERT INTO user (id, user, pass, role, sync_topic, created) - VALUES ('` + everyoneID + `', '*', '', 'anonymous', '', UNIXEPOCH()) + INSERT INTO user (id, user, pass, role, sync_topic, provisioned, created) + VALUES ('` + everyoneID + `', '*', '', 'anonymous', '', false, UNIXEPOCH()) ON CONFLICT (id) DO NOTHING; COMMIT; ` @@ -132,26 +135,26 @@ const ( ` selectUserByIDQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE u.id = ? ` selectUserByNameQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE user = ? ` selectUserByTokenQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u JOIN user_token tk on u.id = tk.user_id LEFT JOIN tier t on t.id = u.tier_id WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?) ` selectUserByStripeCustomerIDQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE u.stripe_customer_id = ? @@ -165,8 +168,8 @@ const ( ` insertUserQuery = ` - INSERT INTO user (id, user, pass, role, sync_topic, created) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO user (id, user, pass, role, sync_topic, provisioned, created) + VALUES (?, ?, ?, ?, ?, ?, ?) ` selectUsernamesQuery = ` SELECT user @@ -189,18 +192,18 @@ const ( deleteUserQuery = `DELETE FROM user WHERE user = ?` upsertUserAccessQuery = ` - INSERT INTO user_access (user_id, topic, read, write, owner_user_id) - VALUES ((SELECT id FROM user WHERE user = ?), ?, ?, ?, (SELECT IIF(?='',NULL,(SELECT id FROM user WHERE user=?)))) + INSERT INTO user_access (user_id, topic, read, write, owner_user_id, provisioned) + VALUES ((SELECT id FROM user WHERE user = ?), ?, ?, ?, (SELECT IIF(?='',NULL,(SELECT id FROM user WHERE user=?))), ?) ON CONFLICT (user_id, topic) - DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id + DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id, provisioned=excluded.provisioned ` selectUserAllAccessQuery = ` - SELECT user_id, topic, read, write + SELECT user_id, topic, read, write, provisioned FROM user_access ORDER BY LENGTH(topic) DESC, write DESC, read DESC, topic ` selectUserAccessQuery = ` - SELECT topic, read, write + SELECT topic, read, write, provisioned FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) ORDER BY LENGTH(topic) DESC, write DESC, read DESC, topic @@ -244,7 +247,8 @@ const ( WHERE user_id = (SELECT id FROM user WHERE user = ?) OR owner_user_id = (SELECT id FROM user WHERE user = ?) ` - deleteTopicAccessQuery = ` + deleteUserAccessProvisionedQuery = `DELETE FROM user_access WHERE provisioned = 1` + deleteTopicAccessQuery = ` DELETE FROM user_access WHERE (user_id = (SELECT id FROM user WHERE user = ?) OR owner_user_id = (SELECT id FROM user WHERE user = ?)) AND topic = ? @@ -427,6 +431,15 @@ const ( migrate4To5UpdateQueries = ` UPDATE user_access SET topic = REPLACE(topic, '_', '\_'); ` + + // 5 -> 6 + migrate5To6UpdateQueries = ` + ALTER TABLE user ADD COLUMN provisioned INT NOT NULL DEFAULT (0); + ALTER TABLE user ALTER COLUMN provisioned DROP DEFAULT; + + ALTER TABLE user_access ADD COLUMN provisioned INT NOT NULL DEFAULT (0); + ALTER TABLE user_access ALTER COLUMN provisioned DROP DEFAULT; + ` ) var ( @@ -435,6 +448,7 @@ var ( 2: migrateFrom2, 3: migrateFrom3, 4: migrateFrom4, + 5: migrateFrom5, } ) @@ -452,8 +466,9 @@ type Config struct { Filename string // Database filename, e.g. "/var/lib/ntfy/user.db" StartupQueries string // Queries to run on startup, e.g. to create initial users or tiers DefaultAccess Permission // Default permission if no ACL matches - ProvisionedUsers []*User // Predefined users to create on startup - ProvisionedAccess map[string][]*Grant // Predefined access grants to create on startup + ProvisionEnabled bool // Enable auto-provisioning of users and access grants + ProvisionUsers []*User // Predefined users to create on startup + ProvisionAccess map[string][]*Grant // Predefined access grants to create on startup QueueWriterInterval time.Duration // Interval for the async queue writer to flush stats and token updates to the database BcryptCost int // Cost of generated passwords; lowering makes testing faster } @@ -469,6 +484,11 @@ func NewManager(config *Config) (*Manager, error) { if config.QueueWriterInterval.Seconds() <= 0 { config.QueueWriterInterval = DefaultUserStatsQueueWriterInterval } + // Check the parent directory of the database file (makes for friendly error messages) + parentDir := filepath.Dir(config.Filename) + if !util.FileExists(parentDir) { + return nil, fmt.Errorf("user database directory %s does not exist or is not accessible", parentDir) + } // Open DB and run setup queries db, err := sql.Open("sqlite3", config.Filename) if err != nil { @@ -486,7 +506,7 @@ func NewManager(config *Config) (*Manager, error) { statsQueue: make(map[string]*Stats), tokenQueue: make(map[string]*TokenUpdate), } - if err := manager.provisionUsers(); err != nil { + if err := manager.maybeProvisionUsersAndAccess(); err != nil { return nil, err } go manager.asyncQueueWriter(config.QueueWriterInterval) @@ -586,7 +606,7 @@ func (a *Manager) Tokens(userID string) ([]*Token, error) { tokens := make([]*Token, 0) for { token, err := a.readToken(rows) - if err == ErrTokenNotFound { + if errors.Is(err, ErrTokenNotFound) { break } else if err != nil { return nil, err @@ -884,6 +904,13 @@ func (a *Manager) resolvePerms(base, perm Permission) error { // AddUser adds a user with the given username, password and role func (a *Manager) AddUser(username, password string, role Role, hashed bool) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.addUserTx(tx, username, password, role, hashed, false) + }) +} + +// AddUser adds a user with the given username, password and role +func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, hashed, provisioned bool) error { if !AllowedUsername(username) || !AllowedRole(role) { return ErrInvalidArgument } @@ -899,8 +926,8 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err } userID := util.RandomStringPrefix(userIDPrefix, userIDLength) syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() - if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil { - if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + if _, err = tx.Exec(insertUserQuery, userID, username, hash, role, syncTopic, provisioned, now); err != nil { + if errors.Is(err, sqlite3.ErrConstraintUnique) { return ErrUserExists } return err @@ -911,11 +938,17 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err // RemoveUser deletes the user with the given username. The function returns nil on success, even // if the user did not exist in the first place. func (a *Manager) RemoveUser(username string) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.removeUserTx(tx, username) + }) +} + +func (a *Manager) removeUserTx(tx *sql.Tx, username string) error { if !AllowedUsername(username) { return ErrInvalidArgument } // Rows in user_access, user_token, etc. are deleted via foreign keys - if _, err := a.db.Exec(deleteUserQuery, username); err != nil { + if _, err := tx.Exec(deleteUserQuery, username); err != nil { return err } return nil @@ -1029,24 +1062,26 @@ func (a *Manager) userByToken(token string) (*User, error) { func (a *Manager) readUser(rows *sql.Rows) (*User, error) { defer rows.Close() var id, username, hash, role, prefs, syncTopic string + var provisioned bool var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString var messages, emails, calls int64 var messagesLimit, messagesExpiryDuration, emailsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64 if !rows.Next() { return nil, ErrUserNotFound } - if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { + if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &provisioned, &messages, &emails, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err } user := &User{ - ID: id, - Name: username, - Hash: hash, - Role: Role(role), - Prefs: &Prefs{}, - SyncTopic: syncTopic, + ID: id, + Name: username, + Hash: hash, + Role: Role(role), + Prefs: &Prefs{}, + SyncTopic: syncTopic, + Provisioned: provisioned, Stats: &Stats{ Messages: messages, Emails: emails, @@ -1097,8 +1132,8 @@ func (a *Manager) AllGrants() (map[string][]Grant, error) { grants := make(map[string][]Grant, 0) for rows.Next() { var userID, topic string - var read, write bool - if err := rows.Scan(&userID, &topic, &read, &write); err != nil { + var read, write, provisioned bool + if err := rows.Scan(&userID, &topic, &read, &write, &provisioned); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -1108,7 +1143,8 @@ func (a *Manager) AllGrants() (map[string][]Grant, error) { } grants[userID] = append(grants[userID], Grant{ TopicPattern: fromSQLWildcard(topic), - Allow: NewPermission(read, write), + Permission: NewPermission(read, write), + Provisioned: provisioned, }) } return grants, nil @@ -1124,15 +1160,16 @@ func (a *Manager) Grants(username string) ([]Grant, error) { grants := make([]Grant, 0) for rows.Next() { var topic string - var read, write bool - if err := rows.Scan(&topic, &read, &write); err != nil { + var read, write, provisioned bool + if err := rows.Scan(&topic, &read, &write, &provisioned); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err } grants = append(grants, Grant{ TopicPattern: fromSQLWildcard(topic), - Allow: NewPermission(read, write), + Permission: NewPermission(read, write), + Provisioned: provisioned, }) } return grants, nil @@ -1218,9 +1255,14 @@ func (a *Manager) ReservationOwner(topic string) (string, error) { // ChangePassword changes a user's password func (a *Manager) ChangePassword(username, password string, hashed bool) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.changePasswordTx(tx, username, password, hashed) + }) +} + +func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error { var hash []byte var err error - if hashed { hash = []byte(password) } else { @@ -1229,7 +1271,7 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error { return err } } - if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil { + if _, err := tx.Exec(updateUserPassQuery, hash, username); err != nil { return err } return nil @@ -1238,14 +1280,20 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error { // ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin, // all existing access control entries (Grant) are removed, since they are no longer needed. func (a *Manager) ChangeRole(username string, role Role) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.changeRoleTx(tx, username, role) + }) +} + +func (a *Manager) changeRoleTx(tx *sql.Tx, username string, role Role) error { if !AllowedUsername(username) || !AllowedRole(role) { return ErrInvalidArgument } - if _, err := a.db.Exec(updateUserRoleQuery, string(role), username); err != nil { + if _, err := tx.Exec(updateUserRoleQuery, string(role), username); err != nil { return err } if role == RoleAdmin { - if _, err := a.db.Exec(deleteUserAccessQuery, username, username); err != nil { + if _, err := tx.Exec(deleteUserAccessQuery, username, username); err != nil { return err } } @@ -1325,13 +1373,19 @@ func (a *Manager) AllowReservation(username string, topic string) error { // read/write access to a topic. The parameter topicPattern may include wildcards (*). The ACL entry // owner may either be a user (username), or the system (empty). func (a *Manager) AllowAccess(username string, topicPattern string, permission Permission) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.allowAccessTx(tx, username, topicPattern, permission, false) + }) +} + +func (a *Manager) allowAccessTx(tx *sql.Tx, username string, topicPattern string, permission Permission, provisioned bool) error { if !AllowedUsername(username) && username != Everyone { return ErrInvalidArgument } else if !AllowedTopicPattern(topicPattern) { return ErrInvalidArgument } owner := "" - if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), permission.IsRead(), permission.IsWrite(), owner, owner); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), permission.IsRead(), permission.IsWrite(), owner, owner, provisioned); err != nil { return err } return nil @@ -1524,20 +1578,65 @@ func (a *Manager) Close() error { return a.db.Close() } -func (a *Manager) provisionUsers() error { - for _, user := range a.config.ProvisionedUsers { - if err := a.AddUser(user.Name, user.Hash, user.Role, true); err != nil && !errors.Is(err, ErrUserExists) { - return err - } +func (a *Manager) maybeProvisionUsersAndAccess() error { + if !a.config.ProvisionEnabled { + return nil } - for username, grants := range a.config.ProvisionedAccess { - for _, grant := range grants { - if err := a.AllowAccess(username, grant.TopicPattern, grant.Allow); err != nil { - return err + users, err := a.Users() + if err != nil { + return err + } + provisionUsernames := util.Map(a.config.ProvisionUsers, func(u *User) string { + return u.Name + }) + return execTx(a.db, func(tx *sql.Tx) error { + // Remove users that are provisioned, but not in the config anymore + for _, user := range users { + if user.Name == Everyone { + continue + } else if user.Provisioned && !util.Contains(provisionUsernames, user.Name) { + log.Tag(tag).Info("Removing previously provisioned user %s", user.Name) + if err := a.removeUserTx(tx, user.Name); err != nil { + return fmt.Errorf("failed to remove provisioned user %s: %v", user.Name, err) + } } } - } - return nil + // Add or update provisioned users + for _, user := range a.config.ProvisionUsers { + if user.Name == Everyone { + continue + } + existingUser, exists := util.Find(users, func(u *User) bool { + return u.Name == user.Name + }) + if !exists { + log.Tag(tag).Info("Adding provisioned user %s", user.Name) + if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) { + return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err) + } + } else if existingUser.Hash != user.Hash || existingUser.Role != user.Role { + log.Tag(tag).Info("Updating provisioned user %s", user.Name) + if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil { + return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err) + } + if err := a.changeRoleTx(tx, user.Name, user.Role); err != nil { + return fmt.Errorf("failed to change role for provisioned user %s: %v", user.Name, err) + } + } + } + // Remove and (re-)add provisioned grants + if _, err := tx.Exec(deleteUserAccessProvisionedQuery); err != nil { + return err + } + for username, grants := range a.config.ProvisionAccess { + for _, grant := range grants { + if err := a.allowAccessTx(tx, username, grant.TopicPattern, grant.Permission, true); err != nil { + return err + } + } + } + return nil + }) } // toSQLWildcard converts a wildcard string to a SQL wildcard string. It only allows '*' as wildcards, @@ -1711,6 +1810,22 @@ func migrateFrom4(db *sql.DB) error { return tx.Commit() } +func migrateFrom5(db *sql.DB) error { + log.Tag(tag).Info("Migrating user database schema: from 5 to 6") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate5To6UpdateQueries); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 6); err != nil { + return err + } + return tx.Commit() +} + func nullString(s string) sql.NullString { if s == "" { return sql.NullString{} @@ -1724,3 +1839,18 @@ func nullInt64(v int64) sql.NullInt64 { } return sql.NullInt64{Int64: v, Valid: true} } + +// execTx executes a function in a transaction. If the function returns an error, the transaction is rolled back. +func execTx(db *sql.DB, f func(tx *sql.Tx) error) error { + tx, err := db.Begin() + if err != nil { + return err + } + if err := f(tx); err != nil { + if e := tx.Rollback(); e != nil { + return err + } + return err + } + return tx.Commit() +} diff --git a/user/manager_test.go b/user/manager_test.go index b57c762c..42def63f 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -489,12 +489,12 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) { benGrants, err := a.Grants("ben") require.Nil(t, err) require.Equal(t, 1, len(benGrants)) - require.Equal(t, PermissionReadWrite, benGrants[0].Allow) + require.Equal(t, PermissionReadWrite, benGrants[0].Permission) everyoneGrants, err := a.Grants(Everyone) require.Nil(t, err) require.Equal(t, 1, len(everyoneGrants)) - require.Equal(t, PermissionDenyAll, everyoneGrants[0].Allow) + require.Equal(t, PermissionDenyAll, everyoneGrants[0].Permission) benReservations, err := a.Reservations("ben") require.Nil(t, err) @@ -1201,16 +1201,16 @@ func TestMigrationFrom1(t *testing.T) { require.NotEqual(t, ben.SyncTopic, phil.SyncTopic) require.Equal(t, 2, len(benGrants)) require.Equal(t, "secret", benGrants[0].TopicPattern) - require.Equal(t, PermissionRead, benGrants[0].Allow) + require.Equal(t, PermissionRead, benGrants[0].Permission) require.Equal(t, "stats", benGrants[1].TopicPattern) - require.Equal(t, PermissionReadWrite, benGrants[1].Allow) + require.Equal(t, PermissionReadWrite, benGrants[1].Permission) require.Equal(t, "u_everyone", everyone.ID) require.Equal(t, Everyone, everyone.Name) require.Equal(t, RoleAnonymous, everyone.Role) require.Equal(t, 1, len(everyoneGrants)) require.Equal(t, "stats", everyoneGrants[0].TopicPattern) - require.Equal(t, PermissionRead, everyoneGrants[0].Allow) + require.Equal(t, PermissionRead, everyoneGrants[0].Permission) } func TestMigrationFrom4(t *testing.T) { diff --git a/user/types.go b/user/types.go index 6f6b1f69..90eeefce 100644 --- a/user/types.go +++ b/user/types.go @@ -12,17 +12,18 @@ import ( // User is a struct that represents a user type User struct { - ID string - Name string - Hash string // password hash (bcrypt) - Token string // Only set if token was used to log in - Role Role - Prefs *Prefs - Tier *Tier - Stats *Stats - Billing *Billing - SyncTopic string - Deleted bool + ID string + Name string + Hash string // Password hash (bcrypt) + Token string // Only set if token was used to log in + Role Role + Prefs *Prefs + Tier *Tier + Stats *Stats + Billing *Billing + SyncTopic string + Provisioned bool // Whether the user was provisioned by the config file + Deleted bool // Whether the user was soft-deleted } // TierID returns the ID of the User.Tier, or an empty string if the user has no tier, @@ -148,7 +149,8 @@ type Billing struct { // Grant is a struct that represents an access control entry to a topic by a user type Grant struct { TopicPattern string // May include wildcard (*) - Allow Permission + Permission Permission + Provisioned bool // Whether the grant was provisioned by the config file } // Reservation is a struct that represents the ownership over a topic by a user diff --git a/util/util.go b/util/util.go index 73b227af..3648e3a4 100644 --- a/util/util.go +++ b/util/util.go @@ -120,6 +120,18 @@ func Filter[T any](slice []T, f func(T) bool) []T { return result } +// Find returns the first element in the slice that satisfies the given function, and a boolean indicating +// whether such an element was found. If no element is found, it returns the zero value of T and false. +func Find[T any](slice []T, f func(T) bool) (T, bool) { + for _, v := range slice { + if f(v) { + return v, true + } + } + var zero T + return zero, false +} + // RandomString returns a random string with a given length func RandomString(length int) string { return RandomStringPrefix("", length) From ef275ac0c189fdf2191717d3f9fa7951cb1041ce Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 22 Jul 2025 11:54:06 +0200 Subject: [PATCH 191/378] Add Ntfy Desktop to integrations --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index 23c5f9e9..01c415d5 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -96,6 +96,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Ntfy_CSV_Reminders](https://github.com/thiswillbeyourgithub/Ntfy_CSV_Reminders) - A Python tool that sends random-timing phone notifications for recurring tasks by using daily probability checks based on CSV-defined frequencies. - [Daily Fact Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - Generate [llm](https://github.com/simonw/llm) generated fact every day about any topic you're interested in. - [ntfyexec](https://github.com/alecthomas/ntfyexec) - Send a notification through ntfy.sh if a command fails +- [Ntfy Desktop](https://github.com/emmaexe/ntfyDesktop) - Fully featured desktop client for Linux, built with Qt and C++. ## Projects + scripts From 4eb7dc563ccef18af40f6fc0d3ec6df8c93976b1 Mon Sep 17 00:00:00 2001 From: Daniel Krol Date: Tue, 22 Jul 2025 18:48:46 -0400 Subject: [PATCH 192/378] Add Ntfy for Sandstorm to integrations.md --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index 01c415d5..8dbca015 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -176,6 +176,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) - [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell) - [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. +- [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform ## Blog + forum posts From 4457e9e26ff82d290ff82e8e8446ee5066be09db Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 26 Jul 2025 11:16:33 +0200 Subject: [PATCH 193/378] Migration --- user/manager.go | 81 ++++++++++++++++++++++++++--- user/manager_test.go | 119 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 173 insertions(+), 27 deletions(-) diff --git a/user/manager.go b/user/manager.go index f2f4875d..09db145e 100644 --- a/user/manager.go +++ b/user/manager.go @@ -316,7 +316,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 5 + currentSchemaVersion = 6 insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)` updateSchemaVersion = `UPDATE schemaVersion SET version = ? WHERE id = 1` selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` @@ -434,11 +434,78 @@ const ( // 5 -> 6 migrate5To6UpdateQueries = ` - ALTER TABLE user ADD COLUMN provisioned INT NOT NULL DEFAULT (0); - ALTER TABLE user ALTER COLUMN provisioned DROP DEFAULT; + PRAGMA foreign_keys=off; - ALTER TABLE user_access ADD COLUMN provisioned INT NOT NULL DEFAULT (0); - ALTER TABLE user_access ALTER COLUMN provisioned DROP DEFAULT; + -- Alter user table: Add provisioned column + ALTER TABLE user RENAME TO user_old; + CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + tier_id TEXT, + user TEXT NOT NULL, + pass TEXT NOT NULL, + role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL, + prefs JSON NOT NULL DEFAULT '{}', + sync_topic TEXT NOT NULL, + provisioned INT NOT NULL, + stats_messages INT NOT NULL DEFAULT (0), + stats_emails INT NOT NULL DEFAULT (0), + stats_calls INT NOT NULL DEFAULT (0), + stripe_customer_id TEXT, + stripe_subscription_id TEXT, + stripe_subscription_status TEXT, + stripe_subscription_interval TEXT, + stripe_subscription_paid_until INT, + stripe_subscription_cancel_at INT, + created INT NOT NULL, + deleted INT, + FOREIGN KEY (tier_id) REFERENCES tier (id) + ); + INSERT INTO user + SELECT + id, + tier_id, + user, + pass, + role, + prefs, + sync_topic, + 0, + stats_messages, + stats_emails, + stats_calls, + stripe_customer_id, + stripe_subscription_id, + stripe_subscription_status, + stripe_subscription_interval, + stripe_subscription_paid_until, + stripe_subscription_cancel_at, + created, deleted + FROM user_old; + DROP TABLE user_old; + + -- Alter user_access table: Add provisioned column + ALTER TABLE user_access RENAME TO user_access_old; + CREATE TABLE user_access ( + user_id TEXT NOT NULL, + topic TEXT NOT NULL, + read INT NOT NULL, + write INT NOT NULL, + owner_user_id INT, + provisioned INTEGER NOT NULL, + PRIMARY KEY (user_id, topic), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, + FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE + ); + INSERT INTO user_access SELECT *, 0 FROM user_access_old; + DROP TABLE user_access_old; + + -- Recreate indices + CREATE UNIQUE INDEX idx_user ON user (user); + CREATE UNIQUE INDEX idx_user_stripe_customer_id ON user (stripe_customer_id); + CREATE UNIQUE INDEX idx_user_stripe_subscription_id ON user (stripe_subscription_id); + + -- Re-enable foreign keys + PRAGMA foreign_keys=on; ` ) @@ -1422,10 +1489,10 @@ func (a *Manager) AddReservation(username string, topic string, everyone Permiss return err } defer tx.Rollback() - if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username, false); err != nil { return err } - if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username, false); err != nil { return err } return tx.Commit() diff --git a/user/manager_test.go b/user/manager_test.go index 42def63f..c2887ff3 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -52,10 +52,10 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) { benGrants, err := a.Grants("ben") require.Nil(t, err) require.Equal(t, []Grant{ - {"everyonewrite", PermissionDenyAll}, - {"mytopic", PermissionReadWrite}, - {"writeme", PermissionWrite}, - {"readme", PermissionRead}, + {"everyonewrite", PermissionDenyAll, false}, + {"mytopic", PermissionReadWrite, false}, + {"writeme", PermissionWrite, false}, + {"readme", PermissionRead, false}, }, benGrants) john, err := a.Authenticate("john", "john") @@ -67,10 +67,10 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) { johnGrants, err := a.Grants("john") require.Nil(t, err) require.Equal(t, []Grant{ - {"mytopic_deny*", PermissionDenyAll}, - {"mytopic_ro*", PermissionRead}, - {"mytopic*", PermissionReadWrite}, - {"*", PermissionRead}, + {"mytopic_deny*", PermissionDenyAll, false}, + {"mytopic_ro*", PermissionRead, false}, + {"mytopic*", PermissionReadWrite, false}, + {"*", PermissionRead, false}, }, johnGrants) notben, err := a.Authenticate("ben", "this is wrong") @@ -277,10 +277,10 @@ func TestManager_UserManagement(t *testing.T) { benGrants, err := a.Grants("ben") require.Nil(t, err) require.Equal(t, []Grant{ - {"everyonewrite", PermissionDenyAll}, - {"mytopic", PermissionReadWrite}, - {"writeme", PermissionWrite}, - {"readme", PermissionRead}, + {"everyonewrite", PermissionDenyAll, false}, + {"mytopic", PermissionReadWrite, false}, + {"writeme", PermissionWrite, false}, + {"readme", PermissionRead, false}, }, benGrants) everyone, err := a.User(Everyone) @@ -292,8 +292,8 @@ func TestManager_UserManagement(t *testing.T) { everyoneGrants, err := a.Grants(Everyone) require.Nil(t, err) require.Equal(t, []Grant{ - {"everyonewrite", PermissionReadWrite}, - {"announcements", PermissionRead}, + {"everyonewrite", PermissionReadWrite, false}, + {"announcements", PermissionRead, false}, }, everyoneGrants) // Ben: Before revoking @@ -1099,19 +1099,98 @@ func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) { func TestManager_WithProvisionedUsers(t *testing.T) { f := filepath.Join(t.TempDir(), "user.db") conf := &Config{ - Filename: f, - DefaultAccess: PermissionReadWrite, - ProvisionedUsers: []*User{ - {Name: "phil", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, + Filename: f, + DefaultAccess: PermissionReadWrite, + ProvisionEnabled: true, + ProvisionUsers: []*User{ + {Name: "philuser", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, + {Name: "philadmin", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, + }, + ProvisionAccess: map[string][]*Grant{ + "philuser": { + {TopicPattern: "stats", Permission: PermissionReadWrite}, + {TopicPattern: "secret", Permission: PermissionRead}, + }, }, } a, err := NewManager(conf) require.Nil(t, err) + + // Manually add user + require.Nil(t, a.AddUser("philmanual", "manual", RoleUser, false)) + + // Check that the provisioned users are there users, err := a.Users() require.Nil(t, err) - for _, u := range users { - fmt.Println(u.ID, u.Name, u.Role) + require.Len(t, users, 4) + + require.Equal(t, "philadmin", users[0].Name) + require.Equal(t, RoleAdmin, users[0].Role) + + require.Equal(t, "philmanual", users[1].Name) + require.Equal(t, RoleUser, users[1].Role) + + grants, err := a.Grants("philuser") + require.Nil(t, err) + require.Equal(t, "philuser", users[2].Name) + require.Equal(t, RoleUser, users[2].Role) + require.Equal(t, 2, len(grants)) + require.Equal(t, "secret", grants[0].TopicPattern) + require.Equal(t, PermissionRead, grants[0].Permission) + require.Equal(t, "stats", grants[1].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[1].Permission) + + require.Equal(t, "*", users[3].Name) + + // Re-open the DB (second app start) + require.Nil(t, a.db.Close()) + conf.ProvisionUsers = []*User{ + {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, } + conf.ProvisionAccess = map[string][]*Grant{ + "philuser": { + {TopicPattern: "stats12", Permission: PermissionReadWrite}, + {TopicPattern: "secret12", Permission: PermissionRead}, + }, + } + a, err = NewManager(conf) + require.Nil(t, err) + + // Check that the provisioned users are there + users, err = a.Users() + require.Nil(t, err) + require.Len(t, users, 3) + + require.Equal(t, "philmanual", users[0].Name) + require.Equal(t, RoleUser, users[0].Role) + + grants, err = a.Grants("philuser") + require.Nil(t, err) + require.Equal(t, "philuser", users[1].Name) + require.Equal(t, RoleUser, users[1].Role) + require.Equal(t, 2, len(grants)) + require.Equal(t, "secret12", grants[0].TopicPattern) + require.Equal(t, PermissionRead, grants[0].Permission) + require.Equal(t, "stats12", grants[1].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[1].Permission) + + require.Equal(t, "*", users[2].Name) + + // Re-open the DB again (third app start) + require.Nil(t, a.db.Close()) + conf.ProvisionUsers = []*User{} + conf.ProvisionAccess = map[string][]*Grant{} + a, err = NewManager(conf) + require.Nil(t, err) + + // Check that the provisioned users are there + users, err = a.Users() + require.Nil(t, err) + require.Len(t, users, 2) + + require.Equal(t, "philmanual", users[0].Name) + require.Equal(t, RoleUser, users[0].Role) + require.Equal(t, "*", users[1].Name) } func TestToFromSQLWildcard(t *testing.T) { From f99801a2e6d8c7675fd624ff8e16075dce734f4f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 26 Jul 2025 12:14:21 +0200 Subject: [PATCH 194/378] Add "ntfy user hash" --- cmd/serve.go | 4 ++-- cmd/user.go | 33 +++++++++++++++++++++++++++++++++ user/manager.go | 29 ++++++++++++++++++++++------- user/manager_test.go | 4 ++-- user/types.go | 9 +++++++++ 5 files changed, 68 insertions(+), 11 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index ef37ee6f..882debdc 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -543,8 +543,8 @@ func parseProvisionUsers(usersRaw []string) ([]*user.User, error) { role := user.Role(strings.TrimSpace(parts[2])) if !user.AllowedUsername(username) { return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine) - } else if passwordHash == "" { - return nil, fmt.Errorf("invalid auth-provision-users: %s, password hash cannot be empty", userLine) + } else if err := user.AllowedPasswordHash(passwordHash); err != nil { + return nil, fmt.Errorf("invalid auth-provision-users: %s, %s", userLine, err.Error()) } else if !user.AllowedRole(role) { return nil, fmt.Errorf("invalid auth-provision-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) } diff --git a/cmd/user.go b/cmd/user.go index 0a6e24a1..49504a94 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -133,6 +133,22 @@ as messages per day, attachment file sizes, etc. Example: ntfy user change-tier phil pro # Change tier to "pro" for user "phil" ntfy user change-tier phil - # Remove tier from user "phil" entirely +`, + }, + { + Name: "hash", + Usage: "Create password hash for a predefined user", + UsageText: "ntfy user hash", + Action: execUserHash, + Description: `Asks for a password and creates a bcrypt password hash. + +This command is useful to create a password hash for a user, which can then be used +for predefined users in the server config file, in auth-provision-users. + +Example: + $ ntfy user hash + (asks for password and confirmation) + $2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C `, }, { @@ -289,6 +305,23 @@ func execUserChangeRole(c *cli.Context) error { return nil } +func execUserHash(c *cli.Context) error { + manager, err := createUserManager(c) + if err != nil { + return err + } + password, err := readPasswordAndConfirm(c) + if err != nil { + return err + } + hash, err := manager.HashPassword(password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + fmt.Fprintf(c.App.Writer, "%s\n", string(hash)) + return nil +} + func execUserChangeTier(c *cli.Context) error { username := c.Args().Get(0) tier := c.Args().Get(1) diff --git a/user/manager.go b/user/manager.go index 09db145e..ecef8747 100644 --- a/user/manager.go +++ b/user/manager.go @@ -981,12 +981,15 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha if !AllowedUsername(username) || !AllowedRole(role) { return ErrInvalidArgument } - var hash []byte + var hash string var err error = nil if hashed { - hash = []byte(password) + hash = password + if err := AllowedPasswordHash(hash); err != nil { + return err + } } else { - hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) + hash, err = a.HashPassword(password) if err != nil { return err } @@ -1328,12 +1331,15 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error { } func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error { - var hash []byte + var hash string var err error if hashed { - hash = []byte(password) + hash = password + if err := AllowedPasswordHash(hash); err != nil { + return err + } } else { - hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) + hash, err = a.HashPassword(password) if err != nil { return err } @@ -1640,6 +1646,15 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) { }, nil } +// HashPassword hashes the given password using bcrypt with the configured cost +func (a *Manager) HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) + if err != nil { + return "", err + } + return string(hash), nil +} + // Close closes the underlying database func (a *Manager) Close() error { return a.db.Close() @@ -1681,7 +1696,7 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) { return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err) } - } else if existingUser.Hash != user.Hash || existingUser.Role != user.Role { + } else if existingUser.Provisioned && (existingUser.Hash != user.Hash || existingUser.Role != user.Role) { log.Tag(tag).Info("Updating provisioned user %s", user.Name) if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil { return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err) diff --git a/user/manager_test.go b/user/manager_test.go index c2887ff3..94bd1b97 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -340,7 +340,7 @@ func TestManager_UserManagement(t *testing.T) { func TestManager_ChangePassword(t *testing.T) { a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false)) - require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true)) + require.Nil(t, a.AddUser("jane", "$2a$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true)) _, err := a.Authenticate("phil", "phil") require.Nil(t, err) @@ -354,7 +354,7 @@ func TestManager_ChangePassword(t *testing.T) { _, err = a.Authenticate("phil", "newpass") require.Nil(t, err) - require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true)) + require.Nil(t, a.ChangePassword("jane", "$2a$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true)) _, err = a.Authenticate("jane", "jane") require.Equal(t, ErrUnauthenticated, err) _, err = a.Authenticate("jane", "newpass") diff --git a/user/types.go b/user/types.go index 90eeefce..aaf77d1f 100644 --- a/user/types.go +++ b/user/types.go @@ -274,6 +274,14 @@ func AllowedTier(tier string) bool { return allowedTierRegex.MatchString(tier) } +// AllowedPasswordHash checks if the given password hash is a valid bcrypt hash +func AllowedPasswordHash(hash string) error { + if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") { + return ErrPasswordHashInvalid + } + return nil +} + // Error constants used by the package var ( ErrUnauthenticated = errors.New("unauthenticated") @@ -281,6 +289,7 @@ var ( ErrInvalidArgument = errors.New("invalid argument") ErrUserNotFound = errors.New("user not found") ErrUserExists = errors.New("user already exists") + ErrPasswordHashInvalid = errors.New("password hash but be a bcrypt hash, use 'ntfy user hash' to generate") ErrTierNotFound = errors.New("tier not found") ErrTokenNotFound = errors.New("token not found") ErrPhoneNumberNotFound = errors.New("phone number not found") From 141ddb3a5187a7e3281f99e96d7e11bf09871388 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 26 Jul 2025 12:20:11 +0200 Subject: [PATCH 195/378] Comments --- user/manager.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user/manager.go b/user/manager.go index ecef8747..70d16370 100644 --- a/user/manager.go +++ b/user/manager.go @@ -529,11 +529,12 @@ type Manager struct { mu sync.Mutex } +// Config holds the configuration for the user Manager type Config struct { Filename string // Database filename, e.g. "/var/lib/ntfy/user.db" StartupQueries string // Queries to run on startup, e.g. to create initial users or tiers DefaultAccess Permission // Default permission if no ACL matches - ProvisionEnabled bool // Enable auto-provisioning of users and access grants + ProvisionEnabled bool // Enable auto-provisioning of users and access grants, disabled for "ntfy user" commands ProvisionUsers []*User // Predefined users to create on startup ProvisionAccess map[string][]*Grant // Predefined access grants to create on startup QueueWriterInterval time.Duration // Interval for the async queue writer to flush stats and token updates to the database From 0d36ab8af37760670358127d89e27c6ee102e154 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sun, 27 Jul 2025 00:01:51 -0600 Subject: [PATCH 196/378] allow newlines in in-line go templates --- server/server.go | 12 +++++++--- server/server_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index f3d2ac51..0b7880cd 100644 --- a/server/server.go +++ b/server/server.go @@ -991,7 +991,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid } - messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") + template = templateMode(readParam(r, "x-template", "template", "tpl")) + var messageStr string + if template.Enabled() && template.Name() == "" { + // don't convert "\n" to literal newline for inline templates + messageStr = readParam(r, "x-message", "message", "m") + } else { + messageStr = strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") + } if messageStr != "" { m.Message = messageStr } @@ -1033,7 +1040,6 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if markdown || strings.ToLower(contentType) == "text/markdown" { m.ContentType = "text/markdown" } - template = templateMode(readParam(r, "x-template", "template", "tpl")) unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! contentEncoding := readParam(r, "content-encoding") if unifiedpush || contentEncoding == "aes128gcm" { @@ -1198,7 +1204,7 @@ func (s *Server) renderTemplate(tpl string, source string) (string, error) { if err := t.Execute(limitWriter, data); err != nil { return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error()) } - return strings.TrimSpace(buf.String()), nil + return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines } func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error { diff --git a/server/server_test.go b/server/server_test.go index 36bbae3f..41633dd5 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3069,6 +3069,61 @@ func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) { require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code) } +func TestServer_MessageTemplate_InlineNewlines(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{"New\nlines"}}`, + "X-Title": `{{"New\nlines"}}`, + "X-Template": "1", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `New +lines`, m.Message) + require.Equal(t, `New +lines`, m.Title) +} + +func TestServer_MessageTemplate_InlineNewlinesOutsideOfTemplate(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar","food":"bag"}`, map[string]string{ + "X-Message": `{{.foo}}{{"\n"}}{{.food}}`, + "X-Title": `{{.food}}{{"\n"}}{{.foo}}`, + "X-Template": "1", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `bar +bag`, m.Message) + require.Equal(t, `bag +bar`, m.Title) +} + +func TestServer_MessageTemplate_TemplateFileNewlines(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.TemplateDir = t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "newline.yml"), []byte(` +title: | + {{.food}}{{"\n"}}{{.foo}} +message: | + {{.foo}}{{"\n"}}{{.food}} +`), 0644)) + s := newTestServer(t, c) + response := request(t, s, "POST", "/mytopic?template=newline", `{"foo":"bar","food":"bag"}`, nil) + fmt.Println(response.Body.String()) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `bar +bag`, m.Message) + require.Equal(t, `bag +bar`, m.Title) +} + var ( //go:embed testdata/webhook_github_comment_created.json githubCommentCreatedJSON string From 1b394e9bb8105107d14e3f56a8f23cbefe73a4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=A4=E0=AE=AE=E0=AE=BF=E0=AE=B4=E0=AF=8D=E0=AE=A8?= =?UTF-8?q?=E0=AF=87=E0=AE=B0=E0=AE=AE=E0=AF=8D?= Date: Sat, 26 Jul 2025 07:39:33 +0200 Subject: [PATCH 197/378] Translated using Weblate (Tamil) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ta/ --- web/public/static/langs/ta.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web/public/static/langs/ta.json b/web/public/static/langs/ta.json index 11635c91..b822107d 100644 --- a/web/public/static/langs/ta.json +++ b/web/public/static/langs/ta.json @@ -13,7 +13,7 @@ "nav_button_documentation": "ஆவணப்படுத்துதல்", "nav_button_publish_message": "அறிவிப்பை வெளியிடுங்கள்", "alert_not_supported_description": "உங்கள் உலாவியில் அறிவிப்புகள் ஆதரிக்கப்படவில்லை", - "alert_not_supported_context_description": "அறிவிப்புகள் HTTP களில் மட்டுமே ஆதரிக்கப்படுகின்றன. இது அறிவிப்புகள் பநிஇ இன் வரம்பு.", + "alert_not_supported_context_description": "அறிவிப்புகள் HTTP களில் மட்டுமே ஆதரிக்கப்படுகின்றன. இதுஅறிவிப்புகள் பநிஇ இன் வரம்பு.", "notifications_list": "அறிவிப்புகள் பட்டியல்", "notifications_delete": "நீக்கு", "notifications_copied_to_clipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", @@ -76,7 +76,7 @@ "publish_dialog_chip_email_label": "மின்னஞ்சலுக்கு அனுப்பவும்", "publish_dialog_chip_call_no_verified_numbers_tooltip": "சரிபார்க்கப்பட்ட தொலைபேசி எண்கள் இல்லை", "publish_dialog_chip_attach_url_label": "முகவரி மூலம் கோப்பை இணைக்கவும்", - "publish_dialog_details_examples_description": "எடுத்துக்காட்டுகள் மற்றும் அனைத்து அனுப்பும் அம்சங்களின் விரிவான விளக்கத்திற்கு, தயவுசெய்து ஆவணங்கள் ஐப் பார்க்கவும்.", + "publish_dialog_details_examples_description": "எடுத்துக்காட்டுகள் மற்றும் அனைத்து அனுப்பும் அம்சங்களின் விரிவான விளக்கத்திற்கு, தயவுசெய்து ஆவணங்கள் ஐப் பார்க்கவும்.", "publish_dialog_chip_attach_file_label": "உள்ளக கோப்பை இணைக்கவும்", "publish_dialog_chip_delay_label": "நேரந்தவறுகை வழங்கல்", "publish_dialog_chip_topic_label": "தலைப்பை மாற்றவும்", @@ -133,10 +133,10 @@ "account_usage_cannot_create_portal_session": "பட்டியலிடல் போர்ட்டலைத் திறக்க முடியவில்லை", "account_delete_title": "கணக்கை நீக்கு", "account_delete_description": "உங்கள் கணக்கை நிரந்தரமாக நீக்கவும்", - "account_upgrade_dialog_cancel_warning": "இது உங்கள் சந்தாவை ரத்துசெய்யும் , மேலும் உங்கள் கணக்கை {{date} at இல் தரமிறக்குகிறது. அந்த தேதியில், தலைப்பு முன்பதிவு மற்றும் சேவையகத்தில் தற்காலிகமாக சேமிக்கப்பட்ட செய்திகளும் நீக்கப்படும் .", + "account_upgrade_dialog_cancel_warning": "இது உங்கள் சந்தாவை ரத்துசெய்யும் , மேலும் உங்கள் கணக்கை {{date}} இல் தரமிறக்குகிறது. அந்தத் தேதியில், தலைப்பு முன்பதிவு மற்றும் சேவையகத்தில் தற்காலிகமாகச் சேமிக்கப்பட்ட செய்திகளும் நீக்கப்படும் .", "account_upgrade_dialog_proration_info": " புரோரேசன் : கட்டணத் திட்டங்களுக்கு இடையில் மேம்படுத்தும்போது, விலை வேறுபாடு உடனடியாக கட்டணம் வசூலிக்கப்படும் . குறைந்த அடுக்குக்கு தரமிறக்கும்போது, எதிர்கால பட்டியலிடல் காலங்களுக்கு செலுத்த இருப்பு பயன்படுத்தப்படும்.", - "account_upgrade_dialog_reservations_warning_one": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கை விட குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தது ஒரு முன்பதிவை நீக்கு . <இணைப்பு> அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", - "account_upgrade_dialog_reservations_warning_other": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கை விட குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தபட்சம் {{count}} முன்பதிவு ஐ நீக்கவும். <இணைப்பு> அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", + "account_upgrade_dialog_reservations_warning_one": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கைவிடக் குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தது ஒரு முன்பதிவை நீக்கு . அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", + "account_upgrade_dialog_reservations_warning_other": "தேர்ந்தெடுக்கப்பட்ட அடுக்கு உங்கள் தற்போதைய அடுக்கைவிடக் குறைவான ஒதுக்கப்பட்ட தலைப்புகளை அனுமதிக்கிறது. உங்கள் அடுக்கை மாற்றுவதற்கு முன், தயவுசெய்து குறைந்தபட்சம் {{count}} முன்பதிவு ஐ நீக்கவும். அமைப்புகள் இல் முன்பதிவுகளை அகற்றலாம்.", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} ஒதுக்கப்பட்ட தலைப்புகள்", "account_upgrade_dialog_tier_features_no_reservations": "ஒதுக்கப்பட்ட தலைப்புகள் இல்லை", "account_upgrade_dialog_tier_features_messages_one": "{{messages}} நாள்தோறும் செய்தி", @@ -153,14 +153,14 @@ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} ஆண்டுதோறும் கட்டணம் செலுத்தப்படுகிறது. {{save}} சேமி.", "account_upgrade_dialog_tier_selected_label": "தேர்ந்தெடுக்கப்பட்டது", "account_upgrade_dialog_tier_current_label": "மின்னோட்ட்ம், ஓட்டம்", - "account_upgrade_dialog_billing_contact_email": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து <இணைப்பு> எங்களை தொடர்பு கொள்ளவும் நேரடியாக.", + "account_upgrade_dialog_billing_contact_email": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து எங்களைத் தொடர்பு கொள்ளவும் நேரடியாக.", "account_upgrade_dialog_button_cancel": "ரத்துசெய்", - "account_upgrade_dialog_billing_contact_website": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து எங்கள் <இணைப்பு> வலைத்தளம் ஐப் பார்க்கவும்.", + "account_upgrade_dialog_billing_contact_website": "பட்டியலிடல் கேள்விகளுக்கு, தயவுசெய்து எங்கள் வலைத்தளம் ஐப் பார்க்கவும்.", "account_upgrade_dialog_button_redirect_signup": "இப்போது பதிவுபெறுக", "account_upgrade_dialog_button_pay_now": "இப்போது பணம் செலுத்தி குழுசேரவும்", "account_upgrade_dialog_button_cancel_subscription": "சந்தாவை ரத்துசெய்", "account_tokens_title": "டோக்கன்களை அணுகவும்", - "account_tokens_description": "NTFY பநிஇ வழியாக வெளியிடும் மற்றும் சந்தா செலுத்தும் போது அணுகல் டோக்கன்களைப் பயன்படுத்தவும், எனவே உங்கள் கணக்கு நற்சான்றிதழ்களை அனுப்ப வேண்டியதில்லை. மேலும் அறிய <இணைப்பு> ஆவணங்கள் ஐப் பாருங்கள்.", + "account_tokens_description": "NTFY பநிஇ வழியாக வெளியிடும் மற்றும் சந்தா செலுத்தும்போது அணுகல் டோக்கன்களைப் பயன்படுத்தவும், எனவே உங்கள் கணக்கு நற்சான்றிதழ்களை அனுப்ப வேண்டியதில்லை. மேலும் அறிய ஆவணங்கள் ஐப் பாருங்கள்.", "account_upgrade_dialog_button_update_subscription": "சந்தாவைப் புதுப்பிக்கவும்", "account_tokens_table_token_header": "கிள்ளாக்கு", "account_tokens_table_label_header": "சிட்டை", @@ -216,7 +216,7 @@ "prefs_notifications_web_push_title": "பின்னணி அறிவிப்புகள்", "prefs_notifications_web_push_enabled_description": "வலை பயன்பாடு இயங்காதபோது கூட அறிவிப்புகள் பெறப்படுகின்றன (வலை புச் வழியாக)", "prefs_notifications_web_push_disabled_description": "வலை பயன்பாடு இயங்கும்போது அறிவிப்பு பெறப்படுகிறது (வெப்சாக்கெட் வழியாக)", - "prefs_notifications_web_push_enabled": "{{server} க்கு க்கு இயக்கப்பட்டது", + "prefs_notifications_web_push_enabled": "{{server}} க்கு இயக்கப்பட்டது", "prefs_notifications_web_push_disabled": "முடக்கப்பட்டது", "prefs_users_title": "பயனர்களை நிர்வகிக்கவும்", "prefs_users_description": "உங்கள் பாதுகாக்கப்பட்ட தலைப்புகளுக்கு பயனர்களை இங்கே சேர்க்கவும்/அகற்றவும். பயனர்பெயர் மற்றும் கடவுச்சொல் உலாவியின் உள்ளக சேமிப்பகத்தில் சேமிக்கப்பட்டுள்ளன என்பதை நினைவில் கொள்க.", @@ -271,7 +271,7 @@ "priority_max": "அதிகபட்சம்", "priority_default": "இயல்புநிலை", "error_boundary_title": "ஓ, NTFY செயலிழந்தது", - "error_boundary_description": "இது வெளிப்படையாக நடக்கக்கூடாது. இதைப் பற்றி மிகவும் வருந்துகிறேன். .", + "error_boundary_description": "இது நிச்சயமாக நடக்கக் கூடாது. இதுகுறித்து மிகவும் வருந்துகிறேன்.
உங்களிடம் ஒரு நிமிடம் இருந்தால், தயவுசெய்து இதை GitHub இல் புகாரளிக்கவும், அல்லது Discord அல்லது Matrix வழியாக எங்களுக்குத் தெரியப்படுத்தவும்.", "error_boundary_button_copy_stack_trace": "அடுக்கு சுவடு நகலெடுக்கவும்", "error_boundary_button_reload_ntfy": "Ntfy ஐ மீண்டும் ஏற்றவும்", "error_boundary_stack_trace": "ச்டாக் சுவடு", @@ -349,7 +349,7 @@ "notifications_no_subscriptions_title": "உங்களிடம் இன்னும் சந்தாக்கள் இல்லை என்று தெரிகிறது.", "notifications_no_subscriptions_description": "ஒரு தலைப்பை உருவாக்க அல்லது குழுசேர \"{{linktext}}\" இணைப்பைக் சொடுக்கு செய்க. அதன்பிறகு, நீங்கள் புட் அல்லது இடுகை வழியாக செய்திகளை அனுப்பலாம், மேலும் நீங்கள் இங்கே அறிவிப்புகளைப் பெறுவீர்கள்.", "notifications_example": "எடுத்துக்காட்டு", - "notifications_more_details": "மேலும் தகவலுக்கு, வலைத்தளம் அல்லது ஆவணங்கள் ஐப் பாருங்கள்.", + "notifications_more_details": "மேலும் தகவலுக்கு, வலைத்தளம் அல்லது ஆவணங்கள் ஐப் பாருங்கள்.", "display_name_dialog_title": "காட்சி பெயரை மாற்றவும்", "display_name_dialog_description": "சந்தா பட்டியலில் காட்டப்படும் தலைப்புக்கு மாற்று பெயரை அமைக்கவும். சிக்கலான பெயர்களைக் கொண்ட தலைப்புகளை மிக எளிதாக அடையாளம் காண இது உதவுகிறது.", "display_name_dialog_placeholder": "காட்சி பெயர்", @@ -399,7 +399,7 @@ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "{{discount}}% வரை சேமிக்கவும்", "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} முன்பதிவு செய்யப்பட்ட தலைப்பு", "prefs_users_add_button": "பயனரைச் சேர்க்கவும்", - "error_boundary_unsupported_indexeddb_description": "NTFY வலை பயன்பாட்டிற்கு செயல்பட குறியீட்டு தேவை, மற்றும் உங்கள் உலாவி தனிப்பட்ட உலாவல் பயன்முறையில் IndexEDDB ஐ ஆதரிக்காது. எப்படியிருந்தாலும் தனிப்பட்ட உலாவல் பயன்முறையில் பயன்பாடு, ஏனென்றால் அனைத்தும் உலாவி சேமிப்பகத்தில் சேமிக்கப்படுகின்றன. இந்த அறிவிலிமையம் இதழில் இல் பற்றி நீங்கள் மேலும் படிக்கலாம் அல்லது டிச்கார்ட் அல்லது மேட்ரிக்ச் இல் எங்களுடன் பேசலாம்.", + "error_boundary_unsupported_indexeddb_description": "ntfy வலை பயன்பாடு செயல்பட IndexedDB தேவை, மேலும் உங்கள் உலாவித் தனிப்பட்ட உலாவல் பயன்முறையில் IndexedDB ஐ ஆதரிக்காது.

இது துரதிர்ஷ்டவசமானது என்றாலும், ntfy வலை பயன்பாட்டைத் தனிப்பட்ட உலாவல் பயன்முறையில் பயன்படுத்துவது உண்மையில் அர்த்தமற்றது, ஏனெனில் அனைத்தும் உலாவிச் சேமிப்பகத்தில் சேமிக்கப்படுகின்றன. இதைப் பற்றி நீங்கள் இந்த GitHub சிக்கலில் மேலும் படிக்கலாம், அல்லது Discord அல்லது Matrix இல் எங்களுடன் பேசலாம்.", "web_push_subscription_expiring_title": "அறிவிப்புகள் இடைநிறுத்தப்படும்", "web_push_subscription_expiring_body": "தொடர்ந்து அறிவிப்புகளைப் பெற NTFY ஐத் திறக்கவும்", "web_push_unknown_notification_title": "சேவையகத்திலிருந்து அறியப்படாத அறிவிப்பு பெறப்பட்டது", From 1470afb71572f7e95597285eb4d1dac40b69979f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 10:15:48 +0200 Subject: [PATCH 198/378] Make templateMode more understandable --- server/server.go | 14 ++++++-------- server/types.go | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/server/server.go b/server/server.go index 0b7880cd..ac7b10dc 100644 --- a/server/server.go +++ b/server/server.go @@ -992,12 +992,10 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid } template = templateMode(readParam(r, "x-template", "template", "tpl")) - var messageStr string - if template.Enabled() && template.Name() == "" { - // don't convert "\n" to literal newline for inline templates - messageStr = readParam(r, "x-message", "message", "m") - } else { - messageStr = strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") + messageStr := readParam(r, "x-message", "message", "m") + if !template.InlineMode() { + // Convert "\n" to literal newline everything but inline mode + messageStr = strings.ReplaceAll(messageStr, "\\n", "\n") } if messageStr != "" { m.Message = messageStr @@ -1125,8 +1123,8 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM return errHTTPEntityTooLargeJSONBody } peekedBody := strings.TrimSpace(string(body.PeekedBytes)) - if templateName := template.Name(); templateName != "" { - if err := s.renderTemplateFromFile(m, templateName, peekedBody); err != nil { + if template.FileMode() { + if err := s.renderTemplateFromFile(m, template.FileName(), peekedBody); err != nil { return err } } else { diff --git a/server/types.go b/server/types.go index ea6b8615..65492e46 100644 --- a/server/types.go +++ b/server/types.go @@ -245,19 +245,46 @@ func (q *queryFilter) Pass(msg *message) bool { return true } +// templateMode represents the mode in which templates are used +// +// It can be +// - empty: templating is disabled +// - a boolean string (yes/1/true/no/0/false): inline-templating mode +// - a filename (e.g. grafana): template mode with a file type templateMode string +// Enabled returns true if templating is enabled func (t templateMode) Enabled() bool { return t != "" } -func (t templateMode) Name() string { - if isBoolValue(string(t)) { - return "" - } - return string(t) +// InlineMode returns true if inline-templating mode is enabled +func (t templateMode) InlineMode() bool { + return t.Enabled() && isBoolValue(string(t)) } +// FileMode returns true if file-templating mode is enabled +func (t templateMode) FileMode() bool { + return t.Enabled() && !isBoolValue(string(t)) +} + +// FileName returns the filename if file-templating mode is enabled, or an empty string otherwise +func (t templateMode) FileName() string { + if t.FileMode() { + return string(t) + } + return "" +} + +// templateFile represents a template file with title and message +// It is used for file-based templates, e.g. grafana, influxdb, etc. +// +// Example YAML: +// +// title: "Alert: {{ .Title }}" +// message: | +// This is a {{ .Type }} alert. +// It can be multiline. type templateFile struct { Title *string `yaml:"title"` Message *string `yaml:"message"` From f3c67f1d716f6ee50af89cfd51a908d962bdc73a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 11:02:34 +0200 Subject: [PATCH 199/378] Refuse to update manually created users --- cmd/serve.go | 4 ++-- server/config.go | 4 ++-- server/server.go | 4 ++-- user/manager.go | 18 +++++++++++------- user/manager_test.go | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 882debdc..7e7e56e1 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -416,8 +416,8 @@ func execServe(c *cli.Context) error { conf.AuthFile = authFile conf.AuthStartupQueries = authStartupQueries conf.AuthDefault = authDefault - conf.AuthProvisionedUsers = authProvisionUsers - conf.AuthProvisionedAccess = authProvisionAccess + conf.AuthProvisionUsers = authProvisionUsers + conf.AuthProvisionAccess = authProvisionAccess conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit diff --git a/server/config.go b/server/config.go index 86971e47..5cf0b035 100644 --- a/server/config.go +++ b/server/config.go @@ -95,8 +95,8 @@ type Config struct { AuthFile string AuthStartupQueries string AuthDefault user.Permission - AuthProvisionedUsers []*user.User - AuthProvisionedAccess map[string][]*user.Grant + AuthProvisionUsers []*user.User + AuthProvisionAccess map[string][]*user.Grant AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration AttachmentCacheDir string diff --git a/server/server.go b/server/server.go index 4fcd9ba3..dbe61905 100644 --- a/server/server.go +++ b/server/server.go @@ -201,8 +201,8 @@ func New(conf *Config) (*Server, error) { StartupQueries: conf.AuthStartupQueries, DefaultAccess: conf.AuthDefault, ProvisionEnabled: true, // Enable provisioning of users and access - ProvisionUsers: conf.AuthProvisionedUsers, - ProvisionAccess: conf.AuthProvisionedAccess, + ProvisionUsers: conf.AuthProvisionUsers, + ProvisionAccess: conf.AuthProvisionAccess, BcryptCost: conf.AuthBcryptCost, QueueWriterInterval: conf.AuthStatsQueueWriterInterval, } diff --git a/user/manager.go b/user/manager.go index 70d16370..2e176450 100644 --- a/user/manager.go +++ b/user/manager.go @@ -1697,13 +1697,17 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) { return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err) } - } else if existingUser.Provisioned && (existingUser.Hash != user.Hash || existingUser.Role != user.Role) { - log.Tag(tag).Info("Updating provisioned user %s", user.Name) - if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil { - return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err) - } - if err := a.changeRoleTx(tx, user.Name, user.Role); err != nil { - return fmt.Errorf("failed to change role for provisioned user %s: %v", user.Name, err) + } else { + if !existingUser.Provisioned { + log.Tag(tag).Warn("Refusing to update manually user %s", user.Name) + } else if existingUser.Hash != user.Hash || existingUser.Role != user.Role { + log.Tag(tag).Info("Updating provisioned user %s", user.Name) + if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil { + return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err) + } + if err := a.changeRoleTx(tx, user.Name, user.Role); err != nil { + return fmt.Errorf("failed to change role for provisioned user %s: %v", user.Name, err) + } } } } diff --git a/user/manager_test.go b/user/manager_test.go index 94bd1b97..2ce078f3 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -1193,6 +1193,39 @@ func TestManager_WithProvisionedUsers(t *testing.T) { require.Equal(t, "*", users[1].Name) } +func TestManager_DoNotUpdateNonProvisionedUsers(t *testing.T) { + f := filepath.Join(t.TempDir(), "user.db") + conf := &Config{ + Filename: f, + DefaultAccess: PermissionReadWrite, + ProvisionEnabled: true, + ProvisionUsers: []*User{}, + ProvisionAccess: map[string][]*Grant{}, + } + a, err := NewManager(conf) + require.Nil(t, err) + + // Manually add user + require.Nil(t, a.AddUser("philuser", "manual", RoleUser, false)) + + // Re-open the DB (second app start) + require.Nil(t, a.db.Close()) + conf.ProvisionUsers = []*User{ + {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, + } + conf.ProvisionAccess = map[string][]*Grant{} + a, err = NewManager(conf) + require.Nil(t, err) + + // Check that the provisioned users are there + users, err := a.Users() + require.Nil(t, err) + require.Len(t, users, 2) + require.Equal(t, "philuser", users[0].Name) + require.Equal(t, RoleUser, users[0].Role) // Should not have been updated + require.NotEqual(t, "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash) +} + func TestToFromSQLWildcard(t *testing.T) { require.Equal(t, "up%", toSQLWildcard("up*")) require.Equal(t, "up\\_%", toSQLWildcard("up_*")) From fe545423c518b42534652aebf4f127a062f09eb3 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 12:10:16 +0200 Subject: [PATCH 200/378] Change to auth-(users|access), upgrade manually added users to provision users --- cmd/serve.go | 40 ++++++++++++++++++++-------------------- server/config.go | 4 ++-- server/server.go | 4 ++-- user/manager.go | 38 ++++++++++++++++++++++++++++---------- user/manager_test.go | 20 ++++++++++---------- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 7e7e56e1..dc503ccc 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -48,8 +48,8 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), - altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provision-users", Aliases: []string{"auth_provision_users"}, EnvVars: []string{"NTFY_AUTH_PROVISION_USERS"}, Usage: "pre-provisioned declarative users"}), - altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-provision-access", Aliases: []string{"auth_provision_access"}, EnvVars: []string{"NTFY_AUTH_PROVISION_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-access", Aliases: []string{"auth_access"}, EnvVars: []string{"NTFY_AUTH_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), @@ -156,8 +156,8 @@ func execServe(c *cli.Context) error { authFile := c.String("auth-file") authStartupQueries := c.String("auth-startup-queries") authDefaultAccess := c.String("auth-default-access") - authProvisionUsersRaw := c.StringSlice("auth-provision-users") - authProvisionAccessRaw := c.StringSlice("auth-provision-access") + authUsersRaw := c.StringSlice("auth-users") + authAccessRaw := c.StringSlice("auth-access") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -353,11 +353,11 @@ func execServe(c *cli.Context) error { if err != nil { return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'") } - authProvisionUsers, err := parseProvisionUsers(authProvisionUsersRaw) + authUsers, err := parseUsers(authUsersRaw) if err != nil { return err } - authProvisionAccess, err := parseProvisionAccess(authProvisionUsers, authProvisionAccessRaw) + authAccess, err := parseAccess(authUsers, authAccessRaw) if err != nil { return err } @@ -416,8 +416,8 @@ func execServe(c *cli.Context) error { conf.AuthFile = authFile conf.AuthStartupQueries = authStartupQueries conf.AuthDefault = authDefault - conf.AuthProvisionUsers = authProvisionUsers - conf.AuthProvisionAccess = authProvisionAccess + conf.AuthUsers = authUsers + conf.AuthAccess = authAccess conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit @@ -531,22 +531,22 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) { return } -func parseProvisionUsers(usersRaw []string) ([]*user.User, error) { +func parseUsers(usersRaw []string) ([]*user.User, error) { provisionUsers := make([]*user.User, 0) for _, userLine := range usersRaw { parts := strings.Split(userLine, ":") if len(parts) != 3 { - return nil, fmt.Errorf("invalid auth-provision-users: %s, expected format: 'name:hash:role'", userLine) + return nil, fmt.Errorf("invalid auth-users: %s, expected format: 'name:hash:role'", userLine) } username := strings.TrimSpace(parts[0]) passwordHash := strings.TrimSpace(parts[1]) role := user.Role(strings.TrimSpace(parts[2])) if !user.AllowedUsername(username) { - return nil, fmt.Errorf("invalid auth-provision-users: %s, username invalid", userLine) + return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine) } else if err := user.AllowedPasswordHash(passwordHash); err != nil { - return nil, fmt.Errorf("invalid auth-provision-users: %s, %s", userLine, err.Error()) + return nil, fmt.Errorf("invalid auth-users: %s, %s", userLine, err.Error()) } else if !user.AllowedRole(role) { - return nil, fmt.Errorf("invalid auth-provision-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) + return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) } provisionUsers = append(provisionUsers, &user.User{ Name: username, @@ -558,12 +558,12 @@ func parseProvisionUsers(usersRaw []string) ([]*user.User, error) { return provisionUsers, nil } -func parseProvisionAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[string][]*user.Grant, error) { +func parseAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[string][]*user.Grant, error) { access := make(map[string][]*user.Grant) for _, accessLine := range provisionAccessRaw { parts := strings.Split(accessLine, ":") if len(parts) != 3 { - return nil, fmt.Errorf("invalid auth-provision-access: %s, expected format: 'user:topic:permission'", accessLine) + return nil, fmt.Errorf("invalid auth-access: %s, expected format: 'user:topic:permission'", accessLine) } username := strings.TrimSpace(parts[0]) if username == userEveryone { @@ -574,20 +574,20 @@ func parseProvisionAccess(provisionUsers []*user.User, provisionAccessRaw []stri }) if username != user.Everyone { if !exists { - return nil, fmt.Errorf("invalid auth-provision-access: %s, user %s is not provisioned", accessLine, username) + return nil, fmt.Errorf("invalid auth-access: %s, user %s is not provisioned", accessLine, username) } else if !user.AllowedUsername(username) { - return nil, fmt.Errorf("invalid auth-provision-access: %s, username %s invalid", accessLine, username) + return nil, fmt.Errorf("invalid auth-access: %s, username %s invalid", accessLine, username) } else if provisionUser.Role != user.RoleUser { - return nil, fmt.Errorf("invalid auth-provision-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username) + return nil, fmt.Errorf("invalid auth-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username) } } topic := strings.TrimSpace(parts[1]) if !user.AllowedTopicPattern(topic) { - return nil, fmt.Errorf("invalid auth-provision-access: %s, topic pattern %s invalid", accessLine, topic) + return nil, fmt.Errorf("invalid auth-access: %s, topic pattern %s invalid", accessLine, topic) } permission, err := user.ParsePermission(strings.TrimSpace(parts[2])) if err != nil { - return nil, fmt.Errorf("invalid auth-provision-access: %s, permission %s invalid, %s", accessLine, parts[2], err.Error()) + return nil, fmt.Errorf("invalid auth-access: %s, permission %s invalid, %s", accessLine, parts[2], err.Error()) } if _, exists := access[username]; !exists { access[username] = make([]*user.Grant, 0) diff --git a/server/config.go b/server/config.go index 5cf0b035..99d829b2 100644 --- a/server/config.go +++ b/server/config.go @@ -95,8 +95,8 @@ type Config struct { AuthFile string AuthStartupQueries string AuthDefault user.Permission - AuthProvisionUsers []*user.User - AuthProvisionAccess map[string][]*user.Grant + AuthUsers []*user.User + AuthAccess map[string][]*user.Grant AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration AttachmentCacheDir string diff --git a/server/server.go b/server/server.go index dbe61905..55fa3af7 100644 --- a/server/server.go +++ b/server/server.go @@ -201,8 +201,8 @@ func New(conf *Config) (*Server, error) { StartupQueries: conf.AuthStartupQueries, DefaultAccess: conf.AuthDefault, ProvisionEnabled: true, // Enable provisioning of users and access - ProvisionUsers: conf.AuthProvisionUsers, - ProvisionAccess: conf.AuthProvisionAccess, + Users: conf.AuthUsers, + Access: conf.AuthAccess, BcryptCost: conf.AuthBcryptCost, QueueWriterInterval: conf.AuthStatsQueueWriterInterval, } diff --git a/user/manager.go b/user/manager.go index 2e176450..5418f534 100644 --- a/user/manager.go +++ b/user/manager.go @@ -184,6 +184,7 @@ const ( selectUserCountQuery = `SELECT COUNT(*) FROM user` updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?` updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?` + updateUserProvisionedQuery = `UPDATE user SET provisioned = ? WHERE user = ?` updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE id = ?` updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ?, stats_calls = ? WHERE id = ?` updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0, stats_calls = 0` @@ -535,8 +536,8 @@ type Config struct { StartupQueries string // Queries to run on startup, e.g. to create initial users or tiers DefaultAccess Permission // Default permission if no ACL matches ProvisionEnabled bool // Enable auto-provisioning of users and access grants, disabled for "ntfy user" commands - ProvisionUsers []*User // Predefined users to create on startup - ProvisionAccess map[string][]*Grant // Predefined access grants to create on startup + Users []*User // Predefined users to create on startup + Access map[string][]*Grant // Predefined access grants to create on startup QueueWriterInterval time.Duration // Interval for the async queue writer to flush stats and token updates to the database BcryptCost int // Cost of generated passwords; lowering makes testing faster } @@ -1374,6 +1375,21 @@ func (a *Manager) changeRoleTx(tx *sql.Tx, username string, role Role) error { return nil } +// ChangeProvisioned changes the provisioned status of a user. This is used to mark users as +// provisioned. A provisioned user is a user defined in the config file. +func (a *Manager) ChangeProvisioned(username string, provisioned bool) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.changeProvisionedTx(tx, username, provisioned) + }) +} + +func (a *Manager) changeProvisionedTx(tx *sql.Tx, username string, provisioned bool) error { + if _, err := tx.Exec(updateUserProvisionedQuery, provisioned, username); err != nil { + return err + } + return nil +} + // ChangeTier changes a user's tier using the tier code. This function does not delete reservations, messages, // or attachments, even if the new tier has lower limits in this regard. That has to be done elsewhere. func (a *Manager) ChangeTier(username, tier string) error { @@ -1669,7 +1685,7 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { if err != nil { return err } - provisionUsernames := util.Map(a.config.ProvisionUsers, func(u *User) string { + provisionUsernames := util.Map(a.config.Users, func(u *User) string { return u.Name }) return execTx(a.db, func(tx *sql.Tx) error { @@ -1678,14 +1694,13 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { if user.Name == Everyone { continue } else if user.Provisioned && !util.Contains(provisionUsernames, user.Name) { - log.Tag(tag).Info("Removing previously provisioned user %s", user.Name) if err := a.removeUserTx(tx, user.Name); err != nil { return fmt.Errorf("failed to remove provisioned user %s: %v", user.Name, err) } } } // Add or update provisioned users - for _, user := range a.config.ProvisionUsers { + for _, user := range a.config.Users { if user.Name == Everyone { continue } @@ -1693,18 +1708,21 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { return u.Name == user.Name }) if !exists { - log.Tag(tag).Info("Adding provisioned user %s", user.Name) if err := a.addUserTx(tx, user.Name, user.Hash, user.Role, true, true); err != nil && !errors.Is(err, ErrUserExists) { return fmt.Errorf("failed to add provisioned user %s: %v", user.Name, err) } } else { if !existingUser.Provisioned { - log.Tag(tag).Warn("Refusing to update manually user %s", user.Name) - } else if existingUser.Hash != user.Hash || existingUser.Role != user.Role { - log.Tag(tag).Info("Updating provisioned user %s", user.Name) + if err := a.changeProvisionedTx(tx, user.Name, true); err != nil { + return fmt.Errorf("failed to change provisioned status for user %s: %v", user.Name, err) + } + } + if existingUser.Hash != user.Hash { if err := a.changePasswordTx(tx, user.Name, user.Hash, true); err != nil { return fmt.Errorf("failed to change password for provisioned user %s: %v", user.Name, err) } + } + if existingUser.Role != user.Role { if err := a.changeRoleTx(tx, user.Name, user.Role); err != nil { return fmt.Errorf("failed to change role for provisioned user %s: %v", user.Name, err) } @@ -1715,7 +1733,7 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { if _, err := tx.Exec(deleteUserAccessProvisionedQuery); err != nil { return err } - for username, grants := range a.config.ProvisionAccess { + for username, grants := range a.config.Access { for _, grant := range grants { if err := a.allowAccessTx(tx, username, grant.TopicPattern, grant.Permission, true); err != nil { return err diff --git a/user/manager_test.go b/user/manager_test.go index 2ce078f3..d55726a3 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -1102,11 +1102,11 @@ func TestManager_WithProvisionedUsers(t *testing.T) { Filename: f, DefaultAccess: PermissionReadWrite, ProvisionEnabled: true, - ProvisionUsers: []*User{ + Users: []*User{ {Name: "philuser", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, {Name: "philadmin", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, }, - ProvisionAccess: map[string][]*Grant{ + Access: map[string][]*Grant{ "philuser": { {TopicPattern: "stats", Permission: PermissionReadWrite}, {TopicPattern: "secret", Permission: PermissionRead}, @@ -1144,10 +1144,10 @@ func TestManager_WithProvisionedUsers(t *testing.T) { // Re-open the DB (second app start) require.Nil(t, a.db.Close()) - conf.ProvisionUsers = []*User{ + conf.Users = []*User{ {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, } - conf.ProvisionAccess = map[string][]*Grant{ + conf.Access = map[string][]*Grant{ "philuser": { {TopicPattern: "stats12", Permission: PermissionReadWrite}, {TopicPattern: "secret12", Permission: PermissionRead}, @@ -1178,8 +1178,8 @@ func TestManager_WithProvisionedUsers(t *testing.T) { // Re-open the DB again (third app start) require.Nil(t, a.db.Close()) - conf.ProvisionUsers = []*User{} - conf.ProvisionAccess = map[string][]*Grant{} + conf.Users = []*User{} + conf.Access = map[string][]*Grant{} a, err = NewManager(conf) require.Nil(t, err) @@ -1199,8 +1199,8 @@ func TestManager_DoNotUpdateNonProvisionedUsers(t *testing.T) { Filename: f, DefaultAccess: PermissionReadWrite, ProvisionEnabled: true, - ProvisionUsers: []*User{}, - ProvisionAccess: map[string][]*Grant{}, + Users: []*User{}, + Access: map[string][]*Grant{}, } a, err := NewManager(conf) require.Nil(t, err) @@ -1210,10 +1210,10 @@ func TestManager_DoNotUpdateNonProvisionedUsers(t *testing.T) { // Re-open the DB (second app start) require.Nil(t, a.db.Close()) - conf.ProvisionUsers = []*User{ + conf.Users = []*User{ {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, } - conf.ProvisionAccess = map[string][]*Grant{} + conf.Access = map[string][]*Grant{} a, err = NewManager(conf) require.Nil(t, err) From 2578236d8d156b1608afe9ad33a41187f5949eec Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 17:10:37 +0200 Subject: [PATCH 201/378] Docs --- docs/config.md | 60 +++++++++++++++++++++++++++++++++++++++++++---- server/server.yml | 6 ++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/config.md b/docs/config.md index be15c9fc..564478f7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -88,6 +88,7 @@ using Docker Compose (i.e. `docker-compose.yml`): NTFY_CACHE_FILE: /var/lib/ntfy/cache.db NTFY_AUTH_FILE: /var/lib/ntfy/auth.db NTFY_AUTH_DEFAULT_ACCESS: deny-all + NTFY_AUTH_USERS: 'phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' NTFY_BEHIND_PROXY: true NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments NTFY_ENABLE_LOGIN: true @@ -195,12 +196,20 @@ To set up auth, simply **configure the following two options**: * `auth-default-access` defines the default/fallback access if no access control entry is found; it can be set to `read-write` (default), `read-only`, `write-only` or `deny-all`. -Once configured, you can use the `ntfy user` command to [add or modify users](#users-and-roles), and the `ntfy access` command -lets you [modify the access control list](#access-control-list-acl) for specific users and topic patterns. Both of these -commands **directly edit the auth database** (as defined in `auth-file`), so they only work on the server, and only if the user -accessing them has the right permissions. +Once configured, you can use the `ntfy user` command and the `auth-users` config option to [add or modify users](#users-and-roles). +The `ntfy access` command and the `auth-access` option let you [modify the access control list](#access-control-list-acl) for specific users +and topic patterns. + +Both of these commands **directly edit the auth database** (as defined in `auth-file`), so they only work on the server, +and only if the user accessing them has the right permissions. ### Users and roles +Users can be added to the ntfy user database in two different ways + +* [Using the CLI](#users-via-the-cli): Using the `ntfy user` command, you can manually add/update/remove users. +* [In the config](#users-via-the-config): You can provision users in the `server.yml` file via `auth-users` key. + +#### Users via the CLI The `ntfy user` command allows you to add/remove/change users in the ntfy user database, as well as change passwords or roles (`user` or `admin`). In practice, you'll often just create one admin user with `ntfy user add --role=admin ...` and be done with all this (see [example below](#example-private-instance)). @@ -223,10 +232,45 @@ ntfy user change-role phil admin # Make user phil an admin ntfy user change-tier phil pro # Change phil's tier to "pro" ``` +#### Users via the config +As an alternative to manually creating users via the `ntfy user` CLI command, you can provision users declaratively in +the `server.yml` file by adding them to the `auth-users` array. This is useful for general admins, or if you'd like to +deploy your ntfy server via Ansible without manually editing the database. + +The `auth-users` option is a list of users that are automatically created when the server starts. Each entry is defined +in the format `::`. + +Here's an example with two users: `phil` is an admin, `ben` is a regular user. + +=== "Declarative users in /etc/ntfy/server.yml" + ``` yaml + auth-users: + - "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin" + - "ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user" + ``` + +=== "Declarative users via env variables" + ``` + # Comma-separated list, use single quotes to avoid issues with the bcrypt hash + NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user' + ``` + +The bcrypt hash can be created using `ntfy user hash` or an [online bcrypt generator](https://bcrypt-generator.com/) (though +note that you're putting your password in an untrusted website). + +!!! important + Users added declaratively via the config file are marked in the database as "provisioned users". Removing users + from the config file will **delete them from the database** the next time ntfy is restarted. + + Also, users that were originally manually created will be "upgraded" to be provisioned users if they are added to + the config. Adding a user manually, then adding it to the config, and then removing it from the config will hence + lead to the **deletion of that user**. + ### Access control list (ACL) The access control list (ACL) **manages access to topics for non-admin users, and for anonymous access (`everyone`/`*`)**. Each entry represents the access permissions for a user to a specific topic or topic pattern. +#### ACL entries via the CLI The ACL can be displayed or modified with the `ntfy access` command: ``` @@ -282,6 +326,14 @@ User `ben` has three topic-specific entries. He can read, but not write to topic to topic `garagedoor` and all topics starting with the word `alerts` (wildcards). Clients that are not authenticated (called `*`/`everyone`) only have read access to the `announcements` and `server-stats` topics. +#### ACL entries via the config +Alternatively to the `ntfy access` command + ++# - auth-access is a list of access control entries that are automatically created when the server starts. +# Each entry is in the format "::", e.g. "phil:mytopic:rw" or "phil:phil-*:rw". +# + + ### Access tokens In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may diff --git a/server/server.yml b/server/server.yml index 02af7383..0d748640 100644 --- a/server/server.yml +++ b/server/server.yml @@ -82,9 +82,9 @@ # set to "read-write" (default), "read-only", "write-only" or "deny-all". # - auth-startup-queries allows you to run commands when the database is initialized, e.g. to enable # WAL mode. This is similar to cache-startup-queries. See above for details. -# - auth-provision-users is a list of users that are automatically created when the server starts. -# Each entry is in the format "::", e.g. "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user" -# - auth-provision-access is a list of access control entries that are automatically created when the server starts. +# - auth-users is a list of users that are automatically created when the server starts. +# Each entry is in the format "::", e.g. "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user" +# - auth-access is a list of access control entries that are automatically created when the server starts. # Each entry is in the format "::", e.g. "phil:mytopic:rw" or "phil:phil-*:rw". # # Debian/RPM package users: From 0e672286054d4623feb9deb718b3ab1b4e794d8a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 17:18:06 +0200 Subject: [PATCH 202/378] Docs --- docs/config.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/config.md b/docs/config.md index 564478f7..47dbb923 100644 --- a/docs/config.md +++ b/docs/config.md @@ -327,12 +327,37 @@ to topic `garagedoor` and all topics starting with the word `alerts` (wildcards) (called `*`/`everyone`) only have read access to the `announcements` and `server-stats` topics. #### ACL entries via the config -Alternatively to the `ntfy access` command +As an alternative to manually creating ACL entries via the `ntfy access` CLI command, you can provision access control +entries declaratively in the `server.yml` file by adding them to the `auth-access` array, similar to the `auth-users` +option (see [users via the config](#users-via-the-config). -+# - auth-access is a list of access control entries that are automatically created when the server starts. -# Each entry is in the format "::", e.g. "phil:mytopic:rw" or "phil:phil-*:rw". -# +The `auth-access` option is a list of access control entries that are automatically created when the server starts. +Each entry is defined in the format `::`. +Here's an example with several ACL entries: + +=== "Declarative ACL entries in /etc/ntfy/server.yml" + ``` yaml + auth-access: + - "phil:mytopic:rw" + - "ben:alerts-*:rw" + - "ben:system-logs:ro" + - "*:announcements:ro" # or: "everyone:announcements,ro" + ``` + +=== "Declarative ACL entries via env variables" + ``` + # Comma-separated list + NTFY_AUTH_ACCESS='phil:mytopic:rw,ben:alerts-*:rw,ben:system-logs:ro,*:announcements:ro' + ``` + +The `` can be any existing user, or `everyone`/`*` for anonymous access. The `` can be a specific +topic name or a pattern with wildcards (`*`). The `` can be one of the following: + +* `read-write` or `rw`: Allows both publishing to and subscribing to the topic +* `read-only`, `read`, or `ro`: Allows only subscribing to the topic +* `write-only`, `write`, or `wo`: Allows only publishing to the topic +* `deny-all`, `deny`, or `none`: Denies all access to the topic ### Access tokens In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful From 07e9670a0966d16aca7967cc269a14b5bf4a13c3 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 22:33:29 +0200 Subject: [PATCH 203/378] Fix bug in test --- cmd/access.go | 4 +-- docs/releases.md | 3 +- user/manager.go | 27 ++++++++++++---- user/manager_test.go | 77 ++++++++++++++++++++++++++++++++++++-------- 4 files changed, 88 insertions(+), 23 deletions(-) diff --git a/cmd/access.go b/cmd/access.go index 10247b5f..f2916f51 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -197,7 +197,7 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error } provisioned := "" if u.Provisioned { - provisioned = ", provisioned user" + provisioned = ", server config" } fmt.Fprintf(c.App.ErrWriter, "user %s (role: %s, tier: %s%s)\n", u.Name, u.Role, tier, provisioned) if u.Role == user.RoleAdmin { @@ -206,7 +206,7 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error for _, grant := range grants { grantProvisioned := "" if grant.Provisioned { - grantProvisioned = ", provisioned access entry" + grantProvisioned = " (server config)" } if grant.Permission.IsReadWrite() { fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s%s\n", grant.TopicPattern, grantProvisioned) diff --git a/docs/releases.md b/docs/releases.md index 6171dcff..4f79f544 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1456,7 +1456,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* Enhanced JSON webhook support via [pre-defined](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) ([#1390](https://github.com/binwiederhier/ntfy/pull/1390)) +* [Declarative users and ACL entries](config.md#users-and-roles) ([#464](https://github.com/binwiederhier/ntfy/issues/464), [#1384](https://github.com/binwiederhier/ntfy/pull/1384), thanks to [pinpox](https://github.com/pinpox) for reporting, to [@wunter8](https://github.com/wunter8) for reviewing) +* [Pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support ([#1390](https://github.com/binwiederhier/ntfy/pull/1390)) * Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work) ### ntfy Android app v1.16.1 (UNRELEASED) diff --git a/user/manager.go b/user/manager.go index 5418f534..36a22dd9 100644 --- a/user/manager.go +++ b/user/manager.go @@ -1484,19 +1484,25 @@ func (a *Manager) allowAccessTx(tx *sql.Tx, username string, topicPattern string // ResetAccess removes an access control list entry for a specific username/topic, or (if topic is // empty) for an entire user. The parameter topicPattern may include wildcards (*). func (a *Manager) ResetAccess(username string, topicPattern string) error { + return execTx(a.db, func(tx *sql.Tx) error { + return a.resetAccessTx(tx, username, topicPattern) + }) +} + +func (a *Manager) resetAccessTx(tx *sql.Tx, username string, topicPattern string) error { if !AllowedUsername(username) && username != Everyone && username != "" { return ErrInvalidArgument } else if !AllowedTopicPattern(topicPattern) && topicPattern != "" { return ErrInvalidArgument } if username == "" && topicPattern == "" { - _, err := a.db.Exec(deleteAllAccessQuery, username) + _, err := tx.Exec(deleteAllAccessQuery, username) return err } else if topicPattern == "" { - _, err := a.db.Exec(deleteUserAccessQuery, username, username) + _, err := tx.Exec(deleteUserAccessQuery, username, username) return err } - _, err := a.db.Exec(deleteTopicAccessQuery, username, username, toSQLWildcard(topicPattern)) + _, err := tx.Exec(deleteTopicAccessQuery, username, username, toSQLWildcard(topicPattern)) return err } @@ -1734,7 +1740,18 @@ func (a *Manager) maybeProvisionUsersAndAccess() error { return err } for username, grants := range a.config.Access { + user, exists := util.Find(a.config.Users, func(u *User) bool { + return u.Name == username + }) + if !exists && username != Everyone { + return fmt.Errorf("user %s is not a provisioned user, refusing to add ACL entry", username) + } else if user != nil && user.Role == RoleAdmin { + return fmt.Errorf("adding access control entries is not allowed for admin roles for user %s", username) + } for _, grant := range grants { + if err := a.resetAccessTx(tx, username, grant.TopicPattern); err != nil { + return fmt.Errorf("failed to reset access for user %s and topic %s: %v", username, grant.TopicPattern, err) + } if err := a.allowAccessTx(tx, username, grant.TopicPattern, grant.Permission, true); err != nil { return err } @@ -1951,10 +1968,8 @@ func execTx(db *sql.DB, f func(tx *sql.Tx) error) error { if err != nil { return err } + defer tx.Rollback() if err := f(tx); err != nil { - if e := tx.Rollback(); e != nil { - return err - } return err } return tx.Commit() diff --git a/user/manager_test.go b/user/manager_test.go index d55726a3..297263e9 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -1193,37 +1193,86 @@ func TestManager_WithProvisionedUsers(t *testing.T) { require.Equal(t, "*", users[1].Name) } -func TestManager_DoNotUpdateNonProvisionedUsers(t *testing.T) { +func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) { f := filepath.Join(t.TempDir(), "user.db") conf := &Config{ Filename: f, DefaultAccess: PermissionReadWrite, ProvisionEnabled: true, Users: []*User{}, - Access: map[string][]*Grant{}, + Access: map[string][]*Grant{ + Everyone: { + {TopicPattern: "food", Permission: PermissionRead}, + }, + }, } a, err := NewManager(conf) require.Nil(t, err) // Manually add user require.Nil(t, a.AddUser("philuser", "manual", RoleUser, false)) + require.Nil(t, a.AllowAccess("philuser", "stats", PermissionReadWrite)) + require.Nil(t, a.AllowAccess("philuser", "food", PermissionReadWrite)) - // Re-open the DB (second app start) - require.Nil(t, a.db.Close()) - conf.Users = []*User{ - {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, - } - conf.Access = map[string][]*Grant{} - a, err = NewManager(conf) - require.Nil(t, err) - - // Check that the provisioned users are there users, err := a.Users() require.Nil(t, err) require.Len(t, users, 2) require.Equal(t, "philuser", users[0].Name) - require.Equal(t, RoleUser, users[0].Role) // Should not have been updated - require.NotEqual(t, "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash) + require.Equal(t, RoleUser, users[0].Role) + require.False(t, users[0].Provisioned) // Manually added + + grants, err := a.Grants("philuser") + require.Nil(t, err) + require.Equal(t, 2, len(grants)) + require.Equal(t, "stats", grants[0].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[0].Permission) + require.False(t, grants[0].Provisioned) // Manually added + require.Equal(t, "food", grants[1].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[1].Permission) + require.False(t, grants[1].Provisioned) // Manually added + + grants, err = a.Grants(Everyone) + require.Nil(t, err) + require.Equal(t, 1, len(grants)) + require.Equal(t, "food", grants[0].TopicPattern) + require.Equal(t, PermissionRead, grants[0].Permission) + require.True(t, grants[0].Provisioned) // Provisioned entry + + // Re-open the DB (second app start) + require.Nil(t, a.db.Close()) + conf.Users = []*User{ + {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, + } + conf.Access = map[string][]*Grant{ + "philuser": { + {TopicPattern: "stats", Permission: PermissionReadWrite}, + }, + } + a, err = NewManager(conf) + require.Nil(t, err) + + // Check that the user was "upgraded" to a provisioned user + users, err = a.Users() + require.Nil(t, err) + require.Len(t, users, 2) + require.Equal(t, "philuser", users[0].Name) + require.Equal(t, RoleUser, users[0].Role) + require.Equal(t, "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash) + require.True(t, users[0].Provisioned) // Updated to provisioned! + + grants, err = a.Grants("philuser") + require.Nil(t, err) + require.Equal(t, 2, len(grants)) + require.Equal(t, "stats", grants[0].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[0].Permission) + require.True(t, grants[0].Provisioned) // Updated to provisioned! + require.Equal(t, "food", grants[1].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[1].Permission) + require.False(t, grants[1].Provisioned) // Manually added grants stay! + + grants, err = a.Grants(Everyone) + require.Nil(t, err) + require.Empty(t, grants) } func TestToFromSQLWildcard(t *testing.T) { From 149c13e9d89cede63e105990e6f6fd88d8cf22de Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 27 Jul 2025 22:38:12 +0200 Subject: [PATCH 204/378] Update config to reference declarative users --- docs/config.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/config.md b/docs/config.md index 47dbb923..587e8844 100644 --- a/docs/config.md +++ b/docs/config.md @@ -393,23 +393,17 @@ Once an access token is created, you can **use it to authenticate against the nt subscribe to topics**. To learn how, check out [authenticate via access tokens](publish.md#access-tokens). ### Example: Private instance -The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`: +The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`, +and to configure a single admin user in the `auth-users` section (see [Users via the config](#users-via-the-config)). === "/etc/ntfy/server.yml" ``` yaml auth-file: "/var/lib/ntfy/user.db" auth-default-access: "deny-all" + auth-users: + - "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin" ``` -After that, simply create an `admin` user: - -``` -$ ntfy user add --role=admin phil -password: mypass -confirm: mypass -user phil added with role admin -``` - Once you've done that, you can publish and subscribe using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password. Here's a simple example: From f8082d94811a517bcfc3abf97e5b9cdda573bbf5 Mon Sep 17 00:00:00 2001 From: timof <54764164+timofej673@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:12:45 +0400 Subject: [PATCH 205/378] Update message_cache.go --- server/message_cache.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/message_cache.go b/server/message_cache.go index a73650ad..1ab77b81 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -525,6 +525,8 @@ func (c *messageCache) Message(id string) (*message, error) { } func (c *messageCache) MarkPublished(m *message) error { + c.mu.Lock() + defer c.mu.Unlock() _, err := c.db.Exec(updateMessagePublishedQuery, m.ID) return err } @@ -570,6 +572,8 @@ func (c *messageCache) Topics() (map[string]*topic, error) { } func (c *messageCache) DeleteMessages(ids ...string) error { + c.mu.Lock() + defer c.mu.Unlock() tx, err := c.db.Begin() if err != nil { return err @@ -584,6 +588,8 @@ func (c *messageCache) DeleteMessages(ids ...string) error { } func (c *messageCache) ExpireMessages(topics ...string) error { + c.mu.Lock() + defer c.mu.Unlock() tx, err := c.db.Begin() if err != nil { return err @@ -618,6 +624,8 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) { } func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error { + c.mu.Lock() + defer c.mu.Unlock() tx, err := c.db.Begin() if err != nil { return err @@ -763,6 +771,8 @@ func readMessage(rows *sql.Rows) (*message, error) { } func (c *messageCache) UpdateStats(messages int64) error { + c.mu.Lock() + defer c.mu.Unlock() _, err := c.db.Exec(updateStatsQuery, messages) return err } From 23ec7702fce5690d1c0f55d61e8e02d5ba93d43a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 31 Jul 2025 07:08:35 +0200 Subject: [PATCH 206/378] Add "auth-tokens" --- cmd/serve.go | 56 ++++++++++-- cmd/token.go | 29 ++++-- server/config.go | 1 + server/server.go | 1 + server/server.yml | 2 + server/server_account.go | 2 +- server/server_account_test.go | 2 +- user/manager.go | 165 ++++++++++++++++++++++++---------- user/manager_test.go | 69 +++++++++----- user/types.go | 24 +++-- 10 files changed, 263 insertions(+), 88 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index dc503ccc..36bef4bd 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -50,6 +50,7 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-access", Aliases: []string{"auth_access"}, EnvVars: []string{"NTFY_AUTH_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-tokens", Aliases: []string{"auth_tokens"}, EnvVars: []string{"NTFY_AUTH_TOKENS"}, Usage: "pre-provisioned declarative access tokens"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), @@ -158,6 +159,7 @@ func execServe(c *cli.Context) error { authDefaultAccess := c.String("auth-default-access") authUsersRaw := c.StringSlice("auth-users") authAccessRaw := c.StringSlice("auth-access") + authTokensRaw := c.StringSlice("auth-tokens") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -361,6 +363,10 @@ func execServe(c *cli.Context) error { if err != nil { return err } + authTokens, err := parseTokens(authUsers, authTokensRaw) + if err != nil { + return err + } // Special case: Unset default if listenHTTP == "-" { @@ -418,6 +424,7 @@ func execServe(c *cli.Context) error { conf.AuthDefault = authDefault conf.AuthUsers = authUsers conf.AuthAccess = authAccess + conf.AuthTokens = authTokens conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit @@ -532,7 +539,7 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) { } func parseUsers(usersRaw []string) ([]*user.User, error) { - provisionUsers := make([]*user.User, 0) + users := make([]*user.User, 0) for _, userLine := range usersRaw { parts := strings.Split(userLine, ":") if len(parts) != 3 { @@ -548,19 +555,19 @@ func parseUsers(usersRaw []string) ([]*user.User, error) { } else if !user.AllowedRole(role) { return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) } - provisionUsers = append(provisionUsers, &user.User{ + users = append(users, &user.User{ Name: username, Hash: passwordHash, Role: role, Provisioned: true, }) } - return provisionUsers, nil + return users, nil } -func parseAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[string][]*user.Grant, error) { +func parseAccess(users []*user.User, accessRaw []string) (map[string][]*user.Grant, error) { access := make(map[string][]*user.Grant) - for _, accessLine := range provisionAccessRaw { + for _, accessLine := range accessRaw { parts := strings.Split(accessLine, ":") if len(parts) != 3 { return nil, fmt.Errorf("invalid auth-access: %s, expected format: 'user:topic:permission'", accessLine) @@ -569,7 +576,7 @@ func parseAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[ if username == userEveryone { username = user.Everyone } - provisionUser, exists := util.Find(provisionUsers, func(u *user.User) bool { + u, exists := util.Find(users, func(u *user.User) bool { return u.Name == username }) if username != user.Everyone { @@ -577,7 +584,7 @@ func parseAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[ return nil, fmt.Errorf("invalid auth-access: %s, user %s is not provisioned", accessLine, username) } else if !user.AllowedUsername(username) { return nil, fmt.Errorf("invalid auth-access: %s, username %s invalid", accessLine, username) - } else if provisionUser.Role != user.RoleUser { + } else if u.Role != user.RoleUser { return nil, fmt.Errorf("invalid auth-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username) } } @@ -601,6 +608,41 @@ func parseAccess(provisionUsers []*user.User, provisionAccessRaw []string) (map[ return access, nil } +func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Token, error) { + tokens := make(map[string][]*user.Token) + for _, tokenLine := range tokensRaw { + parts := strings.Split(tokenLine, ":") + if len(parts) < 2 || len(parts) > 3 { + return nil, fmt.Errorf("invalid auth-tokens: %s, expected format: 'user:token[:label]'", tokenLine) + } + username := strings.TrimSpace(parts[0]) + _, exists := util.Find(users, func(u *user.User) bool { + return u.Name == username + }) + if !exists { + return nil, fmt.Errorf("invalid auth-tokens: %s, user %s is not provisioned", tokenLine, username) + } else if !user.AllowedUsername(username) { + return nil, fmt.Errorf("invalid auth-tokens: %s, username %s invalid", tokenLine, username) + } + token := strings.TrimSpace(parts[1]) + if !user.AllowedToken(token) { + return nil, fmt.Errorf("invalid auth-tokens: %s, token %s invalid, use 'ntfy token generate' to generate a random token", tokenLine, token) + } + var label string + if len(parts) > 2 { + label = parts[2] + } + if _, exists := tokens[username]; !exists { + tokens[username] = make([]*user.Token, 0) + } + tokens[username] = append(tokens[username], &user.Token{ + Value: token, + Label: label, + }) + } + return tokens, nil +} + func reloadLogLevel(inputSource altsrc.InputSourceContext) error { newLevelStr, err := inputSource.String("log-level") if err != nil { diff --git a/cmd/token.go b/cmd/token.go index cb92a130..25399c89 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -72,6 +72,15 @@ Example: This is a server-only command. It directly reads from user.db as defined in the server config file server.yml. The command only works if 'auth-file' is properly defined.`, }, + { + Name: "generate", + Usage: "Generates a random token", + Action: execTokenGenerate, + Description: `Randomly generate a token to be used in provisioned tokens. + +This command only generates the token value, but does not persist it anywhere. +The output can be used in the 'auth-tokens' config option.`, + }, }, Description: `Manage access tokens for individual users. @@ -112,12 +121,12 @@ func execTokenAdd(c *cli.Context) error { return err } u, err := manager.User(username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } else if err != nil { return err } - token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified()) + token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified(), false) if err != nil { return err } @@ -141,7 +150,7 @@ func execTokenDel(c *cli.Context) error { return err } u, err := manager.User(username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } else if err != nil { return err @@ -165,7 +174,7 @@ func execTokenList(c *cli.Context) error { var users []*user.User if username != "" { u, err := manager.User(username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return fmt.Errorf("user %s does not exist", username) } else if err != nil { return err @@ -191,7 +200,7 @@ func execTokenList(c *cli.Context) error { usersWithTokens++ fmt.Fprintf(c.App.ErrWriter, "user %s\n", u.Name) for _, t := range tokens { - var label, expires string + var label, expires, provisioned string if t.Label != "" { label = fmt.Sprintf(" (%s)", t.Label) } @@ -200,7 +209,10 @@ func execTokenList(c *cli.Context) error { } else { expires = fmt.Sprintf("expires %s", t.Expires.Format(time.RFC822)) } - fmt.Fprintf(c.App.ErrWriter, "- %s%s, %s, accessed from %s at %s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822)) + if t.Provisioned { + provisioned = " (server config)" + } + fmt.Fprintf(c.App.ErrWriter, "- %s%s, %s, accessed from %s at %s%s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822), provisioned) } } if usersWithTokens == 0 { @@ -208,3 +220,8 @@ func execTokenList(c *cli.Context) error { } return nil } + +func execTokenGenerate(c *cli.Context) error { + fmt.Println(user.GenerateToken()) + return nil +} diff --git a/server/config.go b/server/config.go index 99d829b2..6a7c4cee 100644 --- a/server/config.go +++ b/server/config.go @@ -97,6 +97,7 @@ type Config struct { AuthDefault user.Permission AuthUsers []*user.User AuthAccess map[string][]*user.Grant + AuthTokens map[string][]*user.Token AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration AttachmentCacheDir string diff --git a/server/server.go b/server/server.go index 55fa3af7..05b5b63a 100644 --- a/server/server.go +++ b/server/server.go @@ -203,6 +203,7 @@ func New(conf *Config) (*Server, error) { ProvisionEnabled: true, // Enable provisioning of users and access Users: conf.AuthUsers, Access: conf.AuthAccess, + Tokens: conf.AuthTokens, BcryptCost: conf.AuthBcryptCost, QueueWriterInterval: conf.AuthStatsQueueWriterInterval, } diff --git a/server/server.yml b/server/server.yml index 0d748640..2a623bc4 100644 --- a/server/server.yml +++ b/server/server.yml @@ -86,6 +86,8 @@ # Each entry is in the format "::", e.g. "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user" # - auth-access is a list of access control entries that are automatically created when the server starts. # Each entry is in the format "::", e.g. "phil:mytopic:rw" or "phil:phil-*:rw". +# - auth-tokens is a list of access tokens that are automatically created when the server starts. +# Each entry is in the format ":[:
-You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and -from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that -the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347). +You can get the Android app from [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy), +[F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/), or via the APKs from [GitHub Releases](https://github.com/binwiederhier/ntfy-android/releases). +The Google Play and F-Droid releases are largely identical, with the one exception that the F-Droid flavor does not use Firebase. +The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347). Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app. The PWA is a website that you can add to your home screen, and it will behave just like a native app. +If you're downloading the APKs from [GitHub](https://github.com/binwiederhier/ntfy-android/releases), they are signed with +a certificate with the following SHA-256 fingerprint: `6e145d7ae685eff75468e5067e03a6c3645453343e4e181dac8b6b17ff67489d`. +You can also query the DNS TXT records for `ntfy.sh` to find this fingerprint. + ## Overview A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them. From 546c94ba98874025cc203fc0f0cb55aa406eae92 Mon Sep 17 00:00:00 2001 From: Alex Gaudon Date: Tue, 23 Sep 2025 13:06:32 -0230 Subject: [PATCH 258/378] Added ntfy-bridge to integrations.md --- docs/integrations.md | 65 ++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/docs/integrations.md b/docs/integrations.md index af983c6e..3718c386 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -63,7 +63,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [FindMyDevice](https://gitlab.com/Nulide/findmydevice/) - Find your Device with an SMS or online with the help of FMDServer - [Tox Push Message App](https://github.com/zoff99/tox_push_msg_app) - Tox Push Message App -## Libraries +## Libraries - [ntfy-php-library](https://github.com/VerifiedJoseph/ntfy-php-library) - PHP library for sending messages using a ntfy server (PHP) - [ntfy-notifier](https://github.com/DAcodedBEAT/ntfy-notifier) - Symfony Notifier integration for ntfy (PHP) @@ -98,7 +98,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyexec](https://github.com/alecthomas/ntfyexec) - Send a notification through ntfy.sh if a command fails - [Ntfy Desktop](https://github.com/emmaexe/ntfyDesktop) - Fully featured desktop client for Linux, built with Qt and C++. -## Projects + scripts +## Projects + scripts - [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust) - [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go) @@ -110,8 +110,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs) - [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell) - [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP) -- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C) -- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell) +- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C) +- [ntfy.sh \*arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell) - [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python) - [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python) - [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python) @@ -135,7 +135,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP) - [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy - [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust) -- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost +- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost - [woodpecker-ntfy](https://codeberg.org/l-x/woodpecker-ntfy)- Woodpecker CI plugin for sending ntfy notfication from a pipeline (Go) - [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell) - [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java) @@ -165,7 +165,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell) - [jetspotter](https://github.com/vvanouytsel/jetspotter) - send notifications when planes are spotted near you (Go) - [monitoring_ntfy](https://www.drupal.org/project/monitoring_ntfy) - Drupal monitoring Ntfy.sh integration (PHP/Drupal) -- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust) +- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust) - [notify-via-ntfy](https://exchange.checkmk.com/p/notify-via-ntfy) - Checkmk plugin to send notifications via ntfy (Python) - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) @@ -178,6 +178,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. - [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform - [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy +- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy. ## Blog + forum posts @@ -204,7 +205,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Easy Push Notifications With ntfy.sh](https://runtimeterror.dev/easy-push-notifications-with-ntfy/) ⭐ - runtimeterror.dev - 9/2023 - [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023 - [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023 -- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023 +- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023 - [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023 - [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023 - [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023 @@ -220,36 +221,36 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023 - [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023 - [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023 -- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023 -- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023 -- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023 +- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023 +- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023 +- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023 - [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023 -- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023 -- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023 -- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023 -- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023 -- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023 -- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023 +- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023 +- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023 +- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023 +- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023 +- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023 +- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023 - [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023 -- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023 -- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023 -- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023 -- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023 +- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023 +- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023 +- [Carnet IP 动态解析以及通过 ntfy 推送 IP 信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023 +- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023 - [Video: Simple Push Notifications ntfy](https://www.youtube.com/watch?v=u9EcWrsjE20) ⭐ - youtube.com - 2/2023 -- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023 -- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023 +- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023 +- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023 - [January 2023 Developer Update](https://community.nodebb.org/topic/16908/january-2023-developer-update) - nodebb.org - 1/2023 - [Comment envoyer des notifications push sur votre téléphone facilement et gratuitement?](https://korben.info/notifications-push-telephone.html) - 1/2023 - [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022 - [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022 -- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022 +- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022 - [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022 - [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022 - [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022 - [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022 -- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022 +- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022 - [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022 -- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022 +- [How to make my phone buzz\*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022 - [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022 - [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022 - [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022 @@ -261,20 +262,20 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - blog.parravidales.es - 11/2022 - [unRAID Notifications with ntfy.sh](https://lder.dev/posts/ntfy-Notifications-With-unRAID/) - lder.dev - 10/2022 - [Zero-cost push notifications to your phone or desktop via PUT/POST ](https://lobste.rs/s/41dq13/zero_cost_push_notifications_your_phone) - lobste.rs - 10/2022 -- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022 +- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022 - [Alarmanlage der dritten Art (YouTube video)](https://www.youtube.com/watch?v=altb5QLHbaU&feature=youtu.be) - youtube.com - 10/2022 - [Neue Services: Ntfy, TikTok und RustDesk](https://adminforge.de/tools/neue-services-ntfy-tiktok-und-rustdesk/) - adminforge.de - 9/2022 - [Ntfy, le service de notifications qu’il vous faut](https://www.cachem.fr/ntfy-le-service-de-notifications-quil-vous-faut/) - cachem.fr - 9/2022 -- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/2022 +- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/2022 - [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - thejeshgn.com - 8/2022 - [Fedora Magazine | 4 cool new projects to try in Copr](https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/) - fedoramagazine.org - 8/2022 - [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - svrforum.com - 8/2022 - [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - sometimesir.com - 6/2022 - [ntfy is finally coming to iOS, and Matrix/UnifiedPush gateway support](https://www.reddit.com/r/selfhosted/comments/vdzvxi/ntfy_is_finally_coming_to_ios_with_full/) ⭐ - reddit.com - 6/2022 - [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - chowdera.com - 5/2022 -- [无需注册的通知服务ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - blog.csdn.net - 5/2022 +- [无需注册的通知服务 ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - blog.csdn.net - 5/2022 - [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - jlelse.blog - 4/2022 -- [Using ntfy and Tasker together](https://lachlanlife.net/posts/2022-04-tasker-ntfy/) - lachlanlife.net - 4/2022 +- [Using ntfy and Tasker together](https://lachlanlife.net/posts/2022-04-tasker-ntfy/) - lachlanlife.net - 4/2022 - [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - reddit.com - 4/2022 - [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた](https://gigazine.net/news/20220404-ntfy-push-notification/) - gigazine.net - 4/2022 - [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - pocketmags.com - 3/2022 @@ -295,9 +296,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the ntfy community. Thanks to everyone running a public server. **You guys rock!** -| URL | Country | -|---------------------------------------------------|--------------------| -| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States | +| URL | Country | +| ------------------------------------------------- | ---------------- | +| [ntfy.sh](https://ntfy.sh/) (_Official_) | 🇺🇸 United States | | [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France | | [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland | | [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany | From 364696e0595fdde98c5a09fda934ca1518f6b11d Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Tue, 23 Sep 2025 12:34:28 -0400 Subject: [PATCH 259/378] Remove F-Droid upgrade warning for ntfy Removed warning about broken ntfy version on F-Droid and related instructions. --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index d9ad43ba..9942e138 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,3 @@ -# ⚠️ F-Droid: Do not upgrade to ntfy 1.17.0 - -9/20/2025 - -📢 **BROKEN ntfy version on F-Droid**: I am so sorry, but it looks like ntfy 1.17.0 got released on F-Droid accidentally. Please **DO NOT UPDATE**, as the app will crash on startup and will not work. - -You can join the Google Play beta to upgrade to 1.17.8 (in testing!!) or manually install the .apk file from https://github.com/binwiederhier/ntfy-android/releases/tag/v1.17.8: - -- Join beta from Android: https://play.google.com/store/apps/details?id=io.heckel.ntfy -- Joined beta from Web: https://play.google.com/apps/testing/io.heckel.ntfy - -My sincere apologies. I forgot that F-Droid automatically picks up tags. It's been a while. I suggest that you use the Backup feature in ntfy (Settings -> Back up to file) to save your current database to a json file, just in case. - -F-Droid release cycles take a while, so this will take a few days to get fixed. I am very sorry, guys. - ---- - ![ntfy](web/public/static/images/ntfy.png) # ntfy.sh | Send push notifications to your phone or desktop via PUT/POST From 83e74b014e69fc95952942cd9aaf55c8b6ce1c99 Mon Sep 17 00:00:00 2001 From: Alex Gaudon Date: Tue, 23 Sep 2025 15:10:46 -0230 Subject: [PATCH 260/378] Revert "Added ntfy-bridge to integrations.md" This reverts commit 546c94ba98874025cc203fc0f0cb55aa406eae92. --- docs/integrations.md | 65 ++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/docs/integrations.md b/docs/integrations.md index 3718c386..af983c6e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -63,7 +63,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [FindMyDevice](https://gitlab.com/Nulide/findmydevice/) - Find your Device with an SMS or online with the help of FMDServer - [Tox Push Message App](https://github.com/zoff99/tox_push_msg_app) - Tox Push Message App -## Libraries +## Libraries - [ntfy-php-library](https://github.com/VerifiedJoseph/ntfy-php-library) - PHP library for sending messages using a ntfy server (PHP) - [ntfy-notifier](https://github.com/DAcodedBEAT/ntfy-notifier) - Symfony Notifier integration for ntfy (PHP) @@ -98,7 +98,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyexec](https://github.com/alecthomas/ntfyexec) - Send a notification through ntfy.sh if a command fails - [Ntfy Desktop](https://github.com/emmaexe/ntfyDesktop) - Fully featured desktop client for Linux, built with Qt and C++. -## Projects + scripts +## Projects + scripts - [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust) - [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go) @@ -110,8 +110,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs) - [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell) - [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP) -- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C) -- [ntfy.sh \*arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell) +- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C) +- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell) - [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python) - [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python) - [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python) @@ -135,7 +135,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP) - [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy - [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust) -- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost +- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost - [woodpecker-ntfy](https://codeberg.org/l-x/woodpecker-ntfy)- Woodpecker CI plugin for sending ntfy notfication from a pipeline (Go) - [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell) - [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java) @@ -165,7 +165,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell) - [jetspotter](https://github.com/vvanouytsel/jetspotter) - send notifications when planes are spotted near you (Go) - [monitoring_ntfy](https://www.drupal.org/project/monitoring_ntfy) - Drupal monitoring Ntfy.sh integration (PHP/Drupal) -- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust) +- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust) - [notify-via-ntfy](https://exchange.checkmk.com/p/notify-via-ntfy) - Checkmk plugin to send notifications via ntfy (Python) - [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) - [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell) @@ -178,7 +178,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. - [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform - [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy -- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy. ## Blog + forum posts @@ -205,7 +204,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Easy Push Notifications With ntfy.sh](https://runtimeterror.dev/easy-push-notifications-with-ntfy/) ⭐ - runtimeterror.dev - 9/2023 - [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023 - [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023 -- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023 +- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023 - [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023 - [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023 - [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023 @@ -221,36 +220,36 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023 - [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023 - [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023 -- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023 -- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023 -- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023 +- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023 +- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023 +- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023 - [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023 -- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023 -- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023 -- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023 -- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023 -- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023 -- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023 +- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023 +- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023 +- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023 +- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023 +- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023 +- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023 - [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023 -- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023 -- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023 -- [Carnet IP 动态解析以及通过 ntfy 推送 IP 信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023 -- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023 +- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023 +- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023 +- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023 +- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023 - [Video: Simple Push Notifications ntfy](https://www.youtube.com/watch?v=u9EcWrsjE20) ⭐ - youtube.com - 2/2023 -- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023 -- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023 +- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023 +- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023 - [January 2023 Developer Update](https://community.nodebb.org/topic/16908/january-2023-developer-update) - nodebb.org - 1/2023 - [Comment envoyer des notifications push sur votre téléphone facilement et gratuitement?](https://korben.info/notifications-push-telephone.html) - 1/2023 - [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022 - [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022 -- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022 +- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022 - [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022 - [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022 - [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022 - [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022 -- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022 +- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022 - [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022 -- [How to make my phone buzz\*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022 +- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022 - [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022 - [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022 - [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022 @@ -262,20 +261,20 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - blog.parravidales.es - 11/2022 - [unRAID Notifications with ntfy.sh](https://lder.dev/posts/ntfy-Notifications-With-unRAID/) - lder.dev - 10/2022 - [Zero-cost push notifications to your phone or desktop via PUT/POST ](https://lobste.rs/s/41dq13/zero_cost_push_notifications_your_phone) - lobste.rs - 10/2022 -- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022 +- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022 - [Alarmanlage der dritten Art (YouTube video)](https://www.youtube.com/watch?v=altb5QLHbaU&feature=youtu.be) - youtube.com - 10/2022 - [Neue Services: Ntfy, TikTok und RustDesk](https://adminforge.de/tools/neue-services-ntfy-tiktok-und-rustdesk/) - adminforge.de - 9/2022 - [Ntfy, le service de notifications qu’il vous faut](https://www.cachem.fr/ntfy-le-service-de-notifications-quil-vous-faut/) - cachem.fr - 9/2022 -- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/2022 +- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/2022 - [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - thejeshgn.com - 8/2022 - [Fedora Magazine | 4 cool new projects to try in Copr](https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/) - fedoramagazine.org - 8/2022 - [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - svrforum.com - 8/2022 - [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - sometimesir.com - 6/2022 - [ntfy is finally coming to iOS, and Matrix/UnifiedPush gateway support](https://www.reddit.com/r/selfhosted/comments/vdzvxi/ntfy_is_finally_coming_to_ios_with_full/) ⭐ - reddit.com - 6/2022 - [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - chowdera.com - 5/2022 -- [无需注册的通知服务 ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - blog.csdn.net - 5/2022 +- [无需注册的通知服务ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - blog.csdn.net - 5/2022 - [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - jlelse.blog - 4/2022 -- [Using ntfy and Tasker together](https://lachlanlife.net/posts/2022-04-tasker-ntfy/) - lachlanlife.net - 4/2022 +- [Using ntfy and Tasker together](https://lachlanlife.net/posts/2022-04-tasker-ntfy/) - lachlanlife.net - 4/2022 - [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - reddit.com - 4/2022 - [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた](https://gigazine.net/news/20220404-ntfy-push-notification/) - gigazine.net - 4/2022 - [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - pocketmags.com - 3/2022 @@ -296,9 +295,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the ntfy community. Thanks to everyone running a public server. **You guys rock!** -| URL | Country | -| ------------------------------------------------- | ---------------- | -| [ntfy.sh](https://ntfy.sh/) (_Official_) | 🇺🇸 United States | +| URL | Country | +|---------------------------------------------------|--------------------| +| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States | | [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France | | [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland | | [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany | From 61e496fc4cf31696eb6cb2845a90ab43f08bbd5c Mon Sep 17 00:00:00 2001 From: Alex Gaudon Date: Tue, 23 Sep 2025 15:11:03 -0230 Subject: [PATCH 261/378] Save without formatting --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index af983c6e..160f72c4 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -178,6 +178,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. - [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform - [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy +- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy. ## Blog + forum posts From 2804acf0f5bae3cc6fe009f079e097d41edd23d4 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 23 Sep 2025 22:46:23 -0400 Subject: [PATCH 262/378] Update repository docs --- docs/install.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/install.md b/docs/install.md index 3ad34508..e487b525 100644 --- a/docs/install.md +++ b/docs/install.md @@ -65,15 +65,21 @@ deb/rpm packages. ``` ## Debian/Ubuntu repository -Installation via Debian repository: + +!!! info + As of September 2025, **the official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh/apt)**. + The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely + go away soon. I suspect I will phase it out some time in early 2026. + +Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 6B7C CFDB 962D 4F1E C4AF`): === "x86_64/amd64" ```bash sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg + sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg sudo apt install apt-transport-https - sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \ - > /etc/apt/sources.list.d/archive.heckel.io.list" + echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \ + | sudo tee /etc/apt/sources.list.d/ntfy.list sudo apt update sudo apt install ntfy sudo systemctl enable ntfy @@ -83,10 +89,10 @@ Installation via Debian repository: === "armv7/armhf" ```bash sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg + sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg sudo apt install apt-transport-https - sudo sh -c "echo 'deb [arch=armhf signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \ - > /etc/apt/sources.list.d/archive.heckel.io.list" + echo "deb [arch=armhf signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \ + | sudo tee /etc/apt/sources.list.d/ntfy.list sudo apt update sudo apt install ntfy sudo systemctl enable ntfy @@ -96,10 +102,10 @@ Installation via Debian repository: === "arm64" ```bash sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg + sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg sudo apt install apt-transport-https - sudo sh -c "echo 'deb [arch=arm64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \ - > /etc/apt/sources.list.d/archive.heckel.io.list" + echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \ + | sudo tee /etc/apt/sources.list.d/ntfy.list sudo apt update sudo apt install ntfy sudo systemctl enable ntfy From 07ef3e565693f5ca082dfa10ec9dfdb1017e8509 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 23 Sep 2025 22:51:06 -0400 Subject: [PATCH 263/378] Release notes --- docs/releases.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index b7c647c8..1035afea 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,22 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). +### ntfy Android app v1.17.8 +Released September 23, 2025 + +This is largely a maintenance update to ensure the SDK is up-to-date. + +**Features:** + +* Markdown is now rendered if "Markdown: yes" was passed ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@NiNiyas](https://github.com/NiNiyas) for reporting) +* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) + +**Bug fixes + maintenance:** + +* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8)) +* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing) +* Bumped all dependencies to the latest versions (no ticket) + ### ntfy server v2.14.0 Released August 5, 2025 @@ -1476,22 +1492,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** +* The official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh) ([#1357](https://github.com/binwiederhier/ntfy/issues/1357)/[#1401](https://github.com/binwiederhier/ntfy/issues/1401), thanks to [@skibbipl](https://github.com/skibbipl) and [@lduesing](https://github.com/lduesing) for reporting) * Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673)) * Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu) * Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting) - -### ntfy Android app v1.17.0 (UNRELEASED) - -This is largely a maintenance update to ensure the SDK is up-to-date. - -**Features:** - -* Markdown is now rendered if "Markdown: yes" was passed ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@NiNiyas](https://github.com/NiNiyas) for reporting) -* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) - -**Bug fixes + maintenance:** - -* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8)) -* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing) -* Bumped all dependencies to the latest versions (no ticket) From 7615aa86adddc35648ccf13a26cd0f020380e038 Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Wed, 24 Sep 2025 20:46:30 -0400 Subject: [PATCH 264/378] Change supported platforms for Markdown in publish.md Updated supported platforms for Markdown formatting. --- docs/publish.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/publish.md b/docs/publish.md index ca0b5547..ce3500e8 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -625,7 +625,7 @@ them with a comma, e.g. `tag1,tag2,tag3`. or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)). ## Markdown formatting -_Supported on:_ :material-firefox: +_Supported on:_ :material-android: :material-firefox: You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/) 🤩. That means you can use **bold text**, *italicized text*, links, images, and more. Supported Markdown features (web app only for now): From 53dce3013d7565f82c9adbb2412c42848e4d4ddd Mon Sep 17 00:00:00 2001 From: Gringo Date: Fri, 26 Sep 2025 00:50:06 +0200 Subject: [PATCH 265/378] Translated using Weblate (Italian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/ --- web/public/static/langs/it.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index 1ba1eba8..a731c487 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -276,9 +276,9 @@ "account_basics_phone_numbers_dialog_verify_button_sms": "Invia SMS", "account_basics_phone_numbers_no_phone_numbers_yet": "Ancora nessun numero di telefono", "account_basics_phone_numbers_dialog_title": "Aggiungi un numero di telefono", - "account_upgrade_dialog_button_cancel": "Cancella", + "account_upgrade_dialog_button_cancel": "Annulla", "account_upgrade_dialog_billing_contact_website": "Per domande di fatturazione, visita per favore in nostro sito.", - "account_upgrade_dialog_button_cancel_subscription": "Cancella iscrizione", + "account_upgrade_dialog_button_cancel_subscription": "Annulla iscrizione", "account_basics_phone_numbers_description": "Per notifiche via chiamata", "account_basics_phone_numbers_copied_to_clipboard": "Numero di telefono copiato negli appunti", "account_basics_phone_numbers_dialog_number_placeholder": "p. e. +391234567890", @@ -296,7 +296,7 @@ "account_upgrade_dialog_tier_selected_label": "Selezionato", "account_upgrade_dialog_button_update_subscription": "Aggiorna iscrizione", "account_usage_attachment_storage_title": "Archivio allegati", - "account_delete_dialog_description": "Il tuo account sarà permanentemente cancellato assieme a tutti i tuoi dati presenti sul server. Dopo la cancellazione, la tua username non sarà disponibile per 7 giorni. Se desideri davvero procedere, inserisci la tua password nella seguente casella.", + "account_delete_dialog_description": "Il tuo account sarà permanentemente eliminato insieme a tutti i tuoi dati presenti sul server. Dopo l'eliminazione, il tuo nome utente non sarà disponibile per 7 giorni. Se desideri davvero procedere, inserisci la tua password nella seguente casella.", "account_delete_dialog_button_cancel": "Annulla", "account_usage_calls_title": "Chiamate effettuate", "account_delete_description": "Elimina permanentemente il tuo account", From e647c68cb9237d42ddaf42ef066d643ff0e5d468 Mon Sep 17 00:00:00 2001 From: Gringo Date: Mon, 29 Sep 2025 21:53:12 +0200 Subject: [PATCH 266/378] Translated using Weblate (Italian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/ --- web/public/static/langs/it.json | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index a731c487..d02bc913 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -8,11 +8,11 @@ "message_bar_error_publishing": "Errore durante la pubblicazione della notifica", "message_bar_show_dialog": "Mostra la finestra di dialogo di pubblicazione", "message_bar_publish": "Pubblica messaggio", - "nav_topics_title": "Topic a cui si è iscritti", + "nav_topics_title": "Argomenti a cui si è iscritti", "nav_button_all_notifications": "Tutte le notifiche", "nav_button_settings": "Impostazioni", "nav_button_publish_message": "Pubblica notifica", - "nav_button_subscribe": "Iscriviti al topic", + "nav_button_subscribe": "Iscriviti all'argomento", "nav_button_muted": "Notifiche disattivate", "nav_button_connecting": "connessione", "alert_notification_permission_required_title": "Le notifiche sono disabilitate", @@ -31,17 +31,17 @@ "notifications_attachment_open_title": "Vai a {{url}}", "notifications_attachment_open_button": "Apri allegato", "notifications_attachment_link_expires": "Il collegamento scade il {{date}}", - "notifications_attachment_link_expired": "link per il download scaduto", + "notifications_attachment_link_expired": "collegamento per il download scaduto", "notifications_attachment_file_image": "file immagine", "notifications_attachment_file_video": "file video", "action_bar_toggle_mute": "Abilita/disabilita le notifiche", "notifications_attachment_file_document": "altro documento", - "notifications_click_copy_url_button": "Copia link", - "notifications_click_open_button": "Apri link", + "notifications_click_copy_url_button": "Copia collegamento", + "notifications_click_open_button": "Apri collegamento", "notifications_actions_open_url_title": "Vai a {{url}}", "notifications_actions_not_supported": "Azione non supportata nell'app Web", - "notifications_none_for_topic_title": "Non hai ancora ricevuto alcuna notifica per questo topic.", - "notifications_none_for_topic_description": "Per inviare notifiche a questo argomento, è sufficiente PUT o POST all'URL del topic.", + "notifications_none_for_topic_title": "Non hai ancora ricevuto alcuna notifica per questo argomento.", + "notifications_none_for_topic_description": "Per inviare notifiche a questo argomento, è sufficiente PUT o POST all'URL dell'argomento.", "notifications_none_for_any_title": "Non hai ricevuto alcuna notifica.", "notifications_no_subscriptions_title": "Sembra che tu non abbia ancora abbonamenti.", "notifications_example": "Esempio", @@ -63,9 +63,9 @@ "publish_dialog_priority_max": "Max. priorità", "publish_dialog_base_url_label": "URL del servizio", "publish_dialog_base_url_placeholder": "URL del servizio, ad es. https://esempio.com", - "publish_dialog_topic_label": "Nome topic", - "publish_dialog_topic_placeholder": "Nome topic, ad es. avvisi_di_phil", - "publish_dialog_topic_reset": "Reset topic", + "publish_dialog_topic_label": "Nome argomento", + "publish_dialog_topic_placeholder": "Nome argomento, ad es. avvisi_di_phil", + "publish_dialog_topic_reset": "Reimposta argomento", "publish_dialog_title_label": "Titolo", "publish_dialog_title_placeholder": "Titolo della notifica, ad es. Avviso di spazio su disco", "publish_dialog_message_label": "Messaggio", @@ -97,13 +97,13 @@ "publish_dialog_attached_file_remove": "Rimuovi il file allegato", "publish_dialog_drop_file_here": "Trascina il file qui", "emoji_picker_search_clear": "Cancella ricerca", - "subscribe_dialog_subscribe_title": "Iscriviti al topic", + "subscribe_dialog_subscribe_title": "Iscriviti all'argomento", "subscribe_dialog_subscribe_topic_placeholder": "Nome dell'argomento, ad es. avvisi_di_phil", "subscribe_dialog_subscribe_base_url_label": "URL del servizio", "subscribe_dialog_subscribe_button_cancel": "Annulla", "subscribe_dialog_login_title": "Accesso richiesto", "subscribe_dialog_login_username_label": "Nome utente, ad es. phil", - "subscribe_dialog_login_button_login": "Login", + "subscribe_dialog_login_button_login": "Accesso", "subscribe_dialog_error_user_anonymous": "anonimo", "prefs_notifications_sound_title": "Suono di notifica", "prefs_notifications_sound_description_some": "Le notifiche riproducono il suono {{sound}} quando arrivano", @@ -122,7 +122,7 @@ "prefs_notifications_delete_after_one_week_description": "Le notifiche vengono eliminate automaticamente dopo una settimana", "prefs_notifications_delete_after_one_month_description": "Le notifiche vengono eliminate automaticamente dopo un mese", "prefs_users_title": "Gestisci gli utenti", - "prefs_users_description": "Aggiungi/rimuovi utenti per i tuoi topic protetti qui. Tieni presente che nome utente e password sono memorizzati nella memoria locale del browser.", + "prefs_users_description": "Aggiungi/rimuovi utenti per i tuoi argomenti protetti qui. Tieni presente che nome utente e password sono memorizzati nella memoria locale del browser.", "prefs_users_table": "Tabella utenti", "prefs_users_add_button": "Aggiungi utente", "prefs_users_edit_button": "Modifica utente", @@ -158,16 +158,16 @@ "alert_notification_permission_required_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop", "alert_not_supported_title": "Notifiche non supportate", "notifications_attachment_file_app": "file app Android", - "notifications_no_subscriptions_description": "Fai clic sul link \"{{linktext}}\" per creare o iscriverti a un topic. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.", + "notifications_no_subscriptions_description": "Fai clic sul collegamento \"{{linktext}}\" per creare o iscriverti a un argomento. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.", "notifications_attachment_file_audio": "file audio", - "notifications_none_for_any_description": "Per inviare notifiche a un topic, è sufficiente PUT o POST all'URL del topic. Ecco un esempio utilizzando uno dei tuoi topic.", + "notifications_none_for_any_description": "Per inviare notifiche a un argomento, è sufficiente PUT o POST all'URL dell'argomento. Ecco un esempio utilizzando uno dei tuoi argomenti.", "notifications_click_copy_url_title": "Copia l'URL del collegamento negli appunti", "prefs_notifications_sound_description_none": "Le notifiche non emettono alcun suono quando arrivano", "publish_dialog_delay_label": "Ritardo", "publish_dialog_tags_placeholder": "Elenco di tag separato da virgole, ad es. avviso, backup-srv1", "publish_dialog_click_placeholder": "URL che viene aperto quando si fa clic sulla notifica", "publish_dialog_attach_placeholder": "Allega file tramite URL, ad es. https://f-droid.org/F-Droid.apk", - "publish_dialog_chip_topic_label": "Cambia topic", + "publish_dialog_chip_topic_label": "Cambia argomento", "publish_dialog_details_examples_description": "Per esempi e una descrizione dettagliata di tutte le funzioni di invio, fare riferimento alla documentazione.", "publish_dialog_attached_file_filename_placeholder": "Nome file allegato", "emoji_picker_search_placeholder": "Cerca emoji", @@ -177,7 +177,7 @@ "subscribe_dialog_subscribe_button_subscribe": "Iscriviti", "prefs_notifications_sound_play": "Riproduci il suono selezionato", "prefs_notifications_min_priority_title": "Priorità minima", - "subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.", + "subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci nome utente e password per iscriverti.", "common_back": "Indietro", "subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato", "prefs_notifications_title": "Notifiche", @@ -268,7 +268,7 @@ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nessun numero verificato", "account_basics_phone_numbers_title": "Numeri di telefono", "account_basics_phone_numbers_dialog_description": "Per usare la funzionalità di notifica tramite chiamata telefonica, devi aggiungere e verificare almeno un numero di telefono. La verifica può essere fatta tramite SMS o chiamata telefonica.", - "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topic riservato", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} argomento riservato", "account_upgrade_dialog_billing_contact_email": "Per domande di fatturazione, contattaci direttamente.", "account_upgrade_dialog_tier_current_label": "Attuale", "account_basics_phone_numbers_dialog_number_label": "Numero di telefono", @@ -281,8 +281,8 @@ "account_upgrade_dialog_button_cancel_subscription": "Annulla iscrizione", "account_basics_phone_numbers_description": "Per notifiche via chiamata", "account_basics_phone_numbers_copied_to_clipboard": "Numero di telefono copiato negli appunti", - "account_basics_phone_numbers_dialog_number_placeholder": "p. e. +391234567890", - "account_basics_phone_numbers_dialog_code_placeholder": "p. e. 123456", + "account_basics_phone_numbers_dialog_number_placeholder": "es. +391234567890", + "account_basics_phone_numbers_dialog_code_placeholder": "es. 123456", "account_tokens_title": "Token d'accesso", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} all'anno. Addebitato annualmente.", "account_basics_phone_numbers_dialog_channel_call": "Chiama", @@ -326,7 +326,7 @@ "account_tokens_dialog_title_edit": "Modifica token di accesso", "account_tokens_dialog_button_create": "Crea token", "account_tokens_dialog_button_update": "Aggiorna token", - "account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mails giornaliere", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} email giornaliere", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messaggi giornalieri", "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} spazio di archiviazione totale", @@ -348,7 +348,7 @@ "account_tokens_dialog_title_create": "Crea token di accesso", "account_tokens_dialog_button_cancel": "Annulla", "web_push_unknown_notification_body": "Potrebbe essere necessario aggiornare ntfy aprendo l'app web", - "account_upgrade_dialog_proration_info": "Prorata: quando si esegue l'upgrade tra piani a pagamento, la differenza di prezzo verrà addebitata immediatamente. Quando si esegue il downgrade a un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.", + "account_upgrade_dialog_proration_info": "Prorata: quando si esegue l'aggiornamento tra piani a pagamento, la differenza di prezzo verrà addebitata immediatamente. Quando si esegue il ritorna ad un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.", "account_tokens_table_last_access_header": "Ultimo accesso", "account_tokens_table_expires_header": "Scade", "account_tokens_table_never_expires": "Non scade mai", From 1956ffbf020f44ccc1a69b2cd275247fed703b4a Mon Sep 17 00:00:00 2001 From: Liviu Roman Date: Mon, 29 Sep 2025 17:33:47 +0200 Subject: [PATCH 267/378] Translated using Weblate (Romanian) Currently translated at 89.3% (362 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/ --- web/public/static/langs/ro.json | 120 +++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index c309932c..0283c759 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -242,5 +242,123 @@ "account_usage_attachment_storage_title": "Stocare atașamente", "account_usage_basis_ip_description": "Statistica și limitele de utilizare pentru acest cont se bazează pe adresa ta IP, așadar pot fi partajate cu alți utilizatori. Limitele afișate mai sus sunt aproximative, bazate pe limitele de viteză existente.", "account_usage_reservations_none": "Nu există subiecte rezervate pentru acest cont", - "account_basics_tier_canceled_subscription": "Abonamentul tău a fost anulat și va fi retrogradat la un cont gratuit în data de {{date}}." + "account_basics_tier_canceled_subscription": "Abonamentul tău a fost anulat și va fi retrogradat la un cont gratuit în data de {{date}}.", + "account_delete_dialog_label": "Parolă", + "account_delete_dialog_button_cancel": "Anulează", + "account_delete_dialog_button_submit": "Șterge permanent contul", + "account_delete_dialog_billing_warning": "Ștergerea contului tău anulează imediat și abonamentul de facturare. Nu vei mai avea acces la tabloul de bord pentru facturare.", + "account_upgrade_dialog_title": "Schimbă nivelul contului", + "account_upgrade_dialog_interval_monthly": "Lunar", + "account_upgrade_dialog_interval_yearly": "Anual", + "account_upgrade_dialog_interval_yearly_discount_save": "economisești {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "economisești până la {{discount}}%", + "prefs_notifications_title": "Notificări", + "prefs_notifications_sound_description_none": "Notificările nu redau niciun sunet atunci când sosesc", + "prefs_notifications_sound_description_some": "Notificările redau sunetul {{sound}} atunci când sosesc", + "prefs_notifications_min_priority_description_any": "Se afișează toate notificările, indiferent de prioritate", + "prefs_notifications_min_priority_description_x_or_higher": "Afișează notificările dacă prioritatea este {{number}} ({{name}}) sau mai mare", + "prefs_notifications_min_priority_description_max": "Afișează notificări dacă prioritatea este 5 (maxim)", + "prefs_notifications_delete_after_title": "Șterge notificările", + "prefs_notifications_delete_after_never_description": "Notificările nu sunt niciodată șterse automat", + "prefs_notifications_delete_after_three_hours_description": "Notificările sunt șterse automat după trei ore", + "prefs_notifications_delete_after_one_day_description": "Notificările sunt șterse automat după o zi", + "prefs_notifications_delete_after_one_week_description": "Notificările sunt șterse automat după o săptămână", + "prefs_notifications_delete_after_one_month_description": "Notificările sunt șterse automat după o lună", + "prefs_notifications_web_push_title": "Notificări în fundal", + "prefs_notifications_web_push_enabled_description": "Notificările sunt primite chiar și atunci când aplicația web nu rulează (prin Web Push)", + "web_push_subscription_expiring_title": "Notificările vor fi suspendate", + "web_push_subscription_expiring_body": "Deschide ntfy pentru a continua să primești notificări", + "account_upgrade_dialog_tier_features_reservations_one": "subiect rezervat {{reservations}}", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} subiecte rezervate", + "account_upgrade_dialog_tier_features_no_reservations": "Nu există subiecte rezervate", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} mesaj zilnic", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} mesaje zilnice", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mail zilnic", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-mailuri zilnice", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} apeluri telefonice zilnice", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} apeluri telefonice zilnice", + "account_upgrade_dialog_tier_features_no_calls": "Fără apeluri telefonice", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per fișier", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} stocare totală", + "account_upgrade_dialog_tier_price_per_month": "lună", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pe an. Facturat lunar.", + "account_upgrade_dialog_tier_selected_label": "Selectat", + "account_upgrade_dialog_tier_current_label": "Actual", + "account_upgrade_dialog_button_cancel": "Anulează", + "account_upgrade_dialog_button_redirect_signup": "Înscrie-te acum", + "account_upgrade_dialog_button_pay_now": "Plătește acum și abonează-te", + "account_upgrade_dialog_button_cancel_subscription": "Anulează abonamentul", + "account_tokens_table_token_header": "Token", + "account_tokens_table_label_header": "Etichetă", + "account_tokens_table_last_access_header": "Ultimul acces", + "account_tokens_table_expires_header": "Expiră", + "account_tokens_table_never_expires": "Nu expiră niciodată", + "account_tokens_table_current_session": "Sesiunea curentă a browserului", + "account_tokens_table_copied_to_clipboard": "Tokenul de acces a fost copiat", + "account_tokens_table_last_origin_tooltip": "De la adresa IP {{ip}}, faceți clic pentru a căuta", + "account_tokens_dialog_title_create": "Crează un token de acces", + "account_tokens_dialog_title_edit": "Modifică tokenul de acces", + "account_tokens_dialog_title_delete": "Șterge tokenul de acces", + "account_tokens_dialog_label": "Etichetă, de exemplu, notificări Radarr", + "account_tokens_dialog_button_create": "Crează un token", + "account_tokens_dialog_button_update": "Actualizare token", + "account_tokens_dialog_button_cancel": "Anulează", + "account_tokens_dialog_expires_label": "Tokenul de acces expiră în", + "account_tokens_dialog_expires_never": "Tokenul nu expiră niciodată", + "account_tokens_delete_dialog_title": "Șterge tokenul de acces", + "account_tokens_delete_dialog_submit_button": "Șterge definitiv tokenul", + "prefs_notifications_sound_title": "Sunet de notificare", + "prefs_notifications_sound_no_sound": "Niciun sunet", + "prefs_notifications_sound_play": "Redă sunetul selectat", + "prefs_notifications_min_priority_title": "Prioritate minimă", + "prefs_notifications_min_priority_any": "Orice prioritate", + "prefs_notifications_min_priority_low_and_higher": "Prioritate scăzută și mai mare", + "prefs_notifications_min_priority_default_and_higher": "Prioritate implicită și mai mare", + "prefs_notifications_min_priority_high_and_higher": "Prioritate ridicată și mai mare", + "prefs_notifications_min_priority_max_only": "Numai prioritate maximă", + "prefs_notifications_delete_after_never": "Niciodată", + "prefs_notifications_delete_after_three_hours": "După trei ore", + "prefs_notifications_delete_after_one_day": "După o zi", + "prefs_notifications_delete_after_one_week": "După o săptămână", + "prefs_notifications_delete_after_one_month": "După o lună", + "prefs_notifications_web_push_disabled_description": "Notificările sunt primite atunci când aplicația web rulează (prin WebSocket)", + "prefs_notifications_web_push_enabled": "Activat pentru {{server}}", + "prefs_notifications_web_push_disabled": "Dezactivat", + "prefs_users_title": "Gestionează utilizatorii", + "prefs_users_description_no_sync": "Utilizatorii și parolele nu sunt sincronizate cu contul tău.", + "prefs_users_table": "Tabel utilizatori", + "prefs_users_add_button": "Adăugă utilizator", + "prefs_users_edit_button": "Modifică utilizatorul", + "prefs_users_delete_button": "Șterge utilizatorul", + "prefs_users_table_cannot_delete_or_edit": "Nu se poate șterge sau modifica utilizatorul conectat", + "prefs_users_table_user_header": "Utilizator", + "prefs_users_table_base_url_header": "URL-ul serviciului", + "prefs_users_dialog_title_add": "Adaugă utilizator", + "prefs_users_dialog_title_edit": "Modifică utilizatorul", + "prefs_users_dialog_base_url_label": "URL-ul serviciului, de exemplu https://ntfy.sh", + "prefs_users_dialog_username_label": "Nume de utilizator, de ex. ionel", + "prefs_users_dialog_password_label": "Parolă", + "prefs_appearance_title": "Aspect", + "prefs_appearance_language_title": "Limbă", + "prefs_appearance_theme_title": "Temă", + "prefs_appearance_theme_system": "Sistem (implicit)", + "prefs_appearance_theme_dark": "Mod întunecat", + "prefs_appearance_theme_light": "Mod luminos", + "prefs_reservations_title": "Subiecte rezervate", + "prefs_reservations_limit_reached": "Ai atins limita de subiecte rezervate.", + "prefs_reservations_add_button": "Adaugă un subiect rezervat", + "prefs_reservations_delete_button": "Resetează accesul la topic", + "prefs_reservations_table_access_header": "Acces", + "prefs_reservations_table_everyone_deny_all": "Numai eu pot publica și mă pot abona", + "prefs_reservations_table_not_subscribed": "Neabonat", + "prefs_reservations_dialog_access_label": "Acces", + "reservation_delete_dialog_action_keep_title": "Păstrează mesajele și atașamentele în cache", + "prefs_users_description": "Adaugă/elimină utilizatori pentru subiectele protejate aici. Reține că numele de utilizator și parola sunt stocate în memoria locală a browserului.", + "reservation_delete_dialog_submit_button": "Șterge rezervarea", + "priority_min": "minim", + "priority_low": "scăzut", + "priority_default": "implicit", + "priority_high": "ridicat", + "priority_max": "maxim", + "error_boundary_button_reload_ntfy": "Reîncarcă ntfy" } From b59e18bed84518b8a27e81180d0e30d0c5e4e50e Mon Sep 17 00:00:00 2001 From: Enis Polat Date: Mon, 6 Oct 2025 19:37:03 +0200 Subject: [PATCH 268/378] Translated using Weblate (Turkish) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/ --- web/public/static/langs/tr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/public/static/langs/tr.json b/web/public/static/langs/tr.json index 424e3351..91746b28 100644 --- a/web/public/static/langs/tr.json +++ b/web/public/static/langs/tr.json @@ -57,7 +57,7 @@ "prefs_notifications_delete_after_never": "Hiçbir zaman", "notifications_attachment_copy_url_button": "URL'yi kopyala", "notifications_attachment_open_button": "Eki aç", - "nav_button_documentation": "Belgelendirme", + "nav_button_documentation": "Dokümantasyon", "nav_button_publish_message": "Bildirim yayınla", "alert_notification_permission_required_title": "Bildirimler devre dışı", "alert_notification_permission_required_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin", @@ -75,7 +75,7 @@ "notifications_click_open_button": "Bağlantıyı aç", "notifications_no_subscriptions_description": "Bir konu oluşturmak veya bir konuya abone olmak için \"{{linktext}}\" bağlantısına tıklayın. Bundan sonra PUT veya POST yoluyla mesaj gönderebilirsiniz ve buradan bildirimler alırsınız.", "notifications_example": "Örnek", - "notifications_more_details": "Daha fazla bilgi için web sitesine veya belgelendirmeye bakın.", + "notifications_more_details": "Daha fazla bilgi için web sitesini veya dokümantasyonu inceleyin.", "publish_dialog_chip_attach_url_label": "URL ile dosya ekle", "prefs_notifications_min_priority_default_and_higher": "Varsayılan öncelik ve üstü", "prefs_notifications_delete_after_three_hours": "Üç saat sonra", @@ -108,7 +108,7 @@ "publish_dialog_button_cancel_sending": "Göndermeyi iptal et", "prefs_notifications_delete_after_one_week": "Bir hafta sonra", "prefs_notifications_delete_after_one_month": "Bir ay sonra", - "publish_dialog_details_examples_description": "Örnekler ve tüm gönderme özelliklerinin ayrıntılı açıklaması için lütfen belgelendirmeye bakın.", + "publish_dialog_details_examples_description": "Tüm gönderme özelliklerinin örnekleri ve ayrıntılı açıklamaları için lütfen dokümantasyona bakın.", "emoji_picker_search_placeholder": "Emoji ara", "prefs_notifications_delete_after_title": "Bildirimleri sil", "prefs_notifications_delete_after_one_day": "Bir gün sonra", From fd0595b5473b8ae9554e4a8cc44c42f7245124a2 Mon Sep 17 00:00:00 2001 From: Alexander Eifler Date: Wed, 8 Oct 2025 21:52:49 +0200 Subject: [PATCH 269/378] docs: fix healthcheck zombies --- docs/install.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/install.md b/docs/install.md index e487b525..516cdfc2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -307,6 +307,7 @@ services: retries: 3 start_period: 40s restart: unless-stopped + init: true # needed, if healthcheck is used. Prevents zombie processes ``` If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid. From c4291cc23e01760e7681dd2b304f0f9da1d59ec3 Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Fri, 10 Oct 2025 05:07:31 +0200 Subject: [PATCH 270/378] Added translation using Weblate (Macedonian) --- web/public/static/langs/mk.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/mk.json diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/mk.json @@ -0,0 +1 @@ +{} From f9a88f841e791e347cf1441d88e477cc09643f9a Mon Sep 17 00:00:00 2001 From: ezn24 Date: Fri, 10 Oct 2025 07:28:34 +0200 Subject: [PATCH 271/378] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hant/ --- web/public/static/langs/zh_Hant.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/public/static/langs/zh_Hant.json b/web/public/static/langs/zh_Hant.json index 683f5a9f..3aecd603 100644 --- a/web/public/static/langs/zh_Hant.json +++ b/web/public/static/langs/zh_Hant.json @@ -91,7 +91,7 @@ "account_upgrade_dialog_reservations_warning_one": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,請至少刪除 1 項保留。你可以在設置中刪除保留。", "account_upgrade_dialog_reservations_warning_other": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,請至少刪除 {{count}} 項保留。你可以在設置中刪除保留。", "account_upgrade_dialog_tier_current_label": "當前", - "account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}} ", + "account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}}", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 總存儲空間", "account_upgrade_dialog_tier_features_calls_one": "每日一通電話", "account_upgrade_dialog_tier_features_calls_other": "每日{{calls}} 通電話", @@ -145,13 +145,13 @@ "action_bar_unsubscribe": "取消訂閱", "alert_notification_ios_install_required_description": "要接收通知,請在 iOS 上點擊共享,然後添加到主屏幕", "alert_notification_ios_install_required_title": "需要安裝 iOS 應用程式", - "alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知。", + "alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知", "alert_notification_permission_denied_title": "已禁用通知", "alert_notification_permission_required_button": "現在授予", - "alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限。", + "alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限", "alert_notification_permission_required_title": "已禁用通知", "alert_not_supported_context_description": "通知僅支援 HTTPS。這是 Notifications API 的限制。", - "alert_not_supported_description": "你的瀏覽器不支援通知。", + "alert_not_supported_description": "你的瀏覽器不支援通知", "alert_not_supported_title": "不支援通知", "common_add": "新增", "common_back": "返回", @@ -223,7 +223,7 @@ "notifications_none_for_topic_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題連結即可。", "notifications_none_for_topic_title": "你尚未收到有關此主題的任何通知。", "notifications_no_subscriptions_description": "點擊 \"{{linktext}}\" 連結以建立或訂閱主題。之後,你可以使用 PUT 或 POST 發送訊息,你將在這裡收到通知。", - "notifications_no_subscriptions_title": "看起來你還未有任何訂閱", + "notifications_no_subscriptions_title": "看起來你還未有任何訂閱。", "notifications_priority_x": "優先級 {{priority}}", "notifications_tags": "標記", "prefs_appearance_language_title": "語言", @@ -261,7 +261,7 @@ "prefs_notifications_web_push_disabled_description": "當網頁程式在運行時將會收到通知 (透過 WebSocket)", "prefs_notifications_web_push_disabled": "己暫用", "prefs_notifications_web_push_enabled_description": "即使網頁程式未有運街亦會收到通知 (via Web Push)", - "prefs_notifications_web_push_enabled": "己為 {{server}} 啟用", + "prefs_notifications_web_push_enabled": "己為 {{server}} 啟用", "prefs_notifications_web_push_title": "背景通知", "prefs_reservations_add_button": "新增保留主題", "prefs_reservations_delete_button": "重置主題訪問", From 0b4bcf573ebbdf76d033cf8dd659d8d38ab57908 Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Fri, 10 Oct 2025 05:24:10 +0200 Subject: [PATCH 272/378] Translated using Weblate (Macedonian) Currently translated at 4.4% (18 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index 0967ef42..aacc4822 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -1 +1,20 @@ -{} +{ + "common_cancel": "Откажи", + "common_save": "Зачувај", + "common_add": "Додади", + "common_back": "Назад", + "common_copy_to_clipboard": "Копирај", + "action_bar_profile_logout": "Одјави се", + "action_bar_sign_in": "Најави се", + "action_bar_sign_up": "Регистрирај се", + "message_bar_type_message": "Пишете порака тука", + "action_bar_profile_title": "Профил", + "action_bar_profile_settings": "Подесувања", + "signup_form_username": "Корисничко име", + "signup_form_password": "Лозинка", + "signup_form_confirm_password": "Повтори лозинка", + "login_form_button_submit": "Најави се", + "login_link_signup": "Регистрирај се", + "signup_form_button_submit": "Регистрирај се", + "action_bar_settings": "Подесувања" +} From db2b3a0dd81c82f55b9362d1ad20681c6bfc6ffa Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Sun, 12 Oct 2025 01:33:53 +0200 Subject: [PATCH 273/378] Translated using Weblate (Macedonian) Currently translated at 9.6% (39 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index aacc4822..7b4739cf 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -16,5 +16,26 @@ "login_form_button_submit": "Најави се", "login_link_signup": "Регистрирај се", "signup_form_button_submit": "Регистрирај се", - "action_bar_settings": "Подесувања" + "action_bar_settings": "Подесувања", + "signup_title": "Создади ntfy профил", + "signup_form_toggle_password_visibility": "Покажи/сокриј лозинка", + "signup_already_have_account": "Имате профил? Најавете се!", + "signup_disabled": "Регистрирање е исклучено", + "signup_error_username_taken": "Корисничкото име {{username}} е веќе земено", + "signup_error_creation_limit_reached": "Лимитот на создадени профили е надминат", + "login_title": "Најавете се на вашиот ntfy профил", + "login_disabled": "Најавувањето е исклучено", + "action_bar_show_menu": "Покажи мени", + "action_bar_logo_alt": "ntfy лого", + "action_bar_account": "Профил", + "action_bar_change_display_name": "Промени покажано име", + "action_bar_reservation_add": "Резервирај тема", + "action_bar_reservation_edit": "Промени резервација", + "account_basics_title": "Профил", + "account_basics_username_title": "Корисничко име", + "nav_button_account": "Профил", + "nav_button_settings": "Подесувања", + "nav_button_documentation": "Документација", + "notifications_attachment_copy_url_button": "Копирај URL", + "publish_dialog_message_label": "Порака" } From 2aae3577cb5238a303eec3c8dde82a2c744181b3 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Fri, 17 Oct 2025 17:39:55 -0600 Subject: [PATCH 274/378] add sid, mtime, and deleted to message_cache --- server/message_cache.go | 79 +++++++++++++++++++++++++++--------- server/message_cache_test.go | 29 +++++++++++-- server/types.go | 13 +++++- 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index 03cb4969..2523d66b 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -28,7 +28,9 @@ const ( CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, mid TEXT NOT NULL, + sid TEXT NOT NULL, time INT NOT NULL, + mtime INT NOT NULL, expires INT NOT NULL, topic TEXT NOT NULL, message TEXT NOT NULL, @@ -48,10 +50,13 @@ const ( user TEXT NOT NULL, content_type TEXT NOT NULL, encoding TEXT NOT NULL, - published INT NOT NULL + published INT NOT NULL, + deleted INT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); + CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); CREATE INDEX IF NOT EXISTS idx_time ON messages (time); + CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires); CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender); @@ -65,56 +70,57 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? AND published = 1 - ORDER BY time, id + ORDER BY mtime, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? - ORDER BY time, id + ORDER BY mtime, id ` selectMessagesSinceIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND id > ? AND published = 1 - ORDER BY time, id + ORDER BY mtime, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND (id > ? OR published = 0) - ORDER BY time, id + ORDER BY mtime, id ` selectMessagesLatestQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 ` selectMessagesDueQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE time <= ? AND published = 0 - ORDER BY time, id + ORDER BY mtime, id ` selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1` updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` + updateMessageDeletedQuery = `UPDATE messages SET deleted = 1 WHERE mid = ?` selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic` selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic` @@ -130,7 +136,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 13 + currentSchemaVersion = 14 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -259,6 +265,15 @@ const ( migrate12To13AlterMessagesTableQuery = ` CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); ` + + //13 -> 14 + migrate13To14AlterMessagesTableQuery = ` + ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT(''); + ALTER TABLE messages ADD COLUMN mtime INT NOT NULL DEFAULT('0'); + ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0'); + CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); + CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime); + ` ) var ( @@ -276,6 +291,7 @@ var ( 10: migrateFrom10, 11: migrateFrom11, 12: migrateFrom12, + 13: migrateFrom13, } ) @@ -393,7 +409,9 @@ func (c *messageCache) addMessages(ms []*message) error { } _, err := stmt.Exec( m.ID, + m.SID, m.Time, + m.MTime, m.Expires, m.Topic, m.Message, @@ -414,6 +432,7 @@ func (c *messageCache) addMessages(ms []*message) error { m.ContentType, m.Encoding, published, + 0, ) if err != nil { return err @@ -692,12 +711,14 @@ func readMessages(rows *sql.Rows) ([]*message, error) { } func readMessage(rows *sql.Rows) (*message, error) { - var timestamp, expires, attachmentSize, attachmentExpires int64 - var priority int - var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var timestamp, mtimestamp, expires, attachmentSize, attachmentExpires int64 + var priority, deleted int + var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string err := rows.Scan( &id, + &sid, ×tamp, + &mtimestamp, &expires, &topic, &msg, @@ -716,6 +737,7 @@ func readMessage(rows *sql.Rows) (*message, error) { &user, &contentType, &encoding, + &deleted, ) if err != nil { return nil, err @@ -746,7 +768,9 @@ func readMessage(rows *sql.Rows) (*message, error) { } return &message{ ID: id, + SID: sid, Time: timestamp, + MTime: mtimestamp, Expires: expires, Event: messageEvent, Topic: topic, @@ -762,6 +786,7 @@ func readMessage(rows *sql.Rows) (*message, error) { User: user, ContentType: contentType, Encoding: encoding, + Deleted: deleted, }, nil } @@ -1016,3 +1041,19 @@ func migrateFrom12(db *sql.DB, _ time.Duration) error { } return tx.Commit() } + +func migrateFrom13(db *sql.DB, _ time.Duration) error { + log.Tag(tagMessageCache).Info("Migrating cache database schema: from 13 to 14") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate13To14AlterMessagesTableQuery); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 14); err != nil { + return err + } + return tx.Commit() +} diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 778f28fe..6878d78d 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -22,9 +22,11 @@ func TestMemCache_Messages(t *testing.T) { func testCacheMessages(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "my message") m1.Time = 1 + m1.MTime = 1000 m2 := newDefaultMessage("mytopic", "my other message") m2.Time = 2 + m2.MTime = 2000 require.Nil(t, c.AddMessage(m1)) require.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message"))) @@ -102,10 +104,13 @@ func testCacheMessagesScheduled(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "message 1") m2 := newDefaultMessage("mytopic", "message 2") m2.Time = time.Now().Add(time.Hour).Unix() + m2.MTime = time.Now().Add(time.Hour).UnixMilli() m3 := newDefaultMessage("mytopic", "message 3") - m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2! + m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2! + m3.MTime = time.Now().Add(time.Minute).UnixMilli() // earlier than m2! m4 := newDefaultMessage("mytopic2", "message 4") m4.Time = time.Now().Add(time.Minute).Unix() + m4.MTime = time.Now().Add(time.Minute).UnixMilli() require.Nil(t, c.AddMessage(m1)) require.Nil(t, c.AddMessage(m2)) require.Nil(t, c.AddMessage(m3)) @@ -179,18 +184,25 @@ func TestMemCache_MessagesSinceID(t *testing.T) { func testCacheMessagesSinceID(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "message 1") m1.Time = 100 + m1.MTime = 100000 m2 := newDefaultMessage("mytopic", "message 2") m2.Time = 200 + m2.MTime = 200000 m3 := newDefaultMessage("mytopic", "message 3") - m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5 + m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5 + m3.MTime = time.Now().Add(time.Hour).UnixMilli() // Scheduled, in the future, later than m7 and m5 m4 := newDefaultMessage("mytopic", "message 4") m4.Time = 400 + m4.MTime = 400000 m5 := newDefaultMessage("mytopic", "message 5") - m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7 + m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7 + m5.MTime = time.Now().Add(time.Minute).UnixMilli() // Scheduled, in the future, later than m7 m6 := newDefaultMessage("mytopic", "message 6") m6.Time = 600 + m6.MTime = 600000 m7 := newDefaultMessage("mytopic", "message 7") m7.Time = 700 + m7.MTime = 700000 require.Nil(t, c.AddMessage(m1)) require.Nil(t, c.AddMessage(m2)) @@ -251,14 +263,17 @@ func testCachePrune(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "my message") m1.Time = now - 10 + m1.MTime = (now - 10) * 1000 m1.Expires = now - 5 m2 := newDefaultMessage("mytopic", "my other message") m2.Time = now - 5 + m2.MTime = (now - 5) * 1000 m2.Expires = now + 5 // In the future m3 := newDefaultMessage("another_topic", "and another one") m3.Time = now - 12 + m3.MTime = (now - 12) * 1000 m3.Expires = now - 2 require.Nil(t, c.AddMessage(m1)) @@ -297,6 +312,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires1 := time.Now().Add(-4 * time.Hour).Unix() // Expired m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" + m.SID = "m1" m.Sender = netip.MustParseAddr("1.2.3.4") m.Attachment = &attachment{ Name: "flower.jpg", @@ -310,6 +326,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires2 := time.Now().Add(2 * time.Hour).Unix() // Future m = newDefaultMessage("mytopic", "sending you a car") m.ID = "m2" + m.SID = "m2" m.Sender = netip.MustParseAddr("1.2.3.4") m.Attachment = &attachment{ Name: "car.jpg", @@ -323,6 +340,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires3 := time.Now().Add(1 * time.Hour).Unix() // Future m = newDefaultMessage("another-topic", "sending you another car") m.ID = "m3" + m.SID = "m3" m.User = "u_BAsbaAa" m.Sender = netip.MustParseAddr("5.6.7.8") m.Attachment = &attachment{ @@ -378,11 +396,13 @@ func TestMemCache_Attachments_Expired(t *testing.T) { func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" + m.SID = "m1" m.Expires = time.Now().Add(time.Hour).Unix() require.Nil(t, c.AddMessage(m)) m = newDefaultMessage("mytopic", "message with attachment") m.ID = "m2" + m.SID = "m2" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "car.jpg", @@ -395,6 +415,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m = newDefaultMessage("mytopic", "message with external attachment") m.ID = "m3" + m.SID = "m3" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "car.jpg", @@ -406,6 +427,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m = newDefaultMessage("mytopic2", "message with expired attachment") m.ID = "m4" + m.SID = "m4" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "expired-car.jpg", @@ -502,6 +524,7 @@ func TestSqliteCache_Migration_From1(t *testing.T) { // Add delayed message delayedMessage := newDefaultMessage("mytopic", "some delayed message") delayedMessage.Time = time.Now().Add(time.Minute).Unix() + delayedMessage.MTime = time.Now().Add(time.Minute).UnixMilli() require.Nil(t, c.AddMessage(delayedMessage)) // 10, not 11! diff --git a/server/types.go b/server/types.go index 65492e46..9e65045f 100644 --- a/server/types.go +++ b/server/types.go @@ -25,7 +25,9 @@ const ( // message represents a message published to a topic type message struct { ID string `json:"id"` // Random message ID + SID string `json:"sid"` // Message sequence ID for updating message contents Time int64 `json:"time"` // Unix time in seconds + MTime int64 `json:"mtime"` // Unix time in milliseconds Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) Event string `json:"event"` // One of the above Topic string `json:"topic"` @@ -42,13 +44,16 @@ type message struct { Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting User string `json:"-"` // UserID of the uploader, used to associated attachments + Deleted int `json:"deleted,omitempty"` } func (m *message) Context() log.Context { fields := map[string]any{ "topic": m.Topic, "message_id": m.ID, + "message_sid": m.SID, "message_time": m.Time, + "message_mtime": m.MTime, "message_event": m.Event, "message_body_size": len(m.Message), } @@ -92,6 +97,7 @@ func newAction() *action { // publishMessage is used as input when publishing as JSON type publishMessage struct { Topic string `json:"topic"` + SID string `json:"sid"` Title string `json:"title"` Message string `json:"message"` Priority int `json:"priority"` @@ -117,6 +123,7 @@ func newMessage(event, topic, msg string) *message { return &message{ ID: util.RandomString(messageIDLength), Time: time.Now().Unix(), + MTime: time.Now().UnixMilli(), Event: event, Topic: topic, Message: msg, @@ -155,7 +162,11 @@ type sinceMarker struct { } func newSinceTime(timestamp int64) sinceMarker { - return sinceMarker{time.Unix(timestamp, 0), ""} + return newSinceMTime(timestamp * 1000) +} + +func newSinceMTime(mtimestamp int64) sinceMarker { + return sinceMarker{time.UnixMilli(mtimestamp), ""} } func newSinceID(id string) sinceMarker { From 83b5470bc57a24e25e0fa3daa65ab8573954c7a1 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Fri, 17 Oct 2025 19:01:41 -0600 Subject: [PATCH 275/378] publish messages with a custom sequence ID --- server/errors.go | 1 + server/server.go | 31 +++++++++++++++++++- server/server_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/server/errors.go b/server/errors.go index c6745779..141adbd7 100644 --- a/server/errors.go +++ b/server/errors.go @@ -125,6 +125,7 @@ var ( errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} + errHTTPBadRequestSIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: SID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/server.go b/server/server.go index 05b5b63a..aae4171e 100644 --- a/server/server.go +++ b/server/server.go @@ -79,6 +79,8 @@ var ( wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`) authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`) publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) + sidRegex = topicRegex + updatePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}$`) webConfigPath = "/config.js" webManifestPath = "/manifest.webmanifest" @@ -542,7 +544,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.transformBodyJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish)))(w, r, v) } else if r.Method == http.MethodPost && r.URL.Path == matrixPushPath { return s.transformMatrixJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublishMatrix)))(w, r, v) - } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) { + } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updatePathRegex.MatchString(r.URL.Path)) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) @@ -955,6 +957,24 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) { + if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) { + pathSID, err := s.sidFromPath(r.URL.Path) + if err != nil { + return false, false, "", "", "", false, err + } + m.SID = pathSID + } else { + sid := readParam(r, "x-sequence-id", "sequence-id", "sid") + if sid != "" { + if sidRegex.MatchString(sid) { + m.SID = sid + } else { + return false, false, "", "", "", false, errHTTPBadRequestSIDInvalid + } + } else { + m.SID = m.ID + } + } cache = readBoolParam(r, true, "x-cache", "cache") firebase = readBoolParam(r, true, "x-firebase", "firebase") m.Title = readParam(r, "x-title", "title", "t") @@ -1693,6 +1713,15 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { return topics, parts[1], nil } +// sidFromPath returns the SID from a POST path like /mytopic/sidHere +func (s *Server) sidFromPath(path string) (string, *errHTTP) { + parts := strings.Split(path, "/") + if len(parts) != 3 { + return "", errHTTPBadRequestSIDInvalid + } + return parts[2], nil +} + // topicsFromIDs returns the topics with the given IDs, creating them if they don't exist. func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) { s.mu.Lock() diff --git a/server/server_test.go b/server/server_test.go index 41633dd5..9270d010 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -703,6 +703,74 @@ func TestServer_PublishInvalidTopic(t *testing.T) { require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code) } +func TestServer_PublishWithSIDInPath(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic/sid", "message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid", msg.SID) +} + +func TestServer_PublishWithSIDInHeader(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic", "message", map[string]string{ + "sid": "sid", + }) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid", msg.SID) +} + +func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "PUT", "/mytopic/sid1", "message", map[string]string{ + "sid": "sid2", + }) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid1", msg.SID) // SID in path has priority over SID in header +} + +func TestServer_PublishWithSIDInQuery(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid1", msg.SID) +} + +func TestServer_PublishWithSIDViaGet(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid1", msg.SID) +} + +func TestServer_PublishWithInvalidSIDInPath(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic/.", "message", nil) + + require.Equal(t, 404, response.Code) +} + +func TestServer_PublishWithInvalidSIDInHeader(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic", "message", map[string]string{ + "X-Sequence-ID": "*&?", + }) + + require.Equal(t, 400, response.Code) + require.Equal(t, 40049, toHTTPError(t, response.Body.String()).Code) +} + func TestServer_PollWithQueryFilters(t *testing.T) { s := newTestServer(t, newTestConfig(t)) From 8293a24cf9a91b9508c970057e9450fdabdca38d Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Fri, 17 Oct 2025 22:10:11 -0600 Subject: [PATCH 276/378] update notification text using sid in web app --- web/public/static/langs/en.json | 2 ++ web/public/sw.js | 10 +++++- web/src/app/SubscriptionManager.js | 51 ++++++++++++++++++++++++---- web/src/app/db.js | 4 +-- web/src/app/notificationUtils.js | 12 +++++-- web/src/components/Notifications.jsx | 22 ++++++++++++ 6 files changed, 90 insertions(+), 11 deletions(-) diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 3ad04ea7..362a3192 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -70,6 +70,8 @@ "notifications_delete": "Delete", "notifications_copied_to_clipboard": "Copied to clipboard", "notifications_tags": "Tags", + "notifications_sid": "Sequence ID", + "notifications_revisions": "Revisions", "notifications_priority_x": "Priority {{priority}}", "notifications_new_indicator": "New notification", "notifications_attachment_image": "Attachment image", diff --git a/web/public/sw.js b/web/public/sw.js index 56d66f16..471cbee2 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -23,9 +23,17 @@ const broadcastChannel = new BroadcastChannel("web-push-broadcast"); const addNotification = async ({ subscriptionId, message }) => { const db = await dbAsync(); + const populatedMessage = message; + + if (!("mtime" in populatedMessage)) { + populatedMessage.mtime = message.time * 1000; + } + if (!("sid" in populatedMessage)) { + populatedMessage.sid = message.id; + } await db.notifications.add({ - ...message, + ...populatedMessage, subscriptionId, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation new: 1, diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index de99b642..00d15d89 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -156,18 +156,41 @@ class SubscriptionManager { // It's actually fine, because the reading and filtering is quite fast. The rendering is what's // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach - return this.db.notifications - .orderBy("time") // Sort by time first + const notifications = await this.db.notifications + .orderBy("mtime") // Sort by time first .filter((n) => n.subscriptionId === subscriptionId) .reverse() .toArray(); + + return this.groupNotificationsBySID(notifications); } async getAllNotifications() { - return this.db.notifications - .orderBy("time") // Efficient, see docs + const notifications = await this.db.notifications + .orderBy("mtime") // Efficient, see docs .reverse() .toArray(); + + return this.groupNotificationsBySID(notifications); + } + + // Collapse notification updates based on sids + groupNotificationsBySID(notifications) { + const results = {}; + notifications.forEach((notification) => { + const key = `${notification.subscriptionId}:${notification.sid}`; + if (key in results) { + if ("history" in results[key]) { + results[key].history.push(notification); + } else { + results[key].history = [notification]; + } + } else { + results[key] = notification; + } + }); + + return Object.values(results); } /** Adds notification, or returns false if it already exists */ @@ -177,9 +200,16 @@ class SubscriptionManager { return false; } try { + const populatedNotification = notification; + if (!("mtime" in populatedNotification)) { + populatedNotification.mtime = notification.time * 1000; + } + if (!("sid" in populatedNotification)) { + populatedNotification.sid = notification.id; + } // sw.js duplicates this logic, so if you change it here, change it there too await this.db.notifications.add({ - ...notification, + ...populatedNotification, subscriptionId, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation new: 1, @@ -195,7 +225,16 @@ class SubscriptionManager { /** Adds/replaces notifications, will not throw if they exist */ async addNotifications(subscriptionId, notifications) { - const notificationsWithSubscriptionId = notifications.map((notification) => ({ ...notification, subscriptionId })); + const notificationsWithSubscriptionId = notifications.map((notification) => { + const populatedNotification = notification; + if (!("mtime" in populatedNotification)) { + populatedNotification.mtime = notification.time * 1000; + } + if (!("sid" in populatedNotification)) { + populatedNotification.sid = notification.id; + } + return { ...populatedNotification, subscriptionId }; + }); const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { diff --git a/web/src/app/db.js b/web/src/app/db.js index b28fb716..f4d36c1b 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -11,9 +11,9 @@ const createDatabase = (username) => { const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user const db = new Dexie(dbName); - db.version(2).stores({ + db.version(3).stores({ subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance + notifications: "&id,sid,subscriptionId,time,mtime,new,[subscriptionId+new]", // compound key for query performance users: "&baseUrl,username", prefs: "&key", }); diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 0bd5136d..2884e2f3 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -53,6 +53,14 @@ export const badge = "/static/images/mask-icon.svg"; export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { const image = isImage(message.attachment) ? message.attachment.url : undefined; + let tag; + + if (message.sid) { + tag = message.sid; + } else { + tag = subscriptionId; + } + // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API return [ formatTitleWithDefault(message, defaultTitle), @@ -61,8 +69,8 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to badge, icon, image, - timestamp: message.time * 1_000, - tag: subscriptionId, + timestamp: message.mtime, + tag, renotify: true, silent: false, // This is used by the notification onclick event diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index dceb5b91..40789d08 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -233,10 +233,20 @@ const NotificationItem = (props) => { const handleDelete = async () => { console.log(`[Notifications] Deleting notification ${notification.id}`); await subscriptionManager.deleteNotification(notification.id); + notification.history?.forEach(async (revision) => { + console.log(`[Notifications] Deleting revision ${revision.id}`); + await subscriptionManager.deleteNotification(revision.id); + }); }; const handleMarkRead = async () => { console.log(`[Notifications] Marking notification ${notification.id} as read`); await subscriptionManager.markNotificationRead(notification.id); + notification.history + ?.filter((revision) => revision.new === 1) + .forEach(async (revision) => { + console.log(`[Notifications] Marking revision ${revision.id} as read`); + await subscriptionManager.markNotificationRead(revision.id); + }); }; const handleCopy = (s) => { navigator.clipboard.writeText(s); @@ -248,6 +258,8 @@ const NotificationItem = (props) => { const hasUserActions = notification.actions && notification.actions.length > 0; const showActions = hasAttachmentActions || hasClickAction || hasUserActions; + const showSid = notification.id !== notification.sid || notification.history; + return ( @@ -304,6 +316,16 @@ const NotificationItem = (props) => { {t("notifications_tags")}: {tags} )} + {showSid && ( + + {t("notifications_sid")}: {notification.sid} + + )} + {notification.history && ( + + {t("notifications_revisions")}: {notification.history.length + 1} + + )} {showActions && ( From 692a1fa5323522fa55d758a25a387474dbb7ed45 Mon Sep 17 00:00:00 2001 From: 109247019824 <109247019824@users.noreply.hosted.weblate.org> Date: Fri, 17 Oct 2025 11:51:53 +0200 Subject: [PATCH 277/378] Translated using Weblate (Bulgarian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/ --- web/public/static/langs/bg.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json index 59b85e5b..31716755 100644 --- a/web/public/static/langs/bg.json +++ b/web/public/static/langs/bg.json @@ -22,7 +22,7 @@ "publish_dialog_chip_email_label": "Препращане към ел. поща", "publish_dialog_chip_attach_url_label": "Прикачване на файл от адрес", "publish_dialog_chip_attach_file_label": "Прикачване местен файл", - "publish_dialog_chip_delay_label": "Отлагане на изпращането", + "publish_dialog_chip_delay_label": "Отложено изпращане", "publish_dialog_chip_topic_label": "Промяна на темата", "publish_dialog_button_cancel_sending": "Отменяне на изпращането", "publish_dialog_button_cancel": "Отказ", @@ -121,7 +121,7 @@ "subscribe_dialog_login_button_login": "Вход", "subscribe_dialog_error_user_not_authorized": "Потребителят {{username}} няма достъп", "prefs_appearance_title": "Външен вид", - "publish_dialog_delay_placeholder": "Отлагане на изпращането, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)", + "publish_dialog_delay_placeholder": "Отложено изпращане, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)", "prefs_notifications_delete_after_one_week": "След една седмица", "prefs_users_title": "Управление на потребители", "prefs_users_table_base_url_header": "Адрес на услугата", @@ -177,7 +177,7 @@ "publish_dialog_topic_reset": "Нулиране на тема", "publish_dialog_click_reset": "Премахване на адрес", "publish_dialog_email_reset": "Премахване на препращането към ел. поща", - "publish_dialog_delay_reset": "Премахва отлагането на изпращането", + "publish_dialog_delay_reset": "Премахва отложеното на изпращане", "publish_dialog_attached_file_remove": "Премахване на прикачения файл", "emoji_picker_search_clear": "Изчистване на търсенето", "subscribe_dialog_subscribe_base_url_label": "Адрес на услугата", @@ -253,7 +253,7 @@ "account_delete_dialog_button_cancel": "Отказ", "account_upgrade_dialog_interval_monthly": "Месечно", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} резервирани теми", - "account_upgrade_dialog_tier_features_no_reservations": "Няма резервирани теми", + "account_upgrade_dialog_tier_features_no_reservations": "Без резервирани теми", "account_tokens_dialog_button_cancel": "Отказ", "account_delete_title": "Премахване на профила", "account_upgrade_dialog_title": "Промяна нивото на профила", From 95f4e58ca0f25da68c3c3e885ea8353f17b4bbc9 Mon Sep 17 00:00:00 2001 From: Ryu Siwoo Date: Fri, 17 Oct 2025 15:45:52 +0200 Subject: [PATCH 278/378] Translated using Weblate (Korean) Currently translated at 46.6% (189 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ko/ --- web/public/static/langs/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ko.json b/web/public/static/langs/ko.json index ed35db70..02b003bd 100644 --- a/web/public/static/langs/ko.json +++ b/web/public/static/langs/ko.json @@ -187,5 +187,6 @@ "prefs_users_dialog_username_label": "사용자 이름, 예를 들면 phil", "prefs_users_dialog_password_label": "비밀번호", "priority_max": "최상", - "error_boundary_description": "이것은 당연히 발생되어서는 안됩니다. 굉장히 죄송합니다.
가능하시다면 이 문제를 깃허브에 제보해 주시거나, 디스코드 서버Matrix를 통해 알려주세요." + "error_boundary_description": "이것은 당연히 발생되어서는 안됩니다. 굉장히 죄송합니다.
가능하시다면 이 문제를 깃허브에 제보해 주시거나, 디스코드 서버Matrix를 통해 알려주세요.", + "common_copy_to_clipboard": "클립보드에 복사" } From 7cabc8bceca52d3193505cddbb6d9ce8082e038a Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Tue, 21 Oct 2025 05:36:06 -0400 Subject: [PATCH 279/378] Add Warp as sponsor Added sponsorship section and updated project description. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 9942e138..be26f7fa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +
+Special thanks to: +
+
+ + Warp sponsorship + + +### [Warp, built for coding with multiple AI agents.](https://www.warp.dev/vim) +[Available for MacOS, Linux, & Windows](https://www.warp.dev/ntfy)
+
+
+ ![ntfy](web/public/static/images/ntfy.png) # ntfy.sh | Send push notifications to your phone or desktop via PUT/POST From 01bb0eeccd44345d8be0e6b60dab046cf7705182 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 21 Oct 2025 05:42:39 -0400 Subject: [PATCH 280/378] Link Warp in Sponsors section --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index be26f7fa..215215b4 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Thank you to our commercial sponsors, who help keep the service running and the + + And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy: From ab2a6869376bb51fabb7de98437ead4c0c60bb09 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 21 Oct 2025 05:54:17 -0400 Subject: [PATCH 281/378] Deps --- README.md | 2 +- go.mod | 56 ++--- go.sum | 61 +++++ web/package-lock.json | 550 +++++++++++++++++++++--------------------- 4 files changed, 364 insertions(+), 305 deletions(-) diff --git a/README.md b/README.md index 215215b4..f139ace1 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Thank you to our commercial sponsors, who help keep the service running and the - + And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy: diff --git a/go.mod b/go.mod index 846ff285..cb9e8224 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.5 require ( - cloud.google.com/go/firestore v1.18.0 // indirect - cloud.google.com/go/storage v1.56.2 // indirect + cloud.google.com/go/firestore v1.20.0 // indirect + cloud.google.com/go/storage v1.57.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 @@ -16,12 +16,12 @@ require ( github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 - golang.org/x/crypto v0.42.0 - golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/crypto v0.43.0 + golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 - golang.org/x/term v0.35.0 - golang.org/x/time v0.13.0 - google.golang.org/api v0.249.0 + golang.org/x/term v0.36.0 + golang.org/x/time v0.14.0 + google.golang.org/api v0.252.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -35,33 +35,33 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.23.2 github.com/stripe/stripe-go/v74 v74.30.0 - golang.org/x/text v0.29.0 + golang.org/x/text v0.30.0 ) require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.122.0 // indirect - cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.8.4 // indirect - cloud.google.com/go/iam v1.5.2 // indirect - cloud.google.com/go/longrunning v0.6.7 // indirect - cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/longrunning v0.7.0 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect github.com/AlekSi/pointer v1.2.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251014123835-2ee22ca58382 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -76,7 +76,7 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/common v0.67.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -92,13 +92,13 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/grpc v1.75.1 // indirect - google.golang.org/protobuf v1.36.9 // indirect + google.golang.org/genproto v0.0.0-20251020155222-88f65dc88635 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 799ed99e..c466180a 100644 --- a/go.sum +++ b/go.sum @@ -2,26 +2,45 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.122.0 h1:0JTLGrcSIs3HIGsgVPvTx3cfyFSP/k9CI8vLPHTd6Wc= cloud.google.com/go v0.122.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute v1.49.1 h1:KYKIG0+pfpAWaAYayFkE/KPrAVCge0Hu82bPraAmsCk= +cloud.google.com/go/compute v1.49.1/go.mod h1:1uoZvP8Avyfhe3Y4he7sMOR16ZiAm2Q+Rc2P5rrJM28= cloud.google.com/go/compute/metadata v0.8.4 h1:oXMa1VMQBVCyewMIOm3WQsnVd9FbKBtm8reqWRaXnHQ= cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= +cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo= +cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/storage v1.56.2 h1:DzxQ4ppJe4OSTtZLtCqscC3knyW919eNl0zLLpojnqo= cloud.google.com/go/storage v1.56.2/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s= +cloud.google.com/go/storage v1.57.0 h1:4g7NB7Ta7KetVbOMpCqy89C+Vg5VE8scqlSHUPm7Rds= +cloud.google.com/go/storage v1.57.0/go.mod h1:329cwlpzALLgJuu8beyJ/uvQznDHpa2U5lGjWednkzg= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= @@ -30,12 +49,19 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= @@ -48,6 +74,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251014123835-2ee22ca58382 h1:5IeUoAZvqwF6LcCnV99NbhrGKN6ihZgahJv5jKjmZ3k= +github.com/cncf/xds/go v0.0.0-20251014123835-2ee22ca58382/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -62,6 +90,8 @@ github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+ github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= @@ -72,6 +102,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -133,6 +165,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -186,6 +220,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -202,8 +238,12 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -227,6 +267,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -238,6 +280,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -251,8 +295,12 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -265,20 +313,33 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w= google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ= +google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI= +google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc= google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk= +google.golang.org/genproto v0.0.0-20251020155222-88f65dc88635 h1:I5FLgnlmGA5voD3BZp9Rc17FGiius/DlMB3WsJ1C4Xw= +google.golang.org/genproto v0.0.0-20251020155222-88f65dc88635/go.mod h1:1Ic78BnpzY8OaTCmzxJDP4qC9INZPbGZl+54RKjtyeI= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635 h1:1wvBeYv+A2zfEbxROscJl69OP0m74S8wGEO+Syat26o= +google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 h1:3uycTxukehWrxH4HtPRtn1PDABTU331ViDjyqrUbaog= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/web/package-lock.json b/web/package-lock.json index 25c2468f..5dff69be 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1756,9 +1756,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -1773,9 +1773,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -1790,9 +1790,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -1807,9 +1807,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -1824,9 +1824,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -1841,9 +1841,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -1858,9 +1858,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -1875,9 +1875,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -1892,9 +1892,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -1909,9 +1909,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -1926,9 +1926,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -1943,9 +1943,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -1960,9 +1960,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -1977,9 +1977,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -1994,9 +1994,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -2011,9 +2011,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -2028,9 +2028,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -2045,9 +2045,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -2062,9 +2062,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -2079,9 +2079,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -2096,9 +2096,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -2113,9 +2113,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -2130,9 +2130,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -2147,9 +2147,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -2164,9 +2164,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -2181,9 +2181,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -2728,9 +2728,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.0.tgz", - "integrity": "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", "cpu": [ "arm" ], @@ -2742,9 +2742,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.0.tgz", - "integrity": "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", "cpu": [ "arm64" ], @@ -2756,9 +2756,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.0.tgz", - "integrity": "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", "cpu": [ "arm64" ], @@ -2770,9 +2770,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.0.tgz", - "integrity": "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", "cpu": [ "x64" ], @@ -2784,9 +2784,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.0.tgz", - "integrity": "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", "cpu": [ "arm64" ], @@ -2798,9 +2798,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.0.tgz", - "integrity": "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", "cpu": [ "x64" ], @@ -2812,9 +2812,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.0.tgz", - "integrity": "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", "cpu": [ "arm" ], @@ -2826,9 +2826,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.0.tgz", - "integrity": "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", "cpu": [ "arm" ], @@ -2840,9 +2840,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.0.tgz", - "integrity": "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", "cpu": [ "arm64" ], @@ -2854,9 +2854,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.0.tgz", - "integrity": "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", "cpu": [ "arm64" ], @@ -2868,9 +2868,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.0.tgz", - "integrity": "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", "cpu": [ "loong64" ], @@ -2882,9 +2882,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.0.tgz", - "integrity": "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", "cpu": [ "ppc64" ], @@ -2896,9 +2896,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.0.tgz", - "integrity": "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", "cpu": [ "riscv64" ], @@ -2910,9 +2910,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.0.tgz", - "integrity": "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", "cpu": [ "riscv64" ], @@ -2924,9 +2924,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.0.tgz", - "integrity": "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", "cpu": [ "s390x" ], @@ -2938,9 +2938,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz", - "integrity": "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", "cpu": [ "x64" ], @@ -2952,9 +2952,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz", - "integrity": "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", "cpu": [ "x64" ], @@ -2966,9 +2966,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.0.tgz", - "integrity": "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", "cpu": [ "arm64" ], @@ -2980,9 +2980,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.0.tgz", - "integrity": "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", "cpu": [ "arm64" ], @@ -2994,9 +2994,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.0.tgz", - "integrity": "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", "cpu": [ "ia32" ], @@ -3008,9 +3008,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz", - "integrity": "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", "cpu": [ "x64" ], @@ -3022,9 +3022,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz", - "integrity": "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", "cpu": [ "x64" ], @@ -3136,9 +3136,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", - "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", "peer": true, "dependencies": { @@ -3496,9 +3496,9 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "dev": true, "license": "MPL-2.0", "engines": { @@ -3590,9 +3590,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", + "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3611,9 +3611,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -3631,9 +3631,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -3711,9 +3711,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "dev": true, "funding": [ { @@ -3855,13 +3855,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", + "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.3" + "browserslist": "^4.26.3" }, "funding": { "type": "opencollective", @@ -4148,9 +4148,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "dev": true, "license": "ISC" }, @@ -4357,9 +4357,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4370,32 +4370,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escalade": { @@ -5104,6 +5104,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5766,14 +5776,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -6593,9 +6604,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", "dev": true, "license": "MIT" }, @@ -7061,24 +7072,24 @@ } }, "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.1.1" + "react": "^19.2.0" } }, "node_modules/react-i18next": { @@ -7116,9 +7127,9 @@ } }, "node_modules/react-is": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", - "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT" }, "node_modules/react-refresh": { @@ -7266,16 +7277,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.3.1.tgz", - "integrity": "sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" }, @@ -7291,31 +7302,18 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/rehype-react": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz", @@ -7367,12 +7365,12 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -7424,9 +7422,9 @@ } }, "node_modules/rollup": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.0.tgz", - "integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", "dependencies": { @@ -7440,28 +7438,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.0", - "@rollup/rollup-android-arm64": "4.52.0", - "@rollup/rollup-darwin-arm64": "4.52.0", - "@rollup/rollup-darwin-x64": "4.52.0", - "@rollup/rollup-freebsd-arm64": "4.52.0", - "@rollup/rollup-freebsd-x64": "4.52.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", - "@rollup/rollup-linux-arm-musleabihf": "4.52.0", - "@rollup/rollup-linux-arm64-gnu": "4.52.0", - "@rollup/rollup-linux-arm64-musl": "4.52.0", - "@rollup/rollup-linux-loong64-gnu": "4.52.0", - "@rollup/rollup-linux-ppc64-gnu": "4.52.0", - "@rollup/rollup-linux-riscv64-gnu": "4.52.0", - "@rollup/rollup-linux-riscv64-musl": "4.52.0", - "@rollup/rollup-linux-s390x-gnu": "4.52.0", - "@rollup/rollup-linux-x64-gnu": "4.52.0", - "@rollup/rollup-linux-x64-musl": "4.52.0", - "@rollup/rollup-openharmony-arm64": "4.52.0", - "@rollup/rollup-win32-arm64-msvc": "4.52.0", - "@rollup/rollup-win32-ia32-msvc": "4.52.0", - "@rollup/rollup-win32-x64-gnu": "4.52.0", - "@rollup/rollup-win32-x64-msvc": "4.52.0", + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" } }, @@ -7566,9 +7564,9 @@ } }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/semver": { @@ -8593,9 +8591,9 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { @@ -8668,9 +8666,9 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.0.3.tgz", - "integrity": "sha512-/OpqIpUldALGxcsEnv/ekQiQ5xHkQ53wcoN5ewX4jiIDNGs3W+eNcI1WYZeyOLmzoEjg09D7aX0O89YGjen1aw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.1.0.tgz", + "integrity": "sha512-VsSpdubPzXhHWVINcSx6uHRMpOHVHQcHsef1QgkOlEoaIDAlssFEW88LBq1a59BuokAhsh2kUDJbaX1bZv4Bjw==", "dev": true, "license": "MIT", "dependencies": { From 5fefcd129616d2fc3bfc172cf6e552bb7621446a Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Tue, 21 Oct 2025 09:25:56 -0400 Subject: [PATCH 282/378] Update Warp link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f139ace1..41bfb98d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Warp sponsorship -### [Warp, built for coding with multiple AI agents.](https://www.warp.dev/vim) +### [Warp, built for coding with multiple AI agents.](https://www.warp.dev/ntfy) [Available for MacOS, Linux, & Windows](https://www.warp.dev/ntfy)

From 65188b1f07d5aea47ed52d08f85d393292455a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 21 Oct 2025 00:25:50 +0200 Subject: [PATCH 283/378] Translated using Weblate (Estonian) Currently translated at 94.5% (383 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 74 ++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index bae23a82..cf09449a 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -24,7 +24,7 @@ "signup_form_toggle_password_visibility": "Vaheta salasõna nähtavust", "action_bar_account": "Kasutajakonto", "action_bar_sign_in": "Logi sisse", - "nav_button_documentation": "Dokumentatsioon", + "nav_button_documentation": "Juhendid ja teave", "action_bar_profile_title": "Profiil", "action_bar_profile_settings": "Seadistused", "action_bar_sign_up": "Liitu", @@ -53,8 +53,8 @@ "account_tokens_table_token_header": "Tunnusluba", "account_tokens_table_last_origin_tooltip": "IP-aadressilt {{ip}}, klõpsi täpsema teabe nägemiseks", "action_bar_reservation_add": "Reserveeri teema", - "action_bar_reservation_edit": "Muuda reserveeringut", - "action_bar_reservation_delete": "Eemalda reserveering", + "action_bar_reservation_edit": "Muuda reserveerimist", + "action_bar_reservation_delete": "Eemalda reserveerimine", "action_bar_reservation_limit_reached": "Ülempiir on käes", "action_bar_send_test_notification": "Saata testteavitus", "action_bar_clear_notifications": "Kustuta kõik teavitused", @@ -126,7 +126,7 @@ "account_usage_unlimited": "Piiramatu", "prefs_notifications_delete_after_never": "Mitte kunagi", "account_upgrade_dialog_interval_monthly": "Iga kuu", - "account_upgrade_dialog_tier_price_per_month": "kuu", + "account_upgrade_dialog_tier_price_per_month": "kuus", "prefs_notifications_web_push_disabled": "Pole kasutusel", "prefs_appearance_title": "Välimus", "prefs_appearance_language_title": "Keel", @@ -185,7 +185,7 @@ "notifications_loading": "Laadin teavitusi…", "publish_dialog_title_topic": "Avalda teemas {{topic}}", "publish_dialog_progress_uploading_detail": "Üleslaadimisel {{loaded}}/{{total}} ({{percent}}%) …", - "publish_dialog_topic_placeholder": "Teema nimi, nt. kati_teavitused", + "publish_dialog_topic_placeholder": "Teema nimi, nt. kadri_kiirteated", "publish_dialog_title_placeholder": "Teavituse pealkiri, nt. Andmeruumi teavitus", "publish_dialog_message_placeholder": "Siia sisesta sõnum", "notifications_none_for_any_title": "Sa pole veel saanud ühtegi teavitust.", @@ -319,5 +319,67 @@ "error_boundary_button_reload_ntfy": "Laadi ntfy uuesti", "error_boundary_button_copy_stack_trace": "Kopeeri pinujälg", "error_boundary_stack_trace": "Pinujälg", - "error_boundary_gathering_info": "Kogu täiendavat teavet…" + "error_boundary_gathering_info": "Kogu täiendavat teavet…", + "notifications_none_for_any_description": "Teemakohaste teavituste saatmiseks tee PUT või POST meetodiga päring teema võrguaadressile. Siin on üks näide ühe sinu teemaga.", + "notifications_no_subscriptions_title": "Tundub, et sul pole veel ühtegi tellimust.", + "notifications_no_subscriptions_description": "Olemasoleva teema tellimiseks või uue loomiseks klõpsa „{{linktext}}“. Peale seda saad PUT või POST meetodiga päringuga saata sõnumeid ning neid siin vastu võtta.", + "notifications_more_details": "Lisateavet leiad veebisaidist või juhendist.", + "publish_dialog_details_examples_description": "Näited ja saatmisvõimaluste üksikasjaliku kirjelduse leiad juhendist.", + "account_tokens_description": "Selleks, et ei peaks ntfy API abil avaldamise ja tellimuse päringusse lisama kasutajanime ja salasõna, kasuta tunnuslubasid. Lisateavet leiad juhendist.", + "subscribe_dialog_subscribe_title": "Telli teema", + "subscribe_dialog_subscribe_description": "Teemasid ei saa salasõnaga kaitsta, seega vali teema nimi, mida pole väga lihtne ära arvata. Peale tellimuse tegemist võide kohe hakata PUT või POST päringutega sõnumeid saatma.", + "subscribe_dialog_subscribe_topic_placeholder": "Teema nimi, näiteks kadri_kiirteated", + "subscribe_dialog_error_user_not_authorized": "Kasutajal {{username}} puudub volitus", + "account_usage_of_limit": "piirangust {{limit}}", + "account_usage_limits_reset_daily": "Kasutuspiirangud lähtestatakse keskööl (UTC järgi)", + "account_basics_tier_admin_suffix_with_tier": "(tasemega {{tier}})", + "account_basics_tier_admin_suffix_no_tier": "(tase puudub)", + "account_upgrade_dialog_title": "Muuda kasutajakonto taset", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} faili kohta", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} kõnet päevas", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} kõne päevas", + "account_upgrade_dialog_tier_features_no_calls": "Ilma telefonikõnedeta", + "account_upgrade_dialog_tier_features_attachment_total_size": "andmeruum kokku {{totalsize}}", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}}aastas. Arveldatuna kord kuus.", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} arveldatuna kord aastas. Sa säästad {{save}}.", + "account_upgrade_dialog_button_pay_now": "Maksa nüüd ja telli", + "account_upgrade_dialog_button_cancel_subscription": "Katkesta tellimus", + "account_upgrade_dialog_interval_yearly_discount_save": "säästa {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "säästa kuni {{discount}}%", + "account_upgrade_dialog_billing_contact_email": "Küsimuste puhul arvelduste kohta, palun kontakteeru meiega otse.", + "account_upgrade_dialog_billing_contact_website": "Küsimuste puhul arvelduste kohta, palun vaata meie veebisaiti.", + "account_delete_dialog_billing_warning": "Sinu kasutajakonto kustutamisel katkeb koheselt ka tellimus. Muu hulgas ei saa sa enam ligi arvelduste haldusvaatele.", + "account_upgrade_dialog_button_update_subscription": "Uuenda tellimust", + "account_tokens_title": "Tunnusload ligipääsuks", + "account_tokens_dialog_label": "Silt, näiteks „Salaradari teavitused“", + "account_usage_attachment_storage_description": "{{filesize}} faili kohta, kustutatud peale {{expiry}}", + "account_usage_cannot_create_portal_session": "Arvelduste vaate avamine ei õnnestu", + "prefs_notifications_min_priority_any": "Kõik prioriteedid", + "prefs_notifications_min_priority_low_and_higher": "Vähetähtsad ja kõrgemad", + "prefs_notifications_min_priority_default_and_higher": "Vaikimisi tähtsusega ja kõrgemad", + "prefs_notifications_min_priority_high_and_higher": "Väga tähtsad ja kõrgemad", + "prefs_notifications_min_priority_max_only": "Vaid kõrgeim prioriteet", + "prefs_reservations_table_everyone_deny_all": "Vaid mina saan avaldada ja tellida", + "prefs_reservations_table_everyone_read_only": "Mina saan avaldada ja tellida, kõik saavad tellida", + "prefs_reservations_table_everyone_write_only": "Mina saan avaldada ja tellida, kõik saavad avaldada", + "prefs_reservations_table_everyone_read_write": "Kõik saavad avaldada ja tellida", + "prefs_reservations_table_not_subscribed": "Pole tellitud", + "prefs_reservations_table_click_to_subscribe": "Tellimiseks klõpsi", + "prefs_reservations_dialog_title_add": "Reserveeri teema", + "prefs_reservations_dialog_title_edit": "Muuda reserveeritud teemat", + "prefs_reservations_dialog_title_delete": "Kustuta teema reserveering", + "prefs_reservations_dialog_description": "Teema reserveerimisega muutud selle omanikuks ja saad teiste jaoks määrata ligipääsuõigusi teemale.", + "reservation_delete_dialog_description": "Teema reserveerimisest loobudes annad teistele võimaluse seda reserveerida ja muutuda selle omanikuks. Sina saad otsustada, kas vanad sõnumid jäävad alles või kustutatakse.", + "reservation_delete_dialog_action_keep_title": "Säilita puhverdatud sõnumid ja manused", + "reservation_delete_dialog_action_keep_description": "Serveris puhverdatud sõnumid ja manused muutuvad avalikult nähtavaks neile, kes teavad teema nime.", + "reservation_delete_dialog_action_delete_title": "Kustuta puhverdatud sõnumid ja manused", + "reservation_delete_dialog_action_delete_description": "Puhverdatud sõnumid ja manused kustuvad jäädavalt. Seda tegevust ei saa hiljem tagasi pöörata.", + "reservation_delete_dialog_submit_button": "Kustuta reserveerimine", + "prefs_reservations_description": "Sa võid teemade nimesid reserveerida isiklikuks kasutuseks. Sellega muutud teema omanikuks ja saad määrata, kes ning mis viisil teemale ligi saab.", + "prefs_reservations_limit_reached": "Oled jõudnud reserveeritud teemade arvu ülempiirini.", + "prefs_reservations_add_button": "Lisa reserveeritud teema", + "prefs_reservations_edit_button": "Muuda ligipääsu teemale", + "prefs_reservations_delete_button": "Lähtesta ligipääs teemale", + "prefs_reservations_table": "Reserveeritud teemade tabel", + "web_push_unknown_notification_body": "Avades veebirakenduse peaksid vist tegema ntfy uuenduse" } From 315326ffc00afd74dce15e380f830598b548c46f Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Tue, 21 Oct 2025 06:24:12 +0200 Subject: [PATCH 284/378] Translated using Weblate (Macedonian) Currently translated at 12.8% (52 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index 7b4739cf..b1caef44 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -37,5 +37,18 @@ "nav_button_settings": "Подесувања", "nav_button_documentation": "Документација", "notifications_attachment_copy_url_button": "Копирај URL", - "publish_dialog_message_label": "Порака" + "publish_dialog_message_label": "Порака", + "action_bar_reservation_delete": "Отстрани резервација", + "action_bar_reservation_limit_reached": "Достигната е границата", + "action_bar_send_test_notification": "Испрати тест нотификација", + "action_bar_clear_notifications": "Исчисти ги сите нотификации", + "action_bar_mute_notifications": "Загуши ги нотификациите", + "action_bar_unsubscribe": "Отпиши се", + "action_bar_toggle_action_menu": "Отвори/затвори мени за акција", + "message_bar_error_publishing": "Грешки при публикација на нотификацијата", + "message_bar_show_dialog": "Покажи дијалог за публикација", + "nav_topics_title": "Претплатени теми", + "nav_button_all_notifications": "Сите нотификации", + "nav_button_publish_message": "Објави нотификација", + "nav_button_subscribe": "Претплати се на тема" } From 9e1636a3f7e45fe38843d9f53ea312496595f69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 24 Oct 2025 14:33:24 +0200 Subject: [PATCH 285/378] Translated using Weblate (Estonian) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index cf09449a..8775900d 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -381,5 +381,27 @@ "prefs_reservations_edit_button": "Muuda ligipääsu teemale", "prefs_reservations_delete_button": "Lähtesta ligipääs teemale", "prefs_reservations_table": "Reserveeritud teemade tabel", - "web_push_unknown_notification_body": "Avades veebirakenduse peaksid vist tegema ntfy uuenduse" + "web_push_unknown_notification_body": "Avades veebirakenduse peaksid vist tegema ntfy uuenduse", + "prefs_users_description_no_sync": "Kasutajad ja nende salasõnad pole sinu kontoga sünkroonitud.", + "error_boundary_title": "Vaat, kus lops - ntfy jooksis kokku", + "error_boundary_description": "Ilmselgelt ei peaks niimoodi juhtuma. Vabandust.
Kui sul on mõni hetk aega, siis palun seate sellest GitHubis või kirjuta Discordis või Matrixis.", + "error_boundary_unsupported_indexeddb_description": "Meie ntfy veebirakendus vajab korralikuks toimimiseks brauseri IndexedDB funktsionaalsust, aga sinu veebibrauser seda privaatses režiimis ei toeta.

See on nüüd õnnetu lugu küll, aga olemuslikult pole ntfy veebirakenduse kasutamisel privaatses režiimis eriti mõtet - kõike hoitakse ju brauseri hallatavas andmekogus. Lisateavet selle kohta leiad GitHubist siit, aga saad ka teema üle meiega arutleda Discordis või Matrixis.", + "account_usage_basis_ip_description": "Selle kasutajakonto statistika ja kasutuspiirangud põhinevad sinu IP-aadressil ja seega võivad nad olla teistega jagatud. Siin näidatud piirangud on hinnangulised ja põhinevad üldistel päringupiirangutel.", + "prefs_notifications_web_push_enabled": "Kasutusel serveris {{server}}", + "prefs_notifications_web_push_disabled_description": "Saad teavitusi siis, kui rakendus on töös (WebSocketi abil)", + "prefs_notifications_web_push_enabled_description": "Saad teavitusi siis, kui rakendus pole töös (Web Pushi abil)", + "prefs_notifications_web_push_title": "Teavitused taustal", + "prefs_notifications_min_priority_description_max": "Näita teavitusi siis, kui prioriteet on 5 (maksimaalne)", + "prefs_notifications_min_priority_description_x_or_higher": "Näita teavitusi siis, kui prioriteet on {{number}} ({{name}}) või kõrgem", + "prefs_notifications_sound_description_none": "Teavitused ei kasuta saabumisel helimärguannet", + "prefs_notifications_sound_description_some": "Teavitused kasutavad saabumisel helimärguannet {{sound}}", + "prefs_notifications_sound_no_sound": "Helimärguanne puudub", + "prefs_notifications_sound_play": "Esita valitud helimärguannet", + "prefs_notifications_min_priority_title": "Väikseim prioriteet", + "prefs_notifications_min_priority_description_any": "Näitan kõiki teavitusi ja seejuures ei arvesta prioriteetidega", + "account_upgrade_dialog_cancel_warning": "Sellega katkestad oma tellimuse ja {{date}} muutub sinu kasutajakonto tase madalamaks. Sel kuupäeval teemade reserveeringud tühistuvad ja puhverdatud sõnumid kustutatakse serverist.", + "account_upgrade_dialog_proration_info": "Summade jagamine: Kui muudad teenusepaketti paremaks, siis pead hinnavahe maksma kohe. Kui muudad teenusepaketti madalamaks, siis hinnavahe arvelt hüvituvad mõned järgmised maksed.", + "account_upgrade_dialog_reservations_warning_one": "Sinu praegune teenusepakett võimaldab senise paketiga võrreldes reserveerida vähem teemasid. Enne paketi muutmist palun esmalt kustuta vähemalt üks reserveering. Seda saad teha siin.", + "account_upgrade_dialog_reservations_warning_other": "Sinu praegune teenusepakett võimaldab senise paketiga võrreldes reserveerida vähem teemasid. Enne paketi muutmist palun esmalt kustuta vähemalt {{count}} reserveeringut. Seda saad teha siin.", + "prefs_users_description": "Oma kaitstud teemade kasutajaid saad lisada ja eemaldada siin. Palun arvesta, et kasutajanimi ja salasõna on salvestatud veebibrauseri kohalikus andmeruumis." } From bb9bbdf7361c29e5ff00f6c543556103493464e0 Mon Sep 17 00:00:00 2001 From: leukosaima <187358+leukosaima@users.noreply.github.com> Date: Sun, 26 Oct 2025 13:00:38 -0400 Subject: [PATCH 286/378] Update ntfyrr, add ntailfy in integrations.md --- docs/integrations.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/integrations.md b/docs/integrations.md index 160f72c4..4613cb58 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -175,10 +175,11 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript) - [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) - [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell) -- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. +- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Overseerr and Maintainerr webhook notification to ntfy helper service (C#) - [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform - [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy - [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy. +- [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go) ## Blog + forum posts From b483891bcbea37c7d30333a0aaf5691d3e022525 Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Thu, 30 Oct 2025 19:35:55 -0400 Subject: [PATCH 287/378] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41bfb98d..2d888468 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Warp sponsorship -### [Warp, built for coding with multiple AI agents.](https://www.warp.dev/ntfy) -[Available for MacOS, Linux, & Windows](https://www.warp.dev/ntfy)
+### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/ntfy) +[Available for MacOS, Linux, & Windows](https://go.warp.dev/ntfy)

From cef61b2c48123d66cfaa8a5b744bf1bd6952f065 Mon Sep 17 00:00:00 2001 From: Angie Song Date: Fri, 14 Nov 2025 15:21:47 -0800 Subject: [PATCH 288/378] fix: show overflow notification action buttons hidden on small screens by setting `overflow-x: auto` --- web/src/components/theme.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/src/components/theme.js b/web/src/components/theme.js index 64217eee..f930e853 100644 --- a/web/src/components/theme.js +++ b/web/src/components/theme.js @@ -17,6 +17,13 @@ const baseThemeOptions = { }, }, }, + MuiCardActions: { + styleOverrides: { + root: { + overflowX: "auto", + }, + }, + }, }, }; From 8131d0d883eb662ca787a9c0ca6108bc4dd50fcd Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 16 Nov 2025 12:36:35 -0500 Subject: [PATCH 289/378] Bump --- go.mod | 38 +-- go.sum | 145 +++------ web/package-lock.json | 670 +++++++++++++++++++++--------------------- 3 files changed, 396 insertions(+), 457 deletions(-) diff --git a/go.mod b/go.mod index cb9e8224..811cbcad 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,22 @@ toolchain go1.24.5 require ( cloud.google.com/go/firestore v1.20.0 // indirect - cloud.google.com/go/storage v1.57.0 // indirect + cloud.google.com/go/storage v1.57.2 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 - github.com/gabriel-vasile/mimetype v1.4.10 + github.com/gabriel-vasile/mimetype v1.4.11 github.com/gorilla/websocket v1.5.3 github.com/mattn/go-sqlite3 v1.14.32 github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 - golang.org/x/crypto v0.43.0 - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.17.0 - golang.org/x/term v0.36.0 + golang.org/x/crypto v0.44.0 + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 + golang.org/x/term v0.37.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.252.0 + google.golang.org/api v0.256.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -35,11 +35,11 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.23.2 github.com/stripe/stripe-go/v74 v74.30.0 - golang.org/x/text v0.30.0 + golang.org/x/text v0.31.0 ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -55,10 +55,10 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20251014123835-2ee22ca58382 // indirect + github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect @@ -69,15 +69,15 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.1 // indirect - github.com/prometheus/procfs v0.17.0 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -92,12 +92,12 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20251020155222-88f65dc88635 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 // indirect + google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index c466180a..9d768eb0 100644 --- a/go.sum +++ b/go.sum @@ -1,65 +1,39 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.122.0 h1:0JTLGrcSIs3HIGsgVPvTx3cfyFSP/k9CI8vLPHTd6Wc= -cloud.google.com/go v0.122.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= -cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute v1.49.1 h1:KYKIG0+pfpAWaAYayFkE/KPrAVCge0Hu82bPraAmsCk= -cloud.google.com/go/compute v1.49.1/go.mod h1:1uoZvP8Avyfhe3Y4he7sMOR16ZiAm2Q+Rc2P5rrJM28= -cloud.google.com/go/compute/metadata v0.8.4 h1:oXMa1VMQBVCyewMIOm3WQsnVd9FbKBtm8reqWRaXnHQ= -cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= -cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo= cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= -cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= -cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/storage v1.56.2 h1:DzxQ4ppJe4OSTtZLtCqscC3knyW919eNl0zLLpojnqo= -cloud.google.com/go/storage v1.56.2/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s= -cloud.google.com/go/storage v1.57.0 h1:4g7NB7Ta7KetVbOMpCqy89C+Vg5VE8scqlSHUPm7Rds= -cloud.google.com/go/storage v1.57.0/go.mod h1:329cwlpzALLgJuu8beyJ/uvQznDHpa2U5lGjWednkzg= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4= +cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= @@ -72,10 +46,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cncf/xds/go v0.0.0-20251014123835-2ee22ca58382 h1:5IeUoAZvqwF6LcCnV99NbhrGKN6ihZgahJv5jKjmZ3k= -github.com/cncf/xds/go v0.0.0-20251014123835-2ee22ca58382/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e h1:gt7U1Igw0xbJdyaCM5H2CnlAlPSkzrhsebQB6WQWjLA= +github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -88,20 +60,16 @@ github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBME github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -128,8 +96,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -163,12 +131,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= -github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -218,10 +184,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -236,14 +200,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= -golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -251,8 +211,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -265,10 +225,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -278,10 +236,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -293,12 +249,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -311,33 +263,20 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w= -google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ= -google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI= -google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc= -google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk= -google.golang.org/genproto v0.0.0-20251020155222-88f65dc88635 h1:I5FLgnlmGA5voD3BZp9Rc17FGiius/DlMB3WsJ1C4Xw= -google.golang.org/genproto v0.0.0-20251020155222-88f65dc88635/go.mod h1:1Ic78BnpzY8OaTCmzxJDP4qC9INZPbGZl+54RKjtyeI= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635 h1:1wvBeYv+A2zfEbxROscJl69OP0m74S8wGEO+Syat26o= -google.golang.org/genproto/googleapis/api v0.0.0-20251020155222-88f65dc88635/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 h1:3uycTxukehWrxH4HtPRtn1PDABTU331ViDjyqrUbaog= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY= +google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/web/package-lock.json b/web/package-lock.json index 5dff69be..2da1d1c0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -60,9 +60,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -70,21 +70,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -108,13 +108,13 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -154,18 +154,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -176,14 +176,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -220,14 +220,14 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -347,9 +347,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -395,12 +395,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -410,14 +410,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -624,9 +624,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", - "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", "dev": true, "license": "MIT", "dependencies": { @@ -712,14 +712,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -812,9 +812,9 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", "dev": true, "license": "MIT", "dependencies": { @@ -911,9 +911,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", "dev": true, "license": "MIT", "dependencies": { @@ -977,16 +977,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1131,9 +1131,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1444,17 +1444,17 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.0", + "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", @@ -1467,42 +1467,42 @@ "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1567,17 +1567,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -1585,13 +1585,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1756,9 +1756,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -1773,9 +1773,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -1790,9 +1790,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -1807,9 +1807,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -1824,9 +1824,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -1841,9 +1841,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -1858,9 +1858,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -1875,9 +1875,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -1892,9 +1892,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -1909,9 +1909,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -1926,9 +1926,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -1943,9 +1943,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -1960,9 +1960,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -1977,9 +1977,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -1994,9 +1994,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -2011,9 +2011,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -2028,9 +2028,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -2045,9 +2045,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -2062,9 +2062,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -2079,9 +2079,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -2096,9 +2096,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -2113,9 +2113,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -2130,9 +2130,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -2147,9 +2147,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -2164,9 +2164,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -2181,9 +2181,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -2217,9 +2217,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2641,9 +2641,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2728,9 +2728,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -2742,9 +2742,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -2756,9 +2756,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -2770,9 +2770,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -2784,9 +2784,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -2798,9 +2798,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -2812,9 +2812,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -2826,9 +2826,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -2840,9 +2840,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -2854,9 +2854,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -2868,9 +2868,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -2882,9 +2882,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -2896,9 +2896,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -2910,9 +2910,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -2924,9 +2924,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -2938,9 +2938,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -2952,9 +2952,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -2966,9 +2966,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", "cpu": [ "arm64" ], @@ -2980,9 +2980,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -2994,9 +2994,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -3008,9 +3008,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", "cpu": [ "x64" ], @@ -3022,9 +3022,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -3136,9 +3136,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "license": "MIT", "peer": true, "dependencies": { @@ -3590,9 +3590,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", - "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3611,9 +3611,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -3631,11 +3631,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -3711,9 +3711,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "dev": true, "funding": [ { @@ -3937,9 +3937,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz", + "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4148,9 +4148,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", - "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", "dev": true, "license": "ISC" }, @@ -4357,9 +4357,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4370,32 +4370,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -6128,9 +6128,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6604,9 +6604,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.25", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", - "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -7165,12 +7165,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.1" }, "engines": { "node": ">=14.0.0" @@ -7180,13 +7180,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" }, "engines": { "node": ">=14.0.0" @@ -7422,9 +7422,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { @@ -7438,28 +7438,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -8130,9 +8130,9 @@ } }, "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8520,9 +8520,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { From 997923dd98a311a5cb9495d9aa491d76d051d578 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 16 Nov 2025 12:55:16 -0500 Subject: [PATCH 290/378] Update Android release notes --- docs/releases.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 1035afea..29ff134d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,7 +2,23 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). -### ntfy Android app v1.17.8 +## ntfy Android app v1.17.13 +Released October 21, 2025 + +This release makes changes to comply with the Google Play policies. See [#1463](https://github.com/binwiederhier/ntfy/issues/1463) +or [ef57cd1](https://github.com/binwiederhier/ntfy-android/commit/ef57cd1374118b3e4d7a7ab496afe337e714fff7) for details. + +The policies do not allow directly or indirectly linking to paid plans or donation links that do not go through Google Play. + +**Changes:** + +- Remove the "Donate" button from menu (all variants) +- Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants) +- Remove links to ntfy docs and issue tracker (Play variant only) +- Remove how-to links to ntfy.sh in a few places (Play variant only) +- Remove "Copy topic address" from subscription menu (Play variant only) + +## ntfy Android app v1.17.8 Released September 23, 2025 This is largely a maintenance update to ensure the SDK is up-to-date. @@ -18,7 +34,7 @@ This is largely a maintenance update to ensure the SDK is up-to-date. * Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing) * Bumped all dependencies to the latest versions (no ticket) -### ntfy server v2.14.0 +## ntfy server v2.14.0 Released August 5, 2025 This release adds support for [declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config). This allows you to define users, ACL entries and tokens in the config file, which is useful for static deployments or deployments that use a configuration management system. @@ -34,7 +50,7 @@ will always remain open source. * [Pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support ([#1390](https://github.com/binwiederhier/ntfy/pull/1390)) * Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work) -### ntfy server v2.13.0 +## ntfy server v2.13.0 Released July 10, 2025 This is a relatively small release, mainly to support IPv6 and to add more sophisticated @@ -53,7 +69,7 @@ ntfy will always remain open source. * Update new languages from Weblate. Thanks to all the contributors! * Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app -### ntfy server v2.12.0 +## ntfy server v2.12.0 Released May 29, 2025 This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few @@ -113,7 +129,7 @@ user support in Discord/Matrix/GitHub! You rock, man! * Update new languages from Weblate. Thanks to all the contributors! * Added Tamil (தமிழ்) as a new language to the web app -### ntfy server v2.11.0 +## ntfy server v2.11.0 Released May 13, 2024 This is a tiny release that fixes a database index issue that caused performance issues on ntfy.sh. It also fixes a bug @@ -128,7 +144,7 @@ and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the * Do not set rate visitor for non-eligible topics (no ticket) * Do not cache `config.js` ([#1098](https://github.com/binwiederhier/ntfy/pull/1098), thanks to [@wunter8](https://github.com/wunter8)) -### ntfy server v2.10.0 +## ntfy server v2.10.0 Released Mar 27, 2024 This release adds support for **message templating** in the ntfy server, which allows you to include a message and/or @@ -139,7 +155,7 @@ This is great for services that let you specify a webhook URL but do not let you * [Message templating](publish.md#message-templating): You can now include a message and/or title template that will be filled with values from a JSON body ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing) -### ntfy server v2.9.0 +## ntfy server v2.9.0 Released Mar 7, 2024 A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer From 926d8b981b515a23b721c4fd441f910aff2ce0b0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 16 Nov 2025 13:35:43 -0500 Subject: [PATCH 291/378] Set "require_login: false" for dev --- web/public/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/public/config.js b/web/public/config.js index 5b904cd5..4d3bda32 100644 --- a/web/public/config.js +++ b/web/public/config.js @@ -9,7 +9,7 @@ var config = { base_url: window.location.origin, // Change to test against a different server app_root: "/", enable_login: true, - require_login: true, + require_login: false, enable_signup: true, enable_payments: false, enable_reservations: true, @@ -18,5 +18,5 @@ var config = { enable_web_push: true, billing_contact: "", web_push_public_key: "", - disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"], + disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"] }; From da6f6f528c3dd5edff04d70e5574b06691e4d14b Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 16 Nov 2025 13:39:50 -0500 Subject: [PATCH 292/378] Bump install and release notes --- docs/install.md | 62 +++++++++++++++++++++++------------------------- docs/releases.md | 32 +++++++++++++++---------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/docs/install.md b/docs/install.md index 516cdfc2..dc50e222 100644 --- a/docs/install.md +++ b/docs/install.md @@ -30,37 +30,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_amd64.tar.gz - tar zxvf ntfy_2.14.0_linux_amd64.tar.gz - sudo cp -a ntfy_2.14.0_linux_amd64/ntfy /usr/local/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.14.0_linux_amd64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.tar.gz + tar zxvf ntfy_2.15.0_linux_amd64.tar.gz + sudo cp -a ntfy_2.15.0_linux_amd64/ntfy /usr/local/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_armv6.tar.gz - tar zxvf ntfy_2.14.0_linux_armv6.tar.gz - sudo cp -a ntfy_2.14.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.14.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.tar.gz + tar zxvf ntfy_2.15.0_linux_armv6.tar.gz + sudo cp -a ntfy_2.15.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_armv7.tar.gz - tar zxvf ntfy_2.14.0_linux_armv7.tar.gz - sudo cp -a ntfy_2.14.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.14.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.tar.gz + tar zxvf ntfy_2.15.0_linux_armv7.tar.gz + sudo cp -a ntfy_2.15.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_arm64.tar.gz - tar zxvf ntfy_2.14.0_linux_arm64.tar.gz - sudo cp -a ntfy_2.14.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.14.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.tar.gz + tar zxvf ntfy_2.15.0_linux_arm64.tar.gz + sudo cp -a ntfy_2.15.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -116,7 +116,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -124,7 +124,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -132,7 +132,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -140,7 +140,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -150,28 +150,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -201,18 +201,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. -To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_darwin_all.tar.gz), +To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz), extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_darwin_all.tar.gz > ntfy_2.14.0_darwin_all.tar.gz -tar zxvf ntfy_2.14.0_darwin_all.tar.gz -sudo cp -a ntfy_2.14.0_darwin_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz > ntfy_2.15.0_darwin_all.tar.gz +tar zxvf ntfy_2.15.0_darwin_all.tar.gz +sudo cp -a ntfy_2.15.0_darwin_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_2.14.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_2.15.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -227,10 +227,9 @@ simply run: brew install ntfy ``` - ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.14.0/ntfy_2.14.0_windows_amd64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). @@ -326,7 +325,6 @@ The setup for Kubernetes is very similar to that for Docker, and requires a fair are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone unmanned pod. - === "deployment" ```yaml apiVersion: apps/v1 diff --git a/docs/releases.md b/docs/releases.md index 29ff134d..29ec8644 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,24 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). +### ntfy server v2.15.0 +Released Nov 16, 2025 + +This release adds a `require-login` flag to topics, which forces users to log in before they can +use the web app. This is useful for self-hosters and will obviously not be enabled on ntfy.sh. + +**Features:** + +* Add `require-login` flag to redirect to login page if not logged in ([#1434](https://github.com/binwiederhier/ntfy/pull/1434)/[#238](https://github.com/binwiederhier/ntfy/issues/238)/[#1329](https://github.com/binwiederhier/ntfy/pull/1329), thanks to [@theatischbein](https://github.com/theatischbein) for implementing most of this) + +**Bug fixes + maintenance:** + +* The official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh) ([#1357](https://github.com/binwiederhier/ntfy/issues/1357)/[#1401](https://github.com/binwiederhier/ntfy/issues/1401), thanks to [@skibbipl](https://github.com/skibbipl) and [@lduesing](https://github.com/lduesing) for reporting) +* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673)) +* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for + packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu) +* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting) + ## ntfy Android app v1.17.13 Released October 21, 2025 @@ -1500,16 +1518,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy server v2.15.0 (UNRELEASED) - -**Features:** - -* Add `require-login` flag to redirect to login page if not logged in ([#1434](https://github.com/binwiederhier/ntfy/pull/1434)/[#238](https://github.com/binwiederhier/ntfy/issues/238)/[#1329](https://github.com/binwiederhier/ntfy/pull/1329), thanks to [@theatischbein](https://github.com/theatischbein) for implementing most of this) - -**Bug fixes + maintenance:** - -* The official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh) ([#1357](https://github.com/binwiederhier/ntfy/issues/1357)/[#1401](https://github.com/binwiederhier/ntfy/issues/1401), thanks to [@skibbipl](https://github.com/skibbipl) and [@lduesing](https://github.com/lduesing) for reporting) -* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673)) -* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for - packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu) -* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting) +_Nothing to see, move along ..._ \ No newline at end of file From b531bc95ea9faedf68c5b3987f2744df0960384d Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 16 Nov 2025 13:43:18 -0500 Subject: [PATCH 293/378] Grr --- web/public/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/public/config.js b/web/public/config.js index 4d3bda32..fcc567aa 100644 --- a/web/public/config.js +++ b/web/public/config.js @@ -18,5 +18,5 @@ var config = { enable_web_push: true, billing_contact: "", web_push_public_key: "", - disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"] + disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"], }; From 4fa265ed28100682babfa46aef2811e34eb74753 Mon Sep 17 00:00:00 2001 From: liilliil Date: Sat, 22 Nov 2025 11:05:12 +0100 Subject: [PATCH 294/378] Translated using Weblate (Esperanto) Currently translated at 0.7% (3 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/eo/ --- web/public/static/langs/eo.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/eo.json b/web/public/static/langs/eo.json index 0967ef42..237262c4 100644 --- a/web/public/static/langs/eo.json +++ b/web/public/static/langs/eo.json @@ -1 +1,5 @@ -{} +{ + "common_cancel": "Nuligi", + "common_save": "Konservi", + "common_add": "Aldoni" +} From b7ae47d61ccb961bb414e337438b38d42500dd29 Mon Sep 17 00:00:00 2001 From: liilliil Date: Sat, 22 Nov 2025 11:05:36 +0100 Subject: [PATCH 295/378] Added translation using Weblate (Slavonic (Old Church)) --- web/public/static/langs/cu.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/cu.json diff --git a/web/public/static/langs/cu.json b/web/public/static/langs/cu.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/cu.json @@ -0,0 +1 @@ +{} From 76bf4a3de752f3d1555ea90d7e71f672f094eaa4 Mon Sep 17 00:00:00 2001 From: liilliil Date: Sat, 22 Nov 2025 11:35:11 +0100 Subject: [PATCH 296/378] Translated using Weblate (Slavonic (Old Church)) Currently translated at 1.4% (6 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/cu/ --- web/public/static/langs/cu.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/cu.json b/web/public/static/langs/cu.json index 0967ef42..73f5da0f 100644 --- a/web/public/static/langs/cu.json +++ b/web/public/static/langs/cu.json @@ -1 +1,8 @@ -{} +{ + "common_cancel": "Отмѣнити", + "common_save": "Сохрани", + "common_add": "Приложити", + "common_back": "Назадъ", + "login_form_button_submit": "Въниди", + "signup_form_password": "Таино слово" +} From 025c2963a0b9acf1e1ba31ba25907b6e40ce2748 Mon Sep 17 00:00:00 2001 From: Ali Benkassou <43420058+khazit@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:45:40 +0100 Subject: [PATCH 297/378] Add Simple Observability to integrations list --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index 4613cb58..fede1703 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -42,6 +42,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. - [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader - [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform +- [Simple Observability](https://simpleobservability.com/docs/alerts/ntfy) - Server monitoring and observability platform ## Integration via HTTP/SMTP/etc. From 693d2d630f1439b14595b04960dd3a9b6212e62c Mon Sep 17 00:00:00 2001 From: Antonio Enrico Russo Date: Fri, 28 Nov 2025 09:24:44 -0700 Subject: [PATCH 298/378] do not build fbsend with nofirebase The nofirebase build tag should remove all build dependencies on firebase. The fbsend test tool depends on firebase (and is also only useful in builds that use firebase). Hence, disable it. Signed-off-by: Antonio Enrico Russo --- tools/fbsend/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/fbsend/main.go b/tools/fbsend/main.go index 832aeb79..e5d56777 100644 --- a/tools/fbsend/main.go +++ b/tools/fbsend/main.go @@ -1,3 +1,5 @@ +//go:build !nofirebase + package main import ( From b59f451c6a04d8fbe656d8c1587f78f97eb845d7 Mon Sep 17 00:00:00 2001 From: Elie CHARRA Date: Wed, 3 Dec 2025 19:41:42 +0100 Subject: [PATCH 299/378] docs: fix typo on `listen-metrics-http` The correct option is `metrics-listen-http` --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 74325dad..98bcab1f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1531,7 +1531,7 @@ See [Installation for Docker](install.md#docker) for an example of how this coul If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)). -To configure the metrics endpoint, either set `enable-metrics` and/or set the `listen-metrics-http` option to a dedicated +To configure the metrics endpoint, either set `enable-metrics` and/or set the `metrics-listen-http` option to a dedicated listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are doing, and/or secure access to the endpoint in your reverse proxy. From 43fe1b9ad85f4a02139d3c1bf58d23f03f01895f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20D=C3=BAi=20Bolinder?= <3706841+mikaeldui@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:57:29 +0200 Subject: [PATCH 300/378] Add Ferron reverse proxy example --- docs/config.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/config.md b/docs/config.md index 74325dad..058e4d50 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1029,6 +1029,36 @@ or the root domain: redir @httpget https://{host}{uri} } ``` + +=== "ferron" + ``` kdl + // /etc/ferron.kdl + // Note that this config is most certainly incomplete. Please help out and let me know what's missing + // via Discord/Matrix or in a GitHub issue. + // Note: Ferron automatically handles both HTTP and WebSockets with proxy + + ntfy.sh { + auto_tls + auto_tls_letsencrypt_production + protocols "h1" "h2" "h3" + + proxy "http://127.0.0.1:2586" + + // Redirect HTTP to HTTPS, but only for GET topic addresses, since we want + // it to work with curl without the annoying https:// prefix + + no_redirect_to_https #true + + condition "is_get_topic" { + is_equal "{method}" "GET" + is_regex "{path}" "^/([-_a-z0-9]{0,64}$|docs/|static/)" + } + + if "is_get_topic" { + no_redirect_to_https #false + } + } + ``` ## Firebase (FCM) !!! info From e0a9e3aa56461b00f79df1ac1ec8d78dd2954659 Mon Sep 17 00:00:00 2001 From: Albert Cervera i Areny Date: Tue, 16 Dec 2025 18:08:58 +0100 Subject: [PATCH 301/378] Translated using Weblate (Catalan) Currently translated at 3.4% (14 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ca/ --- web/public/static/langs/ca.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ca.json b/web/public/static/langs/ca.json index 0d8b4bea..4490f1d5 100644 --- a/web/public/static/langs/ca.json +++ b/web/public/static/langs/ca.json @@ -3,5 +3,14 @@ "action_bar_profile_title": "Perfil", "action_bar_settings": "Configuració", "action_bar_account": "Compte", - "common_add": "Afegir" + "common_add": "Afegir", + "common_cancel": "Cancel·la", + "common_save": "Desa", + "common_back": "Enrere", + "common_copy_to_clipboard": "Copia al portaretalls", + "signup_title": "Crea un compte ntfy", + "signup_form_username": "Nom d'usuari", + "signup_form_password": "Contrasenya", + "signup_form_confirm_password": "Confirma la contrasenya", + "signup_form_button_submit": "Dona't d'alta" } From d9d02dbbc14b21ef03d57427c23765d60a5cfeab Mon Sep 17 00:00:00 2001 From: Luke Stein <44452336+lukestein@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:56:59 -0500 Subject: [PATCH 302/378] Fix formatting in Python example in publish.md --- docs/publish.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index ce3500e8..9c409523 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -705,8 +705,8 @@ As of today, **Markdown is only supported in the web app.** Here's an example of === "Python" ``` python requests.post("https://ntfy.sh/mytopic", - data="Look ma, **bold text**, *italics*, ..." - headers={ "Markdown": "yes" })) + data="Look ma, **bold text**, *italics*, ...", + headers={ "Markdown": "yes" }) ``` === "PHP" From 430135606b93803af22a89e242cc9e80aae1df1b Mon Sep 17 00:00:00 2001 From: faytecCD Date: Thu, 18 Dec 2025 16:03:24 +0800 Subject: [PATCH 303/378] Fix filter api example --- docs/subscribe/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index d98134e5..a52e17f6 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -295,7 +295,7 @@ Available filters (all case-insensitive): | `message` | `X-Message`, `m` | `ntfy.sh/mytopic/json?message=lalala` | Only return messages that match this exact message string | | `title` | `X-Title`, `t` | `ntfy.sh/mytopic/json?title=some+title` | Only return messages that match this exact title string | | `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) | -| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?/jsontags=error,alert` | Only return messages that match *all listed tags* (comma-separated) | +| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic/json?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) | ### Subscribe to multiple topics It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics From 7f842eaeb11eaa4fe3aa1c4a76c61ff9e64283de Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 20 Dec 2025 20:50:11 -0500 Subject: [PATCH 304/378] Release notes --- docs/releases.md | 56 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 29ec8644..6c303755 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,7 +2,31 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). -### ntfy server v2.15.0 +## Current stable releases + +| Component | Version | Release date | +|------------------|---------|--------------| +| ntfy server | v2.15.0 | Nov 16, 2025 | +| ntfy Android app | v1.18.0 | Dec 4, 2025 | +| ntfy iOS app | v1.3 | Nov 26, 2023 | + +Please check out the release notes for [upcoming releases](#not-released-yet) below. + +## ntfy Android app v1.18.0 +Released December 4, 2025 + +**Features:** + +* Added GIF support for preview images ([ntfy-android#76](https://github.com/binwiederhier/ntfy-android/pull/76)/[#532](https://github.com/binwiederhier/ntfy/issues/532), thanks to [@MichaelArkh](https://github.com/MichaelArkh) and [@dimatx](https://github.com/dimatx) for reporting) +* Added WebP support for preview images ([ntfy-android#81](https://github.com/binwiederhier/ntfy-android/pull/81)/[ntfy-android#80](https://github.com/binwiederhier/ntfy-android/issues/80), thanks to [@jokakilla](https://github.com/jokakilla)) +* Added UnifiedPush distributor selection support ([#137](https://github.com/binwiederhier/ntfy-android/pull/137), thanks to [@p1gp1g](https://github.com/p1gp1g)) + +**Bug fixes + maintenance:** + +* Remove REQUEST_INSTALL_PACKAGES permission ([#684](https://github.com/binwiederhier/ntfy/issues/684)) +* Request to ignore battery optimizations before receiving subscription ([ntfy-android#97](https://github.com/binwiederhier/ntfy-android/pull/97), thanks to [@p1gp1g](https://github.com/p1gp1g)) + +## ntfy server v2.15.0 Released Nov 16, 2025 This release adds a `require-login` flag to topics, which forces users to log in before they can @@ -30,11 +54,11 @@ The policies do not allow directly or indirectly linking to paid plans or donati **Changes:** -- Remove the "Donate" button from menu (all variants) -- Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants) -- Remove links to ntfy docs and issue tracker (Play variant only) -- Remove how-to links to ntfy.sh in a few places (Play variant only) -- Remove "Copy topic address" from subscription menu (Play variant only) +* Remove the "Donate" button from menu (all variants) +* Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants) +* Remove links to ntfy docs and issue tracker (Play variant only) +* Remove how-to links to ntfy.sh in a few places (Play variant only) +* Remove "Copy topic address" from subscription menu (Play variant only) ## ntfy Android app v1.17.8 Released September 23, 2025 @@ -1518,4 +1542,22 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -_Nothing to see, move along ..._ \ No newline at end of file +### ntfy Android app v1.19.x + +This release upgrades the Android app to use [Material 3](https://m3.material.io/) design components and adds the +ability to use [dynamic colors](https://developer.android.com/develop/ui/views/theming/dynamic-colors). +**This was a lot of work** and I want to thank [@Bnyro](https://github.com/Bnyro) and [@cyb3rko](https://github.com/cyb3rko) for implementing this. You guys rock! + +**Features:** + +* Moved the user interface to Material 3 and added dynamic color support ([#580](https://github.com/binwiederhier/ntfy/issues/580), + [ntfy-android#56](https://github.com/binwiederhier/ntfy-android/pull/56), [ntfy-android#126](https://github.com/binwiederhier/ntfy-android/pull/126), + [ntfy-android#135](https://github.com/binwiederhier/ntfy-android/pull/135), thanks to [@Bnyro](https://github.com/Bnyro) + and [@cyb3rko](https://github.com/cyb3rko) for the implementation, and to [@RokeJulianLockhart](https://github.com/RokeJulianLockhart) for reporting) + +### ntfy Android app v1.20.x + +**Bug fixes + maintenance:** + +* Updated dependencies, minimum SDK version to 26, clean up legacy code, upgrade Gradle ([ntfy-android#140](https://github.com/binwiederhier/ntfy-android/pull/140), + thanks to [@cyb3rko](https://github.com/cyb3rko) for the implementation) From a380860cab406db492593fc008c53ab98b5c1039 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 20 Dec 2025 20:53:18 -0500 Subject: [PATCH 305/378] Bump --- go.mod | 58 +-- go.sum | 124 +++--- web/package-lock.json | 901 ++++++++++++++++++++++++++++++------------ 3 files changed, 744 insertions(+), 339 deletions(-) diff --git a/go.mod b/go.mod index 811cbcad..e6d91101 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,22 @@ toolchain go1.24.5 require ( cloud.google.com/go/firestore v1.20.0 // indirect - cloud.google.com/go/storage v1.57.2 // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + cloud.google.com/go/storage v1.58.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 - github.com/gabriel-vasile/mimetype v1.4.11 + github.com/gabriel-vasile/mimetype v1.4.12 github.com/gorilla/websocket v1.5.3 github.com/mattn/go-sqlite3 v1.14.32 github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 - golang.org/x/crypto v0.44.0 - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 - golang.org/x/term v0.37.0 + golang.org/x/crypto v0.46.0 + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 + golang.org/x/term v0.38.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.256.0 + google.golang.org/api v0.258.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -35,13 +35,13 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.23.2 github.com/stripe/stripe-go/v74 v74.30.0 - golang.org/x/text v0.31.0 + golang.org/x/text v0.32.0 ) require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth v0.18.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect @@ -55,11 +55,11 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -70,35 +70,35 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9d768eb0..babf7049 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= +cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -18,16 +18,16 @@ cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qob cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4= -cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk= +cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= +cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= @@ -46,8 +46,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e h1:gt7U1Igw0xbJdyaCM5H2CnlAlPSkzrhsebQB6WQWjLA= -github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -58,18 +58,18 @@ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= -github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -98,8 +98,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -131,8 +131,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -156,24 +156,24 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBi github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= @@ -184,8 +184,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -200,10 +200,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -211,8 +211,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -225,8 +225,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -236,8 +236,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -249,8 +249,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -263,22 +263,22 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc= +google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY= -google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 h1:stRtB2UVzFOWnorVuwF0BVVEjQ3AN6SjHWdg811UIQM= +google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/web/package-lock.json b/web/package-lock.json index 2da1d1c0..789b95f7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2298,6 +2298,76 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2728,9 +2798,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", "cpu": [ "arm" ], @@ -2742,9 +2812,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", "cpu": [ "arm64" ], @@ -2756,9 +2826,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", "cpu": [ "arm64" ], @@ -2770,9 +2840,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "cpu": [ "x64" ], @@ -2784,9 +2854,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", "cpu": [ "arm64" ], @@ -2798,9 +2868,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", "cpu": [ "x64" ], @@ -2812,9 +2882,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", "cpu": [ "arm" ], @@ -2826,9 +2896,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", "cpu": [ "arm" ], @@ -2840,9 +2910,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", "cpu": [ "arm64" ], @@ -2854,9 +2924,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", "cpu": [ "arm64" ], @@ -2868,9 +2938,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", "cpu": [ "loong64" ], @@ -2882,9 +2952,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", "cpu": [ "ppc64" ], @@ -2896,9 +2966,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", "cpu": [ "riscv64" ], @@ -2910,9 +2980,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", "cpu": [ "riscv64" ], @@ -2924,9 +2994,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", "cpu": [ "s390x" ], @@ -2938,9 +3008,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "cpu": [ "x64" ], @@ -2952,9 +3022,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "cpu": [ "x64" ], @@ -2966,9 +3036,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", "cpu": [ "arm64" ], @@ -2980,9 +3050,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", "cpu": [ "arm64" ], @@ -2994,9 +3064,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", "cpu": [ "ia32" ], @@ -3008,9 +3078,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", "cpu": [ "x64" ], @@ -3022,9 +3092,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", "cpu": [ "x64" ], @@ -3136,13 +3206,13 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", - "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", "peer": true, "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-transition-group": { @@ -3590,9 +3660,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", - "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3611,9 +3681,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -3631,11 +3701,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3711,9 +3781,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", "dev": true, "funding": [ { @@ -3855,13 +3925,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", - "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.26.3" + "browserslist": "^4.28.0" }, "funding": { "type": "opencollective", @@ -3937,9 +4007,9 @@ } }, "node_modules/csstype": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz", - "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4131,6 +4201,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -4148,9 +4225,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.254", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", - "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -4180,9 +4257,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -4269,27 +4346,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -5026,6 +5103,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -5420,9 +5514,9 @@ } }, "node_modules/humanize-duration": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.1.tgz", - "integrity": "sha512-hwzSCymnRdFx9YdRkQQ0OYequXiVAV6ZGQA2uzocwB0F4309Ke6pO8dg0P8LHhRQJyVjGteRTAA/zNfEcpXn8A==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.2.tgz", + "integrity": "sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ==", "license": "Unlicense", "funding": { "url": "https://github.com/sponsors/EvanHahn" @@ -5775,6 +5869,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -6097,6 +6201,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -6551,6 +6671,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6810,6 +6940,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6894,6 +7031,33 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7072,24 +7236,24 @@ } }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.3" } }, "node_modules/react-i18next": { @@ -7115,9 +7279,9 @@ } }, "node_modules/react-infinite-scroll-component": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", - "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.1.tgz", + "integrity": "sha512-R8YoOyiNDynSWmfVme5LHslsKrP+/xcRUWR2ies8UgUab9dtyw5ECnMCVPPmnmjjF4MWQmfVdRwRWcWaDgeyMA==", "license": "MIT", "dependencies": { "throttle-debounce": "^2.1.0" @@ -7127,9 +7291,9 @@ } }, "node_modules/react-is": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", - "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT" }, "node_modules/react-refresh": { @@ -7422,9 +7586,9 @@ } }, "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, "license": "MIT", "dependencies": { @@ -7438,28 +7602,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" } }, @@ -7737,6 +7901,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -7861,6 +8038,76 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -8002,6 +8249,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -8520,9 +8781,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -8666,17 +8927,17 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.1.0.tgz", - "integrity": "sha512-VsSpdubPzXhHWVINcSx6uHRMpOHVHQcHsef1QgkOlEoaIDAlssFEW88LBq1a59BuokAhsh2kUDJbaX1bZv4Bjw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" }, "engines": { "node": ">=16.0.0" @@ -8687,8 +8948,8 @@ "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" }, "peerDependenciesMeta": { "@vite-pwa/assets-generator": { @@ -8847,30 +9108,30 @@ } }, "node_modules/workbox-background-sync": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", - "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-broadcast-update": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz", - "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-build": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz", - "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", "dev": true, "license": "MIT", "dependencies": { @@ -8887,33 +9148,33 @@ "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", - "glob": "^7.1.6", + "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", + "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "7.3.0", - "workbox-broadcast-update": "7.3.0", - "workbox-cacheable-response": "7.3.0", - "workbox-core": "7.3.0", - "workbox-expiration": "7.3.0", - "workbox-google-analytics": "7.3.0", - "workbox-navigation-preload": "7.3.0", - "workbox-precaching": "7.3.0", - "workbox-range-requests": "7.3.0", - "workbox-recipes": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0", - "workbox-streams": "7.3.0", - "workbox-sw": "7.3.0", - "workbox-window": "7.3.0" + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { @@ -9021,6 +9282,30 @@ "dev": true, "license": "MIT" }, + "node_modules/workbox-build/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -9028,6 +9313,22 @@ "dev": true, "license": "MIT" }, + "node_modules/workbox-build/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/workbox-build/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -9114,140 +9415,241 @@ } }, "node_modules/workbox-cacheable-response": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", - "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-core": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz", - "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", "dev": true, "license": "MIT" }, "node_modules/workbox-expiration": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz", - "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-google-analytics": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz", - "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-background-sync": "7.3.0", - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-navigation-preload": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz", - "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-precaching": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz", - "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-range-requests": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz", - "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-recipes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz", - "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-cacheable-response": "7.3.0", - "workbox-core": "7.3.0", - "workbox-expiration": "7.3.0", - "workbox-precaching": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-routing": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz", - "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-strategies": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz", - "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-streams": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz", - "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" } }, "node_modules/workbox-sw": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz", - "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", "dev": true, "license": "MIT" }, "node_modules/workbox-window": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz", - "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", "dev": true, "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -9274,9 +9676,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", "optional": true, @@ -9286,6 +9688,9 @@ }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { From 6cacdd47f2c113481d833cfac46892c0abdbcf46 Mon Sep 17 00:00:00 2001 From: luneth <4kn30x69@protonmail.ch> Date: Sat, 20 Dec 2025 21:11:09 +0100 Subject: [PATCH 306/378] Translated using Weblate (French) Currently translated at 100.0% (405 of 405 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/ --- web/public/static/langs/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/fr.json b/web/public/static/langs/fr.json index fd8fa78c..572ad659 100644 --- a/web/public/static/langs/fr.json +++ b/web/public/static/langs/fr.json @@ -15,7 +15,7 @@ "notifications_attachment_copy_url_title": "Copier l'URL de la pièce jointe dans le presse-papiers", "notifications_attachment_open_title": "Aller à {{url}}", "notifications_attachment_link_expired": "lien de téléchargement expiré", - "nav_button_publish_message": "Publier la notification", + "nav_button_publish_message": "Publier une notification", "notifications_copied_to_clipboard": "Copié dans le presse-papiers", "alert_not_supported_title": "Notifications non prises en charge", "notifications_tags": "Étiquettes", @@ -272,7 +272,7 @@ "account_delete_dialog_button_submit": "Supprimer définitivement le compte", "account_delete_dialog_billing_warning": "Supprimer votre compte annule aussi immédiatement votre facturation. Vous n'aurez plus accès à votre tableau de bord de facturation.", "account_upgrade_dialog_title": "Changer le tarif du compte", - "account_upgrade_dialog_proration_info": "Facturation : Lors d'un changement vers un tiers payant, la différence de prix sera débitée immédiatement. En passant d'un tiers payant a gratuit, votre solde sera utilisé pour payer de futur factures.", + "account_upgrade_dialog_proration_info": "Proratisation : Lors d'un changement vers le haut entre plans payants, la différence de prix sera facturée immédiatement. En cas de diminutions vers un plan plus économique, la balance sera utilisée pour le paiement des factures suivantes.", "account_upgrade_dialog_reservations_warning_other": "Le tarif sélectionné autorise moins de sujets réservés que votre tarif actuel. Avant de changer de tarif, veuillez supprimer au moins {{count}} sujets réservés. Vous pouvez supprimer des sujets réservés dans les Paramètres.", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} sujets réservés", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers", From 4e03c96108b29d457165235afeafa569e42bccb7 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 21 Dec 2025 21:02:02 -0500 Subject: [PATCH 307/378] Bump release notes --- docs/releases.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 6c303755..bb84961e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -7,11 +7,25 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release | Component | Version | Release date | |------------------|---------|--------------| | ntfy server | v2.15.0 | Nov 16, 2025 | -| ntfy Android app | v1.18.0 | Dec 4, 2025 | +| ntfy Android app | v1.19.4 | Dec 21, 2025 | | ntfy iOS app | v1.3 | Nov 26, 2023 | Please check out the release notes for [upcoming releases](#not-released-yet) below. +### ntfy Android app v1.19.4 +Released December 21, 2025 + +This release upgrades the Android app to use [Material 3](https://m3.material.io/) design components and adds the +ability to use [dynamic colors](https://developer.android.com/develop/ui/views/theming/dynamic-colors). +**This was a lot of work** and I want to thank [@Bnyro](https://github.com/Bnyro) and [@cyb3rko](https://github.com/cyb3rko) for implementing this. You guys rock! + +**Features:** + +* Moved the user interface to Material 3 and added dynamic color support ([#580](https://github.com/binwiederhier/ntfy/issues/580), + [ntfy-android#56](https://github.com/binwiederhier/ntfy-android/pull/56), [ntfy-android#126](https://github.com/binwiederhier/ntfy-android/pull/126), + [ntfy-android#135](https://github.com/binwiederhier/ntfy-android/pull/135), thanks to [@Bnyro](https://github.com/Bnyro) + and [@cyb3rko](https://github.com/cyb3rko) for the implementation, and to [@RokeJulianLockhart](https://github.com/RokeJulianLockhart) for reporting) + ## ntfy Android app v1.18.0 Released December 4, 2025 @@ -1542,19 +1556,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy Android app v1.19.x - -This release upgrades the Android app to use [Material 3](https://m3.material.io/) design components and adds the -ability to use [dynamic colors](https://developer.android.com/develop/ui/views/theming/dynamic-colors). -**This was a lot of work** and I want to thank [@Bnyro](https://github.com/Bnyro) and [@cyb3rko](https://github.com/cyb3rko) for implementing this. You guys rock! - -**Features:** - -* Moved the user interface to Material 3 and added dynamic color support ([#580](https://github.com/binwiederhier/ntfy/issues/580), - [ntfy-android#56](https://github.com/binwiederhier/ntfy-android/pull/56), [ntfy-android#126](https://github.com/binwiederhier/ntfy-android/pull/126), - [ntfy-android#135](https://github.com/binwiederhier/ntfy-android/pull/135), thanks to [@Bnyro](https://github.com/Bnyro) - and [@cyb3rko](https://github.com/cyb3rko) for the implementation, and to [@RokeJulianLockhart](https://github.com/RokeJulianLockhart) for reporting) - ### ntfy Android app v1.20.x **Bug fixes + maintenance:** From 17870cb471e1bdafe535b584d89e4bda434ff1f8 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 21 Dec 2025 21:37:55 -0500 Subject: [PATCH 308/378] Fix use of deprecated secrets file --- server/server_firebase.go | 2 +- tools/fbsend/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/server_firebase.go b/server/server_firebase.go index 1b80172e..13e80b93 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -80,7 +80,7 @@ type firebaseSenderImpl struct { } func newFirebaseSender(credentialsFile string) (firebaseSender, error) { - fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile)) + fb, err := firebase.NewApp(context.Background(), nil, option.WithAuthCredentialsFile(option.ServiceAccount, credentialsFile)) if err != nil { return nil, err } diff --git a/tools/fbsend/main.go b/tools/fbsend/main.go index 832aeb79..9c5e9f70 100644 --- a/tools/fbsend/main.go +++ b/tools/fbsend/main.go @@ -26,7 +26,7 @@ func main() { } data[kv[0]] = kv[1] } - fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(*conffile)) + fb, err := firebase.NewApp(context.Background(), nil, option.WithAuthCredentialsFile(option.ServiceAccount, *conffile)) if err != nil { fail(err.Error()) } From 152a6b96d1f029b610e47b7c37e29720bc8da5a2 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 23 Dec 2025 20:23:48 -0500 Subject: [PATCH 309/378] Run go fix --- cmd/publish_unix.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/publish_unix.go b/cmd/publish_unix.go index 3ce22ffc..d2b49a5e 100644 --- a/cmd/publish_unix.go +++ b/cmd/publish_unix.go @@ -1,5 +1,4 @@ //go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd -// +build darwin linux dragonfly freebsd netbsd openbsd package cmd From b27c4cf95d800a19809fb37ba5c4e9b62595f627 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 26 Dec 2025 13:20:20 -0500 Subject: [PATCH 310/378] Release notes --- docs/releases.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index bb84961e..3f9fa7a3 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1562,3 +1562,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Updated dependencies, minimum SDK version to 26, clean up legacy code, upgrade Gradle ([ntfy-android#140](https://github.com/binwiederhier/ntfy-android/pull/140), thanks to [@cyb3rko](https://github.com/cyb3rko) for the implementation) + +### ntfy Android app v1.21.x + +**Bug fixes + maintenance:** + +* Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco)) From 76fbe19fd14c758a9c30beae08040884ff14c7bc Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 28 Dec 2025 12:48:55 -0500 Subject: [PATCH 311/378] Release notes --- docs/releases.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index 3f9fa7a3..12810619 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1565,6 +1565,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ### ntfy Android app v1.21.x +**Features:** + +* Add message bar to publish messages from the app ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) + **Bug fixes + maintenance:** * Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco)) From 954ac89b51b724ad5bbaebffac43f55c468305de Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 28 Dec 2025 20:20:26 -0500 Subject: [PATCH 312/378] Release notes --- docs/releases.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 12810619..0ab9b237 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -12,6 +12,22 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release Please check out the release notes for [upcoming releases](#not-released-yet) below. +### ntfy Android app v1.20.x +Released December 28, 2025 + +This is the last pure maintenance release for now. It'll bring all dependencies and library version to the latest version, +and fixes some crashes. I had to drop support for about 4,000 devices (only ~200 installations), because the libraries +themselves do not support SDK 21 anymore, which was the previous minimum SDK version (Android 5, 2014). Now the minimum +SDK version is 26 (Android 8, 2017). + +**Bug fixes + maintenance:** + +* Updated dependencies, minimum SDK version to 26, clean up legacy code, upgrade Gradle ([ntfy-android#140](https://github.com/binwiederhier/ntfy-android/pull/140), + thanks to [@cyb3rko](https://github.com/cyb3rko) for the implementation) +* Updated target SDK version to 36 (Android 8, 2017) +* Fixed ForegroundServiceDidNotStartInTimeException ([#1520](https://github.com/binwiederhier/ntfy/issues/1520)) +* Fixed crashes with redrawing the list when temporarily muted topics expire + ### ntfy Android app v1.19.4 Released December 21, 2025 @@ -1556,18 +1572,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy Android app v1.20.x - -**Bug fixes + maintenance:** - -* Updated dependencies, minimum SDK version to 26, clean up legacy code, upgrade Gradle ([ntfy-android#140](https://github.com/binwiederhier/ntfy-android/pull/140), - thanks to [@cyb3rko](https://github.com/cyb3rko) for the implementation) - ### ntfy Android app v1.21.x **Features:** -* Add message bar to publish messages from the app ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) +* Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) +* Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting) **Bug fixes + maintenance:** From 6215113c919c5ed7a3fa0e727aa3a3cef1606110 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 29 Dec 2025 16:58:22 -0500 Subject: [PATCH 313/378] Release notes --- docs/releases.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 0ab9b237..fb817a82 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -12,7 +12,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release Please check out the release notes for [upcoming releases](#not-released-yet) below. -### ntfy Android app v1.20.x +## ntfy Android app v1.20.x Released December 28, 2025 This is the last pure maintenance release for now. It'll bring all dependencies and library version to the latest version, @@ -28,7 +28,7 @@ SDK version is 26 (Android 8, 2017). * Fixed ForegroundServiceDidNotStartInTimeException ([#1520](https://github.com/binwiederhier/ntfy/issues/1520)) * Fixed crashes with redrawing the list when temporarily muted topics expire -### ntfy Android app v1.19.4 +## ntfy Android app v1.19.4 Released December 21, 2025 This release upgrades the Android app to use [Material 3](https://m3.material.io/) design components and adds the @@ -1577,8 +1577,11 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** * Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) +* Define custom HTTP headers to support authenticated proxies, tunnels and SSO ([ntfy-android#116](https://github.com/binwiederhier/ntfy-android/issues/116), [#1018](https://github.com/binwiederhier/ntfy/issues/1018), [ntfy-android#132](https://github.com/binwiederhier/ntfy-android/pull/132), [ntfy-android#146](https://github.com/binwiederhier/ntfy-android/pull/146), thanks to [@CrazyWolf13](https://github.com/CrazyWolf13)) * Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting) +* Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting) **Bug fixes + maintenance:** * Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco)) +* Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel)) From d83be77727745dc064268ca7bf27fa0ab42b83aa Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 29 Dec 2025 21:07:31 -0500 Subject: [PATCH 314/378] Release notes --- docs/releases.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index fb817a82..5ef79633 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -4,15 +4,15 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Current stable releases -| Component | Version | Release date | -|------------------|---------|--------------| -| ntfy server | v2.15.0 | Nov 16, 2025 | -| ntfy Android app | v1.19.4 | Dec 21, 2025 | -| ntfy iOS app | v1.3 | Nov 26, 2023 | +| Component | Version | Release date | +|------------------------------------------|---------|--------------| +| ntfy server | v2.15.0 | Nov 16, 2025 | +| ntfy Android app (_is being rolled out_) | v1.20.0 | Dec 28, 2025 | +| ntfy iOS app | v1.3 | Nov 26, 2023 | Please check out the release notes for [upcoming releases](#not-released-yet) below. -## ntfy Android app v1.20.x +## ntfy Android app v1.20.0 Released December 28, 2025 This is the last pure maintenance release for now. It'll bring all dependencies and library version to the latest version, @@ -1578,6 +1578,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) * Define custom HTTP headers to support authenticated proxies, tunnels and SSO ([ntfy-android#116](https://github.com/binwiederhier/ntfy-android/issues/116), [#1018](https://github.com/binwiederhier/ntfy/issues/1018), [ntfy-android#132](https://github.com/binwiederhier/ntfy-android/pull/132), [ntfy-android#146](https://github.com/binwiederhier/ntfy-android/pull/146), thanks to [@CrazyWolf13](https://github.com/CrazyWolf13)) +* Implement UnifiedPush "raise to foreground" requirement ([ntfy-android#98](https://github.com/binwiederhier/ntfy-android/pull/98), [ntfy-android#148](https://github.com/binwiederhier/ntfy-android/pull/148), thanks to [@p1gp1g](https://github.com/p1gp1g)) * Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting) * Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting) From 3dea1b12cf912ff565b968321d11a3897b38d698 Mon Sep 17 00:00:00 2001 From: Shoshin Akamine Date: Tue, 30 Dec 2025 04:27:04 +0100 Subject: [PATCH 315/378] Translated using Weblate (Japanese) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/ --- web/public/static/langs/ja.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json index 3d9643e0..ebd76b54 100644 --- a/web/public/static/langs/ja.json +++ b/web/public/static/langs/ja.json @@ -403,5 +403,7 @@ "prefs_appearance_theme_system": "システム (既定)", "prefs_appearance_theme_dark": "ダークモード", "web_push_unknown_notification_title": "不明な通知を受信しました", - "web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります" + "web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります", + "account_basics_cannot_edit_or_delete_provisioned_user": "自動作成されたユーザーの編集や削除はできません", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "自動作成されたトークンは編集や削除はできません" } From f52b29931ff8f7fce0a598bd9770beacc0107d68 Mon Sep 17 00:00:00 2001 From: 109247019824 <109247019824@users.noreply.hosted.weblate.org> Date: Mon, 29 Dec 2025 20:12:44 +0100 Subject: [PATCH 316/378] Translated using Weblate (Bulgarian) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/ --- web/public/static/langs/bg.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json index 31716755..465e969b 100644 --- a/web/public/static/langs/bg.json +++ b/web/public/static/langs/bg.json @@ -403,5 +403,7 @@ "prefs_appearance_theme_system": "Системна (подразбирана)", "web_push_subscription_expiring_title": "Известията временно ще бъдат спрени", "web_push_subscription_expiring_body": "За да продължите да получавате известия, отворете ntfy", - "action_bar_unmute_notifications": "Включване звука на известията" + "action_bar_unmute_notifications": "Включване звука на известията", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Кодът за защита от външна система не може да бъде променян или премахван", + "account_basics_cannot_edit_or_delete_provisioned_user": "Потребител от външна система не може да бъде променян или премахван" } From 293dda3e7930bb66e758d007ec77453a6ae6108e Mon Sep 17 00:00:00 2001 From: Kachelkaiser Date: Mon, 29 Dec 2025 12:07:23 +0100 Subject: [PATCH 317/378] Translated using Weblate (German) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/ --- web/public/static/langs/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 0654483a..dc98901e 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -403,5 +403,7 @@ "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom Server empfangen", "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest", - "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht geöffnet ist (via Web Push)" + "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht geöffnet ist (via Web Push)", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Bereitgestelltes Token kann nicht bearbeitet oder gelöscht werden", + "account_basics_cannot_edit_or_delete_provisioned_user": "Ein bereitgestellter Benutzer kann nicht bearbeitet oder gelöscht werden" } From f48a9aef2c2ee4587210f427eed27ea5ae0b29e5 Mon Sep 17 00:00:00 2001 From: luneth <4kn30x69@protonmail.ch> Date: Tue, 30 Dec 2025 10:58:50 +0100 Subject: [PATCH 318/378] Translated using Weblate (French) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/ --- web/public/static/langs/fr.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/fr.json b/web/public/static/langs/fr.json index 572ad659..33112327 100644 --- a/web/public/static/langs/fr.json +++ b/web/public/static/langs/fr.json @@ -64,7 +64,7 @@ "notifications_actions_not_supported": "Cette action n'est pas supportée dans l'application web", "notifications_actions_http_request_title": "Envoyer une requête HTTP {{method}} à {{url}}", "publish_dialog_attachment_limits_quota_reached": "quota dépassé, {{remainingBytes}} restants", - "publish_dialog_tags_placeholder": "Liste séparée par des virgules d'étiquettes, par ex. avertissement,backup-srv1", + "publish_dialog_tags_placeholder": "Liste d'étiquettes séparée par des virgules, par ex. avertissement,backup-srv1", "publish_dialog_priority_label": "Priorité", "publish_dialog_click_label": "URL du clic", "publish_dialog_click_placeholder": "URL ouverte lors d'un clic sur la notification", @@ -403,5 +403,7 @@ "web_push_subscription_expiring_title": "Les notifications seront suspendues", "web_push_subscription_expiring_body": "Ouvrez ntfy pour continuer à recevoir les notifications", "web_push_unknown_notification_title": "Notification inconnue reçue du serveur", - "web_push_unknown_notification_body": "Il est possible que vous deviez mettre à jour ntfy en ouvrant l'application web" + "web_push_unknown_notification_body": "Il est possible que vous deviez mettre à jour ntfy en ouvrant l'application web", + "account_basics_cannot_edit_or_delete_provisioned_user": "Un utilisateur provisionné ne peut pas être modifié ou supprimé", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Impossible de modifier ou de supprimer le jeton provisionné" } From 06577c99f252c660c62fab7ecd29e72a69ca4e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 29 Dec 2025 14:52:10 +0100 Subject: [PATCH 319/378] Translated using Weblate (Estonian) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/ --- web/public/static/langs/et.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/et.json b/web/public/static/langs/et.json index 8775900d..19c0c704 100644 --- a/web/public/static/langs/et.json +++ b/web/public/static/langs/et.json @@ -139,7 +139,7 @@ "display_name_dialog_placeholder": "Kuvatav nimi", "publish_dialog_title_no_topic": "Avalda teavitus", "publish_dialog_progress_uploading": "Laadin üles…", - "publish_dialog_message_published": "Teavitus on saadetud", + "publish_dialog_message_published": "Teavitus on avaldatud", "publish_dialog_emoji_picker_show": "Vali emoji", "publish_dialog_priority_low": "Vähetähtis", "publish_dialog_priority_default": "Vaikimisi tähtsus", @@ -403,5 +403,7 @@ "account_upgrade_dialog_proration_info": "Summade jagamine: Kui muudad teenusepaketti paremaks, siis pead hinnavahe maksma kohe. Kui muudad teenusepaketti madalamaks, siis hinnavahe arvelt hüvituvad mõned järgmised maksed.", "account_upgrade_dialog_reservations_warning_one": "Sinu praegune teenusepakett võimaldab senise paketiga võrreldes reserveerida vähem teemasid. Enne paketi muutmist palun esmalt kustuta vähemalt üks reserveering. Seda saad teha siin.", "account_upgrade_dialog_reservations_warning_other": "Sinu praegune teenusepakett võimaldab senise paketiga võrreldes reserveerida vähem teemasid. Enne paketi muutmist palun esmalt kustuta vähemalt {{count}} reserveeringut. Seda saad teha siin.", - "prefs_users_description": "Oma kaitstud teemade kasutajaid saad lisada ja eemaldada siin. Palun arvesta, et kasutajanimi ja salasõna on salvestatud veebibrauseri kohalikus andmeruumis." + "prefs_users_description": "Oma kaitstud teemade kasutajaid saad lisada ja eemaldada siin. Palun arvesta, et kasutajanimi ja salasõna on salvestatud veebibrauseri kohalikus andmeruumis.", + "account_basics_cannot_edit_or_delete_provisioned_user": "Eelsisestatud kasutajat ei saa muuta ega kustutada", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Eelsisestatud tunnusluba ei saa muuta ega kustutada" } From b348bce06e2457bfe5f0d5f99d8de65d8e71806a Mon Sep 17 00:00:00 2001 From: Eskuero <3skuero@gmail.com> Date: Mon, 29 Dec 2025 19:50:19 +0100 Subject: [PATCH 320/378] Translated using Weblate (Spanish) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/ --- web/public/static/langs/es.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/es.json b/web/public/static/langs/es.json index 1d516e5d..598d271a 100644 --- a/web/public/static/langs/es.json +++ b/web/public/static/langs/es.json @@ -404,5 +404,7 @@ "prefs_appearance_theme_dark": "Oscuro", "web_push_subscription_expiring_body": "Abrir ntfy para seguir recibiendo notificaciones", "web_push_unknown_notification_title": "Notificación desconocida recibida del servidor", - "web_push_unknown_notification_body": "Puede que necesites actualizar ntfy abriendo la aplicación web" + "web_push_unknown_notification_body": "Puede que necesites actualizar ntfy abriendo la aplicación web", + "account_basics_cannot_edit_or_delete_provisioned_user": "Un usuario provisionado no se puede editar o eliminar", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "No se puede editar o eliminar un token provisionado" } From c0f57a448d760217c07a2e1abe699df76bbd6a64 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 30 Dec 2025 10:06:47 -0500 Subject: [PATCH 321/378] Document ?display= parameter, update changelog --- docs/releases.md | 1 + docs/subscribe/phone.md | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 5ef79633..74f5c9ef 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1581,6 +1581,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Implement UnifiedPush "raise to foreground" requirement ([ntfy-android#98](https://github.com/binwiederhier/ntfy-android/pull/98), [ntfy-android#148](https://github.com/binwiederhier/ntfy-android/pull/148), thanks to [@p1gp1g](https://github.com/p1gp1g)) * Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting) * Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting) +* Support for port and display name in [ntfy://](subscribe/phone.md#ntfy-links) links ([ntfy-android#130](https://github.com/binwiederhier/ntfy-android/pull/130), thanks to [@godovski](https://github.com/godovski)) **Bug fixes + maintenance:** diff --git a/docs/subscribe/phone.md b/docs/subscribe/phone.md index 94798f43..3015be88 100644 --- a/docs/subscribe/phone.md +++ b/docs/subscribe/phone.md @@ -129,10 +129,11 @@ or to simply directly link to a topic from a mobile website. **Supported link formats:** -| Link format | Example | Description | -|-------------------------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ntfy:///` | `ntfy://ntfy.sh/mytopic` | Directly opens the Android app detail view for the given topic and server. Subscribes to the topic if not already subscribed. This is equivalent to the web view `https://ntfy.sh/mytopic` (HTTPS!) | -| `ntfy:///?secure=false` | `ntfy://example.com/mytopic?secure=false` | Same as above, except that this will use HTTP instead of HTTPS as topic URL. This is equivalent to the web view `http://example.com/mytopic` (HTTP!) | +| Link format | Example | Description | +|---------------------------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ntfy:///` | `ntfy://ntfy.sh/mytopic` | Directly opens the Android app detail view for the given topic and server. Subscribes to the topic if not already subscribed. This is equivalent to the web view `https://ntfy.sh/mytopic` (HTTPS!) | +| `ntfy:///?display=` | `ntfy://ntfy.sh/mytopic?display=My+Topic` | Same as above, but also defines a display name for the topic. | +| `ntfy:///?secure=false` | `ntfy://example.com/mytopic?secure=false` | Same as above, except that this will use HTTP instead of HTTPS as topic URL. This is equivalent to the web view `http://example.com/mytopic` (HTTP!) | ## Integrations From 90667e7176799f1686dee512e5bc626bcdae2f63 Mon Sep 17 00:00:00 2001 From: Cliff Brake Date: Tue, 30 Dec 2025 17:19:07 -0500 Subject: [PATCH 322/378] add BRun to list of integrations --- docs/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations.md b/docs/integrations.md index fede1703..948cd758 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -181,6 +181,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy - [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy. - [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go) +- [BRun](https://github.com/cbrake/brun) - Native Linux automation platform connecting triggers to actions without containers (Go) ## Blog + forum posts From 49b3c724cf87e4a353388c428e5b2f1898593061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Tue, 30 Dec 2025 14:20:07 +0100 Subject: [PATCH 323/378] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hans/ --- web/public/static/langs/zh_Hans.json | 52 +++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json index e26e7f14..d421bc35 100644 --- a/web/public/static/langs/zh_Hans.json +++ b/web/public/static/langs/zh_Hans.json @@ -203,17 +203,17 @@ "error_boundary_description": "这显然不应该发生。对此非常抱歉。
如果您有时间,请在GitHub上报告,或通过DiscordMatrix告诉我们。", "prefs_users_table": "用户表", "prefs_users_edit_button": "编辑用户", - "publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup", + "publish_dialog_tags_placeholder": "英文逗号分隔的标签列表,例如 warning, srv1-backup", "publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅文档。", "subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易被猜中的名字。订阅后,您可以使用 PUT/POST 通知。", "publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(仅限英语)", - "account_usage_basis_ip_description": "此帐户的使用统计信息和限制基于您的 IP 地址,因此可能会与其他用户共享。上面显示的限制是基于现有速率限制的近似值。", + "account_usage_basis_ip_description": "此账户的使用统计信息和限制基于您的 IP 地址,因此可能会与其他用户共享。上面显示的限制是基于现有速率限制的近似值。", "account_usage_cannot_create_portal_session": "无法打开计费门户", - "account_delete_title": "删除帐户", - "account_delete_description": "永久删除您的帐户", + "account_delete_title": "删除账户", + "account_delete_description": "永久删除您的账户", "signup_error_username_taken": "用户名 {{username}} 已被占用", - "signup_error_creation_limit_reached": "已达到帐户创建限制", - "login_title": "请登录你的 ntfy 帐户", + "signup_error_creation_limit_reached": "已达到账户创建限制", + "login_title": "请登录你的 ntfy 账户", "action_bar_change_display_name": "更改显示名称", "action_bar_reservation_add": "保留主题", "action_bar_reservation_delete": "移除保留", @@ -223,7 +223,7 @@ "action_bar_profile_logout": "登出", "action_bar_sign_in": "登录", "action_bar_sign_up": "注册", - "nav_button_account": "帐户", + "nav_button_account": "账户", "nav_upgrade_banner_label": "升级到 ntfy Pro", "nav_upgrade_banner_description": "保留主题,更多消息和邮件,以及更大的附件", "alert_not_supported_context_description": "通知仅支持 HTTPS。这是 Notifications API 的限制。", @@ -233,7 +233,7 @@ "reserve_dialog_checkbox_label": "保留主题并配置访问", "subscribe_dialog_subscribe_button_generate_topic_name": "生成名称", "account_basics_username_description": "嘿,那是你 ❤", - "account_basics_password_description": "更改您的帐户密码", + "account_basics_password_description": "更改您的账户密码", "account_basics_password_dialog_title": "更改密码", "account_basics_password_dialog_current_password_label": "当前密码", "account_basics_password_dialog_new_password_label": "新密码", @@ -244,8 +244,8 @@ "account_usage_of_limit": "{{limit}} 的", "account_usage_unlimited": "无限", "account_usage_limits_reset_daily": "使用限制每天午夜 (UTC) 重置", - "account_basics_tier_title": "帐户类型", - "account_basics_tier_description": "您帐户的权限级别", + "account_basics_tier_title": "账户类型", + "account_basics_tier_description": "您账户的权限级别", "account_basics_tier_admin": "管理员", "account_basics_tier_admin_suffix_with_tier": "(有 {{tier}} 等级)", "account_basics_tier_admin_suffix_no_tier": "(无等级)", @@ -258,7 +258,7 @@ "account_usage_messages_title": "已发布消息", "account_usage_emails_title": "已发送电子邮件", "account_usage_reservations_title": "保留主题", - "account_usage_reservations_none": "此帐户没有保留主题", + "account_usage_reservations_none": "此账户没有保留主题", "account_usage_attachment_storage_title": "附件存储", "account_usage_attachment_storage_description": "每个文件 {{filesize}},在 {{expiry}} 后删除", "account_upgrade_dialog_button_pay_now": "立即付款并订阅", @@ -276,7 +276,7 @@ "account_tokens_delete_dialog_title": "删除访问令牌", "account_tokens_delete_dialog_description": "在删除访问令牌之前,请确保没有应用程序或脚本正在活跃使用它。 此操作无法撤消。", "account_tokens_delete_dialog_submit_button": "永久删除令牌", - "prefs_users_description_no_sync": "用户和密码不会同步到您的帐户。", + "prefs_users_description_no_sync": "用户和密码不会同步到您的账户。", "prefs_users_table_cannot_delete_or_edit": "无法删除或编辑已登录用户", "prefs_reservations_title": "保留主题", "prefs_reservations_description": "您可以在此处保留主题名称供个人使用。保留主题使您拥有该主题的所有权,并允许您为其他用户定义对该主题的访问权限。", @@ -305,13 +305,13 @@ "reservation_delete_dialog_action_delete_title": "删除缓存的邮件和附件", "reservation_delete_dialog_action_delete_description": "缓存的邮件和附件将被永久删除。此操作无法撤消。", "reservation_delete_dialog_submit_button": "删除保留", - "account_delete_dialog_description": "这将永久删除您的帐户,包括存储在服务器上的所有数据。删除后,您的用户名将在 7 天内不可用。如果您真的想继续,请在下面的框中使用您的密码进行确认。", + "account_delete_dialog_description": "这将永久删除您的账户,包括存储在服务器上的所有数据。删除后,您的用户名将在 7 天内不可用。如果您真的想继续,请在下面的框中使用您的密码进行确认。", "account_delete_dialog_label": "密码", "account_delete_dialog_button_cancel": "取消", - "account_delete_dialog_button_submit": "永久删除帐户", - "account_delete_dialog_billing_warning": "删除您的帐户也会立即取消您的计费订阅。您将无法再访问计费仪表板。", - "account_upgrade_dialog_title": "更改帐户等级", - "account_upgrade_dialog_cancel_warning": "这将取消您的订阅,并在 {{date}} 降级您的帐户。在那一天,主题保留以及缓存在服务器上的消息将被删除。", + "account_delete_dialog_button_submit": "永久删除账户", + "account_delete_dialog_billing_warning": "删除您的账户也会立即取消您的计费订阅。您将无法再访问计费仪表板。", + "account_upgrade_dialog_title": "更改账户等级", + "account_upgrade_dialog_cancel_warning": "这将取消您的订阅,并在 {{date}} 降级您的账户。在那一天,主题保留以及缓存在服务器上的消息将被删除。", "account_upgrade_dialog_proration_info": "按比例分配:在付费计划之间升级时,差价将被立刻收取。在降级到较低级别时,余额将被用于支付未来的账单周期。", "account_upgrade_dialog_reservations_warning_one": "所选等级允许的保留主题少于当前等级。在更改您的等级之前,请至少删除 1 项保留。您可以在设置中删除保留。", "account_upgrade_dialog_reservations_warning_other": "所选等级允许的保留主题少于当前等级。在更改您的等级之前,请至少删除 {{count}} 项保留。您可以在设置中删除保留。", @@ -322,30 +322,30 @@ "signup_form_confirm_password": "确认密码", "signup_form_button_submit": "注册", "signup_form_toggle_password_visibility": "切换密码可见性", - "signup_title": "创建一个 ntfy 帐户", + "signup_title": "创建一个 ntfy 账户", "signup_form_username": "用户名", "signup_form_password": "密码", - "signup_already_have_account": "已有帐户?登录!", + "signup_already_have_account": "已有账户?登录!", "signup_disabled": "注册已禁用", "login_form_button_submit": "登录", "login_link_signup": "注册", "login_disabled": "登录已禁用", - "action_bar_account": "帐户", + "action_bar_account": "账户", "action_bar_reservation_edit": "更改保留", "subscribe_dialog_error_topic_already_reserved": "主题已保留", - "account_basics_title": "帐户", + "account_basics_title": "账户", "account_basics_username_title": "用户名", "account_basics_username_admin_tooltip": "你是管理员", "account_basics_password_title": "密码", - "account_basics_tier_payment_overdue": "您的付款已逾期。请更新您的付款方式,否则您的帐户将很快被降级。", - "account_basics_tier_canceled_subscription": "您的订阅已取消,并将在 {{date}} 降级为免费帐户。", + "account_basics_tier_payment_overdue": "您的付款已逾期。请更新您的付款方式,否则您的账户将很快被降级。", + "account_basics_tier_canceled_subscription": "您的订阅已取消,并将在 {{date}} 降级为免费账户。", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 总存储空间", "account_upgrade_dialog_tier_selected_label": "已选", "account_upgrade_dialog_tier_current_label": "当前", "account_upgrade_dialog_button_cancel": "取消", "account_upgrade_dialog_button_redirect_signup": "立即注册", "account_tokens_title": "访问令牌", - "account_tokens_description": "通过 ntfy API 发布和订阅时使用访问令牌,因此您不必发送您的帐户凭据。查看文档以了解更多信息。", + "account_tokens_description": "通过 ntfy API 发布和订阅时使用访问令牌,因此您不必发送您的账户凭据。查看文档以了解更多信息。", "account_tokens_table_token_header": "令牌", "account_tokens_table_label_header": "标签", "account_tokens_table_last_access_header": "最后访问", @@ -403,5 +403,7 @@ "web_push_subscription_expiring_title": "通知将被暂停", "web_push_subscription_expiring_body": "打开ntfy以继续接收通知", "web_push_unknown_notification_title": "接收到未知通知", - "web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy" + "web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy", + "account_basics_cannot_edit_or_delete_provisioned_user": "已设置的用户无法被编辑或删除", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "无法编辑或删除已设置的令牌" } From 9301546e6d5d15535f84c45e11fc7b1b1ecb4943 Mon Sep 17 00:00:00 2001 From: ezn24 Date: Wed, 31 Dec 2025 19:03:52 +0100 Subject: [PATCH 324/378] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hant/ --- web/public/static/langs/zh_Hant.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/zh_Hant.json b/web/public/static/langs/zh_Hant.json index 3aecd603..7f2cbdad 100644 --- a/web/public/static/langs/zh_Hant.json +++ b/web/public/static/langs/zh_Hant.json @@ -403,5 +403,7 @@ "web_push_subscription_expiring_body": "開啟ntfy以繼續接收通知", "web_push_subscription_expiring_title": "通知會被暫停", "web_push_unknown_notification_body": "你可能需要開啟網頁來更新ntfy", - "web_push_unknown_notification_title": "接收到不明通知" + "web_push_unknown_notification_title": "接收到不明通知", + "account_basics_cannot_edit_or_delete_provisioned_user": "已佈建的使用者無法編輯或刪除", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "無法編輯或刪除已佈建的權杖" } From 275487b3fdecca3f97970e98a924a37b324aa8b7 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 1 Jan 2026 21:49:26 -0500 Subject: [PATCH 325/378] Release notes --- docs/releases.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index 74f5c9ef..e6e27e4b 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1587,3 +1587,5 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco)) * Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel)) +* Fix crash in user add dialog (onAddUser) +* Fix ForegroundServiceDidNotStartInTimeException (attempt 2, see [#1520](https://github.com/binwiederhier/ntfy/issues/1520)) From 4f9f1292f194a567c86e6e04be8556feb92e644b Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 2 Jan 2026 07:35:06 -0500 Subject: [PATCH 326/378] Update privacy policy --- docs/privacy.md | 200 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 8 deletions(-) diff --git a/docs/privacy.md b/docs/privacy.md index f89f9aaa..bbf380b4 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -1,12 +1,196 @@ # Privacy policy -I love free software, and I'm doing this because it's fun. I have no bad intentions, and **I will -never monetize or sell your information, and this service and software will always stay free and open.** +**Last updated:** January 2, 2026 -Neither the server nor the app record any personal information, or share any of the messages and topics with -any outside service. All data is exclusively used to make the service function properly. The only external service -I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see -[FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version. +This privacy policy describes how ntfy ("we", "us", or "our") collects, uses, and handles your information +when you use the ntfy.sh service, web app, and mobile applications (Android and iOS). -For debugging purposes, the ntfy server may temporarily log request paths, remote IP addresses or even topics -or messages, though typically this is turned off. +## Our commitment to privacy + +We love free software, and we're doing this because it's fun. We have no bad intentions, and **we will +never monetize or sell your information**. The ntfy service and software will always stay free and open source. +If you don't trust us or your messages are sensitive, you can [self-host your own ntfy server](install.md). + +## Information we collect + +### Account information (optional) + +If you create an account on ntfy.sh, we collect: + +- **Username** - A unique identifier you choose +- **Password** - Stored as a secure bcrypt hash (we never store your plaintext password) +- **Email address** - Only if you subscribe to a paid plan (for billing purposes) +- **Phone number** - Only if you enable the phone call notification feature (verified via SMS/call) + +You can use ntfy without creating an account. Anonymous usage is fully supported. + +### Messages and notifications + +- **Message content** - Messages you publish are temporarily cached on our servers (default: 12 hours) to support + message polling and to overcome client network disruptions. Messages are deleted after the cache duration expires. +- **Attachments** - File attachments are temporarily stored (default: 3 hours) and then automatically deleted. +- **Topic names** - The topic names you publish to or subscribe to are processed by our servers. + +### Technical information + +- **IP addresses** - Used for rate limiting to prevent abuse. May be temporarily logged for debugging purposes, + though this is typically turned off. +- **Access tokens** - If you create access tokens, we store the token value, an optional label, last access time, + and the IP address of the last access. +- **Web push subscriptions** - If you enable browser notifications, we store your browser's push subscription + endpoint to deliver notifications. + +### Billing information (paid plans only) + +If you subscribe to a paid plan, payment processing is handled by Stripe. We store: + +- Stripe customer ID +- Subscription status and billing period + +We do not store your credit card numbers or payment details directly. These are handled entirely by Stripe. + +## Third-party services + +To provide the ntfy.sh service, we use the following third-party services: + +### Firebase Cloud Messaging (FCM) + +We use Google's Firebase Cloud Messaging to deliver push notifications to Android and iOS devices. When you +receive a notification through the mobile apps (Google Play or App Store versions): + +- Message metadata and content may be transmitted through Google's FCM infrastructure +- Google's [privacy policy](https://policies.google.com/privacy) applies to their handling of this data + +**To avoid FCM entirely:** Download the [F-Droid version](https://f-droid.org/en/packages/io.heckel.ntfy/) of +the Android app and use a self-hosted server, or use the instant delivery feature with your own server. + +### Twilio (phone calls) + +If you use the phone call notification feature (`X-Call` header), we use Twilio to: + +- Make voice calls to your verified phone number +- Send SMS or voice calls for phone number verification + +Your phone number is shared with Twilio to deliver these services. Twilio's +[privacy policy](https://www.twilio.com/legal/privacy) applies. + +### Amazon SES (email delivery) + +If you use the email notification feature (`X-Email` header), we use Amazon Simple Email Service (SES) to +deliver emails. The recipient email address and message content are transmitted through Amazon's infrastructure. +Amazon's [privacy policy](https://aws.amazon.com/privacy/) applies. + +### Stripe (payments) + +If you subscribe to a paid plan, payments are processed by Stripe. Your payment information is handled directly +by Stripe and is subject to Stripe's [privacy policy](https://stripe.com/privacy). + +Note: We have explicitly disabled Stripe's telemetry features in our integration. + +### Web push providers + +If you enable browser notifications in the ntfy web app, push messages are delivered through your browser +vendor's push service: + +- Google (Chrome) +- Mozilla (Firefox) +- Apple (Safari) +- Microsoft (Edge) + +Your browser's push subscription endpoint is shared with these providers to deliver notifications. + +## Mobile applications + +### Android app + +The Android app is available from two sources: + +- **Google Play Store** - Uses Firebase Cloud Messaging for push notifications. Firebase Analytics is + **explicitly disabled** in our app. +- **F-Droid** - Does not include any Google services or Firebase. Uses a foreground service to maintain + a direct connection to the server. + +The Android app stores the following data locally on your device: + +- Subscribed topics and their settings +- Cached notifications +- User credentials (if you add a server with authentication) +- Application logs (for debugging, stored locally only) + +### iOS app + +The iOS app uses Firebase Cloud Messaging (via Apple Push Notification service) to deliver notifications. +The app stores the following data locally on your device: + +- Subscribed topics +- Cached notifications +- User credentials (if configured) + +## Web application + +The ntfy web app is a static website that stores all data locally in your browser: + +- **IndexedDB** - Stores your subscriptions and cached notifications +- **Local Storage** - Stores your preferences and session information + +No cookies are used for tracking. The web app does not have a backend beyond the ntfy API. + +## Data retention + +| Data type | Retention period | +|-----------|------------------| +| Messages | 12 hours (configurable by server operators) | +| Attachments | 3 hours (configurable by server operators) | +| User accounts | Until you delete your account | +| Access tokens | Until you revoke them or delete your account | +| Phone numbers | Until you remove them or delete your account | +| Web push subscriptions | 60 days of inactivity, then automatically removed | +| Server logs | Varies; debugging logs are typically temporary | + +## Self-hosting + +If you prefer complete control over your data, you can [self-host your own ntfy server](install.md). +When self-hosting: + +- You control all data storage and retention +- You can choose whether to use Firebase, Twilio, email delivery, or any other integrations +- No data is shared with ntfy.sh or any third party (unless you configure those integrations) + +The server and all apps are fully open source: + +- Server: [github.com/binwiederhier/ntfy](https://github.com/binwiederhier/ntfy) +- Android app: [github.com/binwiederhier/ntfy-android](https://github.com/binwiederhier/ntfy-android) +- iOS app: [github.com/binwiederhier/ntfy-ios](https://github.com/binwiederhier/ntfy-ios) + +## Data security + +- All connections to ntfy.sh are encrypted using TLS/HTTPS +- Passwords are hashed using bcrypt before storage +- Access tokens are generated using cryptographically secure random values +- The server does not log message content by default + +## Your rights + +You have the right to: + +- **Access** - View your account information and data +- **Delete** - Delete your account and associated data via the web app +- **Export** - Your messages are available via the API while cached + +To delete your account, use the account settings in the web app or contact us. + +## Changes to this policy + +We may update this privacy policy from time to time. Changes will be posted on this page with an updated +"Last updated" date. For significant changes, we may provide additional notice on Discord/Matrix or through the [announcements](https://ntfy.sh/announcements) ntfy topic. + +## Contact + +If you have questions about this privacy policy or our data practices, you can reach us: + +- **GitHub Issues**: [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) +- **Discord**: [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) +- **Matrix**: [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) +- **Email**: [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh) + +For more information about ntfy, visit [ntfy.sh](https://ntfy.sh). From 4ce9508fd3bf35dc29482ffb585a03e25a5e0d03 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 2 Jan 2026 07:44:39 -0500 Subject: [PATCH 327/378] Privacy policy commit history --- docs/privacy.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/privacy.md b/docs/privacy.md index bbf380b4..e9edb462 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -137,15 +137,15 @@ No cookies are used for tracking. The web app does not have a backend beyond the ## Data retention -| Data type | Retention period | -|-----------|------------------| -| Messages | 12 hours (configurable by server operators) | -| Attachments | 3 hours (configurable by server operators) | -| User accounts | Until you delete your account | -| Access tokens | Until you revoke them or delete your account | -| Phone numbers | Until you remove them or delete your account | +| Data type | Retention period | +|------------------------|---------------------------------------------------| +| Messages | 12 hours (configurable by server operators) | +| Attachments | 3 hours (configurable by server operators) | +| User accounts | Until you delete your account | +| Access tokens | Until you revoke them or delete your account | +| Phone numbers | Until you remove them or delete your account | | Web push subscriptions | 60 days of inactivity, then automatically removed | -| Server logs | Varies; debugging logs are typically temporary | +| Server logs | Varies; debugging logs are typically temporary | ## Self-hosting @@ -182,7 +182,10 @@ To delete your account, use the account settings in the web app or contact us. ## Changes to this policy We may update this privacy policy from time to time. Changes will be posted on this page with an updated -"Last updated" date. For significant changes, we may provide additional notice on Discord/Matrix or through the [announcements](https://ntfy.sh/announcements) ntfy topic. +"Last updated" date. You may also review all changes in the [Git history](https://github.com/binwiederhier/ntfy/commits/main/docs/privacy.md). + +For significant changes, we may provide additional notice on Discord/Matrix or through the +[announcements](https://ntfy.sh/announcements) ntfy topic. ## Contact From eca1ed4d8d2fdb49622fc69edef8832017ea9b70 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 2 Jan 2026 07:59:02 -0500 Subject: [PATCH 328/378] Contact and contributing page --- SECURITY.md | 6 ++-- docs/config.md | 4 +-- docs/contact.md | 68 +++++++++++++++++++++++++++++++++++++++++ docs/contributing.md | 51 +++++++++++++++++++++++++++++++ docs/develop.md | 2 +- docs/faq.md | 10 +++--- docs/privacy.md | 9 ++---- docs/troubleshooting.md | 4 +-- mkdocs.yml | 2 ++ 9 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 docs/contact.md create mode 100644 docs/contributing.md diff --git a/SECURITY.md b/SECURITY.md index 45573756..a96cc823 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,5 +6,7 @@ As of today, I only support the latest version of ntfy. Please make sure you sta ## Reporting a Vulnerability -Please report severe security issues privately via ntfy@heckel.io, [Discord](https://discord.gg/cT7ECsZj9w), -or [Matrix](https://matrix.to/#/#ntfy:matrix.org) (my username is `binwiederhier`). +Please report security vulnerabilities privately via email to [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh). + +You can also reach me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org) +(my username is `binwiederhier`). diff --git a/docs/config.md b/docs/config.md index b04438e7..02418b19 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1013,7 +1013,7 @@ or the root domain: === "caddy" ``` # Note that this config is most certainly incomplete. Please help out and let me know what's missing - # via Discord/Matrix or in a GitHub issue. + # via the contact page (https://ntfy.sh/docs/contact/) or in a GitHub issue. # Note: Caddy automatically handles both HTTP and WebSockets with reverse_proxy ntfy.sh, http://nfty.sh { @@ -1034,7 +1034,7 @@ or the root domain: ``` kdl // /etc/ferron.kdl // Note that this config is most certainly incomplete. Please help out and let me know what's missing - // via Discord/Matrix or in a GitHub issue. + // via the contact page (https://ntfy.sh/docs/contact/) or in a GitHub issue. // Note: Ferron automatically handles both HTTP and WebSockets with proxy ntfy.sh { diff --git a/docs/contact.md b/docs/contact.md new file mode 100644 index 00000000..f511cee9 --- /dev/null +++ b/docs/contact.md @@ -0,0 +1,68 @@ +# Contact + +This service is run by **Philipp C. Heckel**. There are several ways to get in touch with me and the +ntfy community. Please choose the appropriate channel based on your needs. + +## Community support (free) + +For general questions, feature discussions, and community help, please use one of these public channels: + +| Channel | Link | Description | +|---------|------|-------------| +| **Discord** | [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) | Real-time chat with the community (I'm `binwiederhier`) | +| **Matrix** | [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) | Bridged from Discord, same community (I'm `binwiederhier`) | +| **Matrix Space** | [#ntfy-space:matrix.org](https://matrix.to/#/#ntfy-space:matrix.org) | Matrix space with all ntfy rooms | +| **Lemmy** | [discuss.ntfy.sh/c/ntfy](https://discuss.ntfy.sh/c/ntfy) | Forum-style discussions | +| **GitHub Issues** | [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) | Bug reports and feature requests | + +!!! info "Why public channels?" + Answering questions in public channels benefits the entire community. Other users can learn from the + discussion, and answers can be referenced later. This is much more scalable than 1-on-1 support. + +## Paid support (ntfy Pro subscribers) + +If you are subscribed to a [ntfy Pro](https://ntfy.sh/#pricing) plan, you are entitled to priority support +via the following channels: + +| Channel | Contact | Description | +|---------|---------|-------------| +| **Discord/Matrix** | Mention your Pro status | Priority responses in community channels | +| **Email** | [support@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Direct email support for Pro subscribers | + +Please include your ntfy.sh username when contacting support so we can verify your subscription status. + +## Specific inquiries + +### Privacy inquiries + +For questions about our [privacy policy](privacy.md), data handling, or to exercise your data rights +(access, deletion, etc.): + +- **Email**: [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh) + +### General inquiries + +For business inquiries, partnerships, press, or other general questions that don't fit the categories above: + +- **Email**: [contact@mail.ntfy.sh](mailto:contact@mail.ntfy.sh) + +### Security issues + +If you discover a security vulnerability, please report it responsibly: + +- **Email**: [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh) + +See also: [SECURITY.md](https://github.com/binwiederhier/ntfy/blob/main/SECURITY.md) + +## Announcements + +Stay up to date with ntfy news and releases: + +- **ntfy topic**: Subscribe to [ntfy.sh/announcements](https://ntfy.sh/announcements) for release announcements +- **GitHub Releases**: [github.com/binwiederhier/ntfy/releases](https://github.com/binwiederhier/ntfy/releases) +- **iOS TestFlight**: [Join TestFlight](https://testflight.apple.com/join/P1fFnAm9) for iOS beta testing + +## Contributing + +Want to contribute to ntfy? See the [contributing page](contributing.md) for details on how to help with +code, translations, documentation, or donations. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..d722f079 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,51 @@ +# Contributing + +Thank you for your interest in contributing to ntfy! There are many ways to help, whether you're a developer, +translator, or just an enthusiastic user. + +## Code contributions + +If you'd like to contribute code to ntfy: + +1. Check out the [development guide](develop.md) to set up your environment +2. Look at [open issues](https://github.com/binwiederhier/ntfy/issues) for ideas, or propose your own +3. For larger features or architectural changes, please reach out on [Discord/Matrix](contact.md) first to discuss + before investing significant time +4. Submit a pull request on GitHub + +All contributions are welcome, from small bug fixes to major features. + +## Translations + +Help make ntfy accessible to users around the world! We use Hosted Weblate for translations: + +- **Weblate**: [hosted.weblate.org/projects/ntfy](https://hosted.weblate.org/projects/ntfy/) + +You can start translating immediately without any coding knowledge. + +## Documentation + +Found a typo? Want to improve the docs? Documentation contributions are very welcome: + +- Edit any page directly on GitHub using the edit button +- Submit a pull request with your improvements + +## Bug reports and feature requests + +- **GitHub Issues**: [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) + +Please search existing issues before creating a new one to avoid duplicates. + +## Donations + +If you'd like to support ntfy financially, donations are gratefully accepted: + +- **GitHub Sponsors**: [github.com/sponsors/binwiederhier](https://github.com/sponsors/binwiederhier) + +Your support helps cover server costs and development time. Even small donations are very much appreciated! + +## Code of Conduct + +Please be respectful and constructive in all interactions. See the +[Code of Conduct](https://github.com/binwiederhier/ntfy/blob/main/CODE_OF_CONDUCT.md) for details. + diff --git a/docs/develop.md b/docs/develop.md index 43ac2d4f..4ddff5ec 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -2,7 +2,7 @@ Hurray 🥳 🎉, you are interested in writing code for ntfy! **That's awesome.** 😎 I tried my very best to write up detailed instructions, but if at any point in time you run into issues, don't -hesitate to **contact me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)**. +hesitate to reach out via one of the channels listed on the [contact page](contact.md). ## ntfy server The ntfy server source code is available [on GitHub](https://github.com/binwiederhier/ntfy). The codebase for the diff --git a/docs/faq.md b/docs/faq.md index 6ff97cfe..5fa5252c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -94,11 +94,11 @@ I would be humbled if you helped me carry the server and developer account costs appreciated. ## Can I email you? Can I DM you on Discord/Matrix? -While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org), -[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally -**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a -[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities. +For community support, please use the public channels listed on the [contact page](contact.md). I generally +**do not respond to direct messages** about ntfy, unless you are paying for a [ntfy Pro](https://ntfy.sh/#pricing) +plan (see [paid support](contact.md#paid-support-ntfy-pro-subscribers)), or you are inquiring about business +opportunities (see [general inquiries](contact.md#general-inquiries)). I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions -in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users +in public forums benefits others, since I can link to the discussion at a later point in time, or other users may be able to help out. I hope you understand. diff --git a/docs/privacy.md b/docs/privacy.md index e9edb462..5572b3f3 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -189,11 +189,6 @@ For significant changes, we may provide additional notice on Discord/Matrix or t ## Contact -If you have questions about this privacy policy or our data practices, you can reach us: +For privacy-related inquiries, please email [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh). -- **GitHub Issues**: [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) -- **Discord**: [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) -- **Matrix**: [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) -- **Email**: [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh) - -For more information about ntfy, visit [ntfy.sh](https://ntfy.sh). +For all other contact options, see the [contact page](contact.md). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d37561c5..3d090306 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,7 +1,7 @@ # Troubleshooting This page lists a few suggestions of what to do when things don't work as expected. This is not a complete list. -If this page does not help, feel free to drop by the [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org) -and ask there. We're happy to help. +If this page does not help, feel free to reach out via one of the channels listed on the [contact page](contact.md). +We're happy to help. ## ntfy server If you host your own ntfy server, and you're having issues with any component, it is always helpful to enable debugging/tracing diff --git a/mkdocs.yml b/mkdocs.yml index adaf166b..1e15b5db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,6 +99,8 @@ nav: - "Known issues": known-issues.md - "Deprecation notices": deprecations.md - "Development": develop.md + - "Contributing": contributing.md - "Privacy policy": privacy.md + - "Contact": contact.md From 2cd444ece0807cd2f102458a82bdb7094d0e909d Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 2 Jan 2026 08:05:39 -0500 Subject: [PATCH 329/378] Contact page --- docs/contact.md | 51 +++++++++++++++++--------------------------- docs/contributing.md | 8 ------- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/docs/contact.md b/docs/contact.md index f511cee9..3e0c0be7 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -1,58 +1,47 @@ # Contact -This service is run by **Philipp C. Heckel**. There are several ways to get in touch with me and the +This service is run by [Philipp C. Heckel](https://heckel.io). There are several ways to get in touch with me and the ntfy community. Please choose the appropriate channel based on your needs. ## Community support (free) For general questions, feature discussions, and community help, please use one of these public channels: -| Channel | Link | Description | -|---------|------|-------------| -| **Discord** | [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) | Real-time chat with the community (I'm `binwiederhier`) | -| **Matrix** | [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) | Bridged from Discord, same community (I'm `binwiederhier`) | -| **Matrix Space** | [#ntfy-space:matrix.org](https://matrix.to/#/#ntfy-space:matrix.org) | Matrix space with all ntfy rooms | -| **Lemmy** | [discuss.ntfy.sh/c/ntfy](https://discuss.ntfy.sh/c/ntfy) | Forum-style discussions | -| **GitHub Issues** | [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) | Bug reports and feature requests | +| Channel | Link | Description | +|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------| +| **Discord** | [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) | Real-time chat with the community (I'm `binwiederhier`) | +| **Matrix** | [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) | Bridged from Discord, same community (I'm `binwiederhier`) | +| **Matrix Space** | [#ntfy-space:matrix.org](https://matrix.to/#/#ntfy-space:matrix.org) | Matrix space with all ntfy rooms | +| **GitHub Issues** | [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) | Bug reports and feature requests | !!! info "Why public channels?" Answering questions in public channels benefits the entire community. Other users can learn from the discussion, and answers can be referenced later. This is much more scalable than 1-on-1 support. -## Paid support (ntfy Pro subscribers) +## Paid support If you are subscribed to a [ntfy Pro](https://ntfy.sh/#pricing) plan, you are entitled to priority support via the following channels: -| Channel | Contact | Description | -|---------|---------|-------------| -| **Discord/Matrix** | Mention your Pro status | Priority responses in community channels | -| **Email** | [support@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Direct email support for Pro subscribers | +| Channel | Contact | Description | +|-----------------------|-----------------------------------------------------|------------------------------------------| +| **General Support** | [support@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Direct email support for Pro subscribers | +| **Billing Inquiries** | [billing@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Inquire about billing issues | +| **Discord/Matrix** | Mention your Pro status | Priority responses in community channels | Please include your ntfy.sh username when contacting support so we can verify your subscription status. -## Specific inquiries - -### Privacy inquiries - -For questions about our [privacy policy](privacy.md), data handling, or to exercise your data rights -(access, deletion, etc.): - -- **Email**: [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh) - -### General inquiries - -For business inquiries, partnerships, press, or other general questions that don't fit the categories above: - -- **Email**: [contact@mail.ntfy.sh](mailto:contact@mail.ntfy.sh) - ### Security issues -If you discover a security vulnerability, please report it responsibly: +If you discover a security vulnerability, please report it responsibly via [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh). See also: [SECURITY.md](https://github.com/binwiederhier/ntfy/blob/main/SECURITY.md). -- **Email**: [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh) +## Other inquiries -See also: [SECURITY.md](https://github.com/binwiederhier/ntfy/blob/main/SECURITY.md) +For questions about our [privacy policy](privacy.md), data handling, or to exercise your data rights +(access, deletion, etc.), please email [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh). + +For business inquiries, partnerships, press, or other general questions that don't fit the categories above, please +use [contact@mail.ntfy.sh](mailto:contact@mail.ntfy.sh). ## Announcements diff --git a/docs/contributing.md b/docs/contributing.md index d722f079..620ae257 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -36,14 +36,6 @@ Found a typo? Want to improve the docs? Documentation contributions are very wel Please search existing issues before creating a new one to avoid duplicates. -## Donations - -If you'd like to support ntfy financially, donations are gratefully accepted: - -- **GitHub Sponsors**: [github.com/sponsors/binwiederhier](https://github.com/sponsors/binwiederhier) - -Your support helps cover server costs and development time. Even small donations are very much appreciated! - ## Code of Conduct Please be respectful and constructive in all interactions. See the From 3575abcde364a63822701fd709425d1f95cc2e0f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 2 Jan 2026 08:10:34 -0500 Subject: [PATCH 330/378] Restructure contact page --- docs/contact.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/contact.md b/docs/contact.md index 3e0c0be7..2be59cb2 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -3,7 +3,9 @@ This service is run by [Philipp C. Heckel](https://heckel.io). There are several ways to get in touch with me and the ntfy community. Please choose the appropriate channel based on your needs. -## Community support (free) +## Support + +### Community support For general questions, feature discussions, and community help, please use one of these public channels: @@ -18,7 +20,7 @@ For general questions, feature discussions, and community help, please use one o Answering questions in public channels benefits the entire community. Other users can learn from the discussion, and answers can be referenced later. This is much more scalable than 1-on-1 support. -## Paid support +### Paid support If you are subscribed to a [ntfy Pro](https://ntfy.sh/#pricing) plan, you are entitled to priority support via the following channels: @@ -31,7 +33,7 @@ via the following channels: Please include your ntfy.sh username when contacting support so we can verify your subscription status. -### Security issues +## Security issues If you discover a security vulnerability, please report it responsibly via [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh). See also: [SECURITY.md](https://github.com/binwiederhier/ntfy/blob/main/SECURITY.md). @@ -42,16 +44,3 @@ For questions about our [privacy policy](privacy.md), data handling, or to exerc For business inquiries, partnerships, press, or other general questions that don't fit the categories above, please use [contact@mail.ntfy.sh](mailto:contact@mail.ntfy.sh). - -## Announcements - -Stay up to date with ntfy news and releases: - -- **ntfy topic**: Subscribe to [ntfy.sh/announcements](https://ntfy.sh/announcements) for release announcements -- **GitHub Releases**: [github.com/binwiederhier/ntfy/releases](https://github.com/binwiederhier/ntfy/releases) -- **iOS TestFlight**: [Join TestFlight](https://testflight.apple.com/join/P1fFnAm9) for iOS beta testing - -## Contributing - -Want to contribute to ntfy? See the [contributing page](contributing.md) for details on how to help with -code, translations, documentation, or donations. From 20ccee5d7d8660eced3dc84b9e3020f4a43ca0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m=2E?= Date: Sat, 3 Jan 2026 15:48:02 +0100 Subject: [PATCH 331/378] Translated using Weblate (Galician) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/ --- web/public/static/langs/gl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json index 2fa8c32d..d69da79c 100644 --- a/web/public/static/langs/gl.json +++ b/web/public/static/langs/gl.json @@ -406,5 +406,7 @@ "web_push_subscription_expiring_body": "Abrir ntfy para seguir recibindo notificacións", "web_push_unknown_notification_title": "Recibida unha notificación descoñecida desde o servidor", "web_push_unknown_notification_body": "Poderías ter que actualizar ntfy abrindo a app web", - "subscribe_dialog_subscribe_use_another_background_info": "As notificacións procedentes doutros servidores non se van recibir cando a app web estea pechada" + "subscribe_dialog_subscribe_use_another_background_info": "As notificacións procedentes doutros servidores non se van recibir cando a app web estea pechada", + "account_basics_cannot_edit_or_delete_provisioned_user": "Unha usuaria predefinida non se pode editar ou eliminar", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Non se pode editar un token de usuaria predefinida" } From e18d64933fdfad2710ad8a930e1f880c2714cb52 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 4 Jan 2026 10:57:11 -0500 Subject: [PATCH 332/378] Release notes --- docs/releases.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index e6e27e4b..afcd849a 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1572,7 +1572,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy Android app v1.21.x +### ntfy Android app v1.21.1-rc1 (IN TESTING) **Features:** @@ -1589,3 +1589,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel)) * Fix crash in user add dialog (onAddUser) * Fix ForegroundServiceDidNotStartInTimeException (attempt 2, see [#1520](https://github.com/binwiederhier/ntfy/issues/1520)) +* Hide "Exact alarms" setting if battery optimization exemption has been granted ([#1456](https://github.com/binwiederhier/ntfy/issues/1456), thanks for reporting [@HappyLer](https://github.com/HappyLer)) From 39936a95f87e388c30b030f046706ad84bd77bc8 Mon Sep 17 00:00:00 2001 From: Pixelguin <11445611+Pixelguin@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:11:20 -0800 Subject: [PATCH 333/378] Add troubleshooting steps for "Reconnecting" error on mobile --- docs/troubleshooting.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3d090306..e093647e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -129,3 +129,14 @@ keyboard. ## iOS app Sorry, there is no way to debug or get the logs from the iOS app (yet), outside of running the app in Xcode. + +## Other + +### "Reconnecting..." / Late notifications on mobile (self-hosted) + +If all of your topics are showing as "Reconnecting" and notifications are taking a long time (30+ minutes) to come in, or if you're only getting new pushes with a manual refresh, double-check your configuration: + +* If ntfy is behind a reverse proxy, make sure `behind_proxy` is enabled. +* If ntfy is behind Nginx, make sure WebSockets are enabled. +* Make sure you have granted permission to access all of your topics, either to a logged-in user account or to `everyone`. All subscribed topics are joined into a single WebSocket/JSON, so a single topic that receives `403 Forbidden` will prevent the entire request from going through. + * In particular, double-check that your user has permission to read `up*` if you are using UnifiedPush. From f356309f705db10977a1f4f222c4183c5a05a42c Mon Sep 17 00:00:00 2001 From: Pixelguin <11445611+Pixelguin@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:13:53 -0800 Subject: [PATCH 334/378] Clarify up* r/w permissions --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e093647e..4b45a939 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -139,4 +139,4 @@ If all of your topics are showing as "Reconnecting" and notifications are taking * If ntfy is behind a reverse proxy, make sure `behind_proxy` is enabled. * If ntfy is behind Nginx, make sure WebSockets are enabled. * Make sure you have granted permission to access all of your topics, either to a logged-in user account or to `everyone`. All subscribed topics are joined into a single WebSocket/JSON, so a single topic that receives `403 Forbidden` will prevent the entire request from going through. - * In particular, double-check that your user has permission to read `up*` if you are using UnifiedPush. + * In particular, double-check that `everyone` has permission to write to `up*` and your user has permission to read `up*` if you are using UnifiedPush. From 1c32ee76130e358403de838898daa9f31e4863e9 Mon Sep 17 00:00:00 2001 From: Pixelguin <11445611+Pixelguin@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:36:56 -0800 Subject: [PATCH 335/378] Clarify wording --- docs/troubleshooting.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4b45a939..7cdd4cda 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -136,7 +136,8 @@ Sorry, there is no way to debug or get the logs from the iOS app (yet), outside If all of your topics are showing as "Reconnecting" and notifications are taking a long time (30+ minutes) to come in, or if you're only getting new pushes with a manual refresh, double-check your configuration: -* If ntfy is behind a reverse proxy, make sure `behind_proxy` is enabled. -* If ntfy is behind Nginx, make sure WebSockets are enabled. -* Make sure you have granted permission to access all of your topics, either to a logged-in user account or to `everyone`. All subscribed topics are joined into a single WebSocket/JSON, so a single topic that receives `403 Forbidden` will prevent the entire request from going through. +* If ntfy is behind a reverse proxy (such as Nginx): + * Make sure `behind_proxy` is enabled in ntfy's config. + * Make sure WebSockets are enabled in the reverse proxy config. +* Make sure you have granted permission to access all of your topics, either to a logged-in user account or to `everyone`. All subscribed topics are joined into a single WebSocket/JSON request, so a single topic that receives `403 Forbidden` will prevent the entire request from going through. * In particular, double-check that `everyone` has permission to write to `up*` and your user has permission to read `up*` if you are using UnifiedPush. From aca9a774984789ca5b5e83cddd79cf42bc0e0e1f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 5 Jan 2026 21:14:29 -0500 Subject: [PATCH 336/378] Remove mtime --- server/message_cache.go | 37 ++++++++++--------------- server/message_cache_test.go | 22 ++------------- server/server.go | 6 ++-- server/types.go | 27 ++++++++++-------- web/public/static/langs/en.json | 3 +- web/public/sw.js | 3 -- web/src/app/SubscriptionManager.js | 41 +++++++++++++++------------- web/src/app/db.js | 4 +-- web/src/app/notificationUtils.js | 2 +- web/src/components/Notifications.jsx | 19 ++++--------- 10 files changed, 67 insertions(+), 97 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index 58080979..bd4dddd4 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -31,7 +31,6 @@ const ( mid TEXT NOT NULL, sid TEXT NOT NULL, time INT NOT NULL, - mtime INT NOT NULL, expires INT NOT NULL, topic TEXT NOT NULL, message TEXT NOT NULL, @@ -57,7 +56,6 @@ const ( CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); CREATE INDEX IF NOT EXISTS idx_time ON messages (time); - CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires); CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender); @@ -71,53 +69,53 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? AND published = 1 - ORDER BY mtime, id + ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? - ORDER BY mtime, id + ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND id > ? AND published = 1 - ORDER BY mtime, id + ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND (id > ? OR published = 0) - ORDER BY mtime, id + ORDER BY time, id ` selectMessagesLatestQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 ` selectMessagesDueQuery = ` - SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE time <= ? AND published = 0 - ORDER BY mtime, id + ORDER BY time, id ` selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1` updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` @@ -270,10 +268,8 @@ const ( //13 -> 14 migrate13To14AlterMessagesTableQuery = ` ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT(''); - ALTER TABLE messages ADD COLUMN mtime INT NOT NULL DEFAULT('0'); ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0'); CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); - CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime); ` ) @@ -415,7 +411,6 @@ func (c *messageCache) addMessages(ms []*message) error { m.ID, m.SID, m.Time, - m.MTime, m.Expires, m.Topic, m.Message, @@ -723,14 +718,13 @@ func readMessages(rows *sql.Rows) ([]*message, error) { } func readMessage(rows *sql.Rows) (*message, error) { - var timestamp, mtimestamp, expires, attachmentSize, attachmentExpires int64 + var timestamp, expires, attachmentSize, attachmentExpires int64 var priority, deleted int var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string err := rows.Scan( &id, &sid, ×tamp, - &mtimestamp, &expires, &topic, &msg, @@ -782,7 +776,6 @@ func readMessage(rows *sql.Rows) (*message, error) { ID: id, SID: sid, Time: timestamp, - MTime: mtimestamp, Expires: expires, Event: messageEvent, Topic: topic, diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 18f69fd3..64203136 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -24,11 +24,9 @@ func TestMemCache_Messages(t *testing.T) { func testCacheMessages(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "my message") m1.Time = 1 - m1.MTime = 1000 m2 := newDefaultMessage("mytopic", "my other message") m2.Time = 2 - m2.MTime = 2000 require.Nil(t, c.AddMessage(m1)) require.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message"))) @@ -126,13 +124,10 @@ func testCacheMessagesScheduled(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "message 1") m2 := newDefaultMessage("mytopic", "message 2") m2.Time = time.Now().Add(time.Hour).Unix() - m2.MTime = time.Now().Add(time.Hour).UnixMilli() m3 := newDefaultMessage("mytopic", "message 3") - m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2! - m3.MTime = time.Now().Add(time.Minute).UnixMilli() // earlier than m2! + m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2! m4 := newDefaultMessage("mytopic2", "message 4") m4.Time = time.Now().Add(time.Minute).Unix() - m4.MTime = time.Now().Add(time.Minute).UnixMilli() require.Nil(t, c.AddMessage(m1)) require.Nil(t, c.AddMessage(m2)) require.Nil(t, c.AddMessage(m3)) @@ -206,25 +201,18 @@ func TestMemCache_MessagesSinceID(t *testing.T) { func testCacheMessagesSinceID(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "message 1") m1.Time = 100 - m1.MTime = 100000 m2 := newDefaultMessage("mytopic", "message 2") m2.Time = 200 - m2.MTime = 200000 m3 := newDefaultMessage("mytopic", "message 3") - m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5 - m3.MTime = time.Now().Add(time.Hour).UnixMilli() // Scheduled, in the future, later than m7 and m5 + m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5 m4 := newDefaultMessage("mytopic", "message 4") m4.Time = 400 - m4.MTime = 400000 m5 := newDefaultMessage("mytopic", "message 5") - m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7 - m5.MTime = time.Now().Add(time.Minute).UnixMilli() // Scheduled, in the future, later than m7 + m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7 m6 := newDefaultMessage("mytopic", "message 6") m6.Time = 600 - m6.MTime = 600000 m7 := newDefaultMessage("mytopic", "message 7") m7.Time = 700 - m7.MTime = 700000 require.Nil(t, c.AddMessage(m1)) require.Nil(t, c.AddMessage(m2)) @@ -285,17 +273,14 @@ func testCachePrune(t *testing.T, c *messageCache) { m1 := newDefaultMessage("mytopic", "my message") m1.Time = now - 10 - m1.MTime = (now - 10) * 1000 m1.Expires = now - 5 m2 := newDefaultMessage("mytopic", "my other message") m2.Time = now - 5 - m2.MTime = (now - 5) * 1000 m2.Expires = now + 5 // In the future m3 := newDefaultMessage("another_topic", "and another one") m3.Time = now - 12 - m3.MTime = (now - 12) * 1000 m3.Expires = now - 2 require.Nil(t, c.AddMessage(m1)) @@ -546,7 +531,6 @@ func TestSqliteCache_Migration_From1(t *testing.T) { // Add delayed message delayedMessage := newDefaultMessage("mytopic", "some delayed message") delayedMessage.Time = time.Now().Add(time.Minute).Unix() - delayedMessage.MTime = time.Now().Add(time.Minute).UnixMilli() require.Nil(t, c.AddMessage(delayedMessage)) // 10, not 11! diff --git a/server/server.go b/server/server.go index 39c08c7d..9c612ebd 100644 --- a/server/server.go +++ b/server/server.go @@ -874,7 +874,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } minc(metricMessagesPublishedSuccess) - return s.writeJSON(w, m) + return s.writeJSON(w, m.forJSON()) } func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error { @@ -1291,7 +1291,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(&msg); err != nil { + if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { return "", err } return buf.String(), nil @@ -1302,7 +1302,7 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v * func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(&msg); err != nil { + if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { return "", err } if msg.Event != messageEvent { diff --git a/server/types.go b/server/types.go index c8376673..467e80f5 100644 --- a/server/types.go +++ b/server/types.go @@ -24,10 +24,9 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - SID string `json:"sid"` // Message sequence ID for updating message contents - Time int64 `json:"time"` // Unix time in seconds - MTime int64 `json:"mtime"` // Unix time in milliseconds + ID string `json:"id"` // Random message ID + SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) + Time int64 `json:"time"` // Unix time in seconds Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) Event string `json:"event"` // One of the above Topic string `json:"topic"` @@ -53,7 +52,6 @@ func (m *message) Context() log.Context { "message_id": m.ID, "message_sid": m.SID, "message_time": m.Time, - "message_mtime": m.MTime, "message_event": m.Event, "message_body_size": len(m.Message), } @@ -66,6 +64,16 @@ func (m *message) Context() log.Context { return fields } +// forJSON returns a copy of the message prepared for JSON output. +// It clears SID if it equals ID (to avoid redundant output). +func (m *message) forJSON() *message { + msg := *m + if msg.SID == msg.ID { + msg.SID = "" // Will be omitted due to omitempty + } + return &msg +} + type attachment struct { Name string `json:"name"` Type string `json:"type,omitempty"` @@ -123,7 +131,6 @@ func newMessage(event, topic, msg string) *message { return &message{ ID: util.RandomString(messageIDLength), Time: time.Now().Unix(), - MTime: time.Now().UnixMilli(), Event: event, Topic: topic, Message: msg, @@ -162,11 +169,7 @@ type sinceMarker struct { } func newSinceTime(timestamp int64) sinceMarker { - return newSinceMTime(timestamp * 1000) -} - -func newSinceMTime(mtimestamp int64) sinceMarker { - return sinceMarker{time.UnixMilli(mtimestamp), ""} + return sinceMarker{time.Unix(timestamp, 0), ""} } func newSinceID(id string) sinceMarker { @@ -557,7 +560,7 @@ func newWebPushPayload(subscriptionID string, message *message) *webPushPayload return &webPushPayload{ Event: webPushMessageEvent, SubscriptionID: subscriptionID, - Message: message, + Message: message.forJSON(), } } diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 2094f0c2..0895b2eb 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -70,8 +70,7 @@ "notifications_delete": "Delete", "notifications_copied_to_clipboard": "Copied to clipboard", "notifications_tags": "Tags", - "notifications_sid": "Sequence ID", - "notifications_revisions": "Revisions", + "notifications_modified": "modified {{date}}", "notifications_priority_x": "Priority {{priority}}", "notifications_new_indicator": "New notification", "notifications_attachment_image": "Attachment image", diff --git a/web/public/sw.js b/web/public/sw.js index 471cbee2..e010e4d4 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -25,9 +25,6 @@ const addNotification = async ({ subscriptionId, message }) => { const db = await dbAsync(); const populatedMessage = message; - if (!("mtime" in populatedMessage)) { - populatedMessage.mtime = message.time * 1000; - } if (!("sid" in populatedMessage)) { populatedMessage.sid = message.id; } diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 00d15d89..086fc048 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -157,7 +157,7 @@ class SubscriptionManager { // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach const notifications = await this.db.notifications - .orderBy("mtime") // Sort by time first + .orderBy("time") // Sort by time .filter((n) => n.subscriptionId === subscriptionId) .reverse() .toArray(); @@ -167,30 +167,39 @@ class SubscriptionManager { async getAllNotifications() { const notifications = await this.db.notifications - .orderBy("mtime") // Efficient, see docs + .orderBy("time") // Efficient, see docs .reverse() .toArray(); return this.groupNotificationsBySID(notifications); } - // Collapse notification updates based on sids + // Collapse notification updates based on sids, keeping only the latest version + // Also tracks the original time (earliest) for each sequence groupNotificationsBySID(notifications) { - const results = {}; + const latestBySid = {}; + const originalTimeBySid = {}; + notifications.forEach((notification) => { const key = `${notification.subscriptionId}:${notification.sid}`; - if (key in results) { - if ("history" in results[key]) { - results[key].history.push(notification); - } else { - results[key].history = [notification]; - } - } else { - results[key] = notification; + + // Track the latest notification for each sid (first one since sorted DESC) + if (!(key in latestBySid)) { + latestBySid[key] = notification; + } + + // Track the original (earliest) time for each sid + const currentOriginal = originalTimeBySid[key]; + if (currentOriginal === undefined || notification.time < currentOriginal) { + originalTimeBySid[key] = notification.time; } }); - return Object.values(results); + // Return latest notifications with originalTime set + return Object.entries(latestBySid).map(([key, notification]) => ({ + ...notification, + originalTime: originalTimeBySid[key], + })); } /** Adds notification, or returns false if it already exists */ @@ -201,9 +210,6 @@ class SubscriptionManager { } try { const populatedNotification = notification; - if (!("mtime" in populatedNotification)) { - populatedNotification.mtime = notification.time * 1000; - } if (!("sid" in populatedNotification)) { populatedNotification.sid = notification.id; } @@ -227,9 +233,6 @@ class SubscriptionManager { async addNotifications(subscriptionId, notifications) { const notificationsWithSubscriptionId = notifications.map((notification) => { const populatedNotification = notification; - if (!("mtime" in populatedNotification)) { - populatedNotification.mtime = notification.time * 1000; - } if (!("sid" in populatedNotification)) { populatedNotification.sid = notification.id; } diff --git a/web/src/app/db.js b/web/src/app/db.js index f4d36c1b..f11a5d0b 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -11,9 +11,9 @@ const createDatabase = (username) => { const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user const db = new Dexie(dbName); - db.version(3).stores({ + db.version(4).stores({ subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,sid,subscriptionId,time,mtime,new,[subscriptionId+new]", // compound key for query performance + notifications: "&id,sid,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance users: "&baseUrl,username", prefs: "&key", }); diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 2884e2f3..55d398c9 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -69,7 +69,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to badge, icon, image, - timestamp: message.mtime, + timestamp: message.time * 1000, tag, renotify: true, silent: false, diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 6872f46e..343a284a 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -236,7 +236,9 @@ const NotificationItem = (props) => { const { t, i18n } = useTranslation(); const { notification } = props; const { attachment } = notification; - const date = formatShortDateTime(notification.time, i18n.language); + const isModified = notification.originalTime && notification.originalTime !== notification.time; + const originalDate = formatShortDateTime(notification.originalTime || notification.time, i18n.language); + const modifiedDate = isModified ? formatShortDateTime(notification.time, i18n.language) : null; const otherTags = unmatchedTags(notification.tags); const tags = otherTags.length > 0 ? otherTags.join(", ") : null; const handleDelete = async () => { @@ -267,8 +269,6 @@ const NotificationItem = (props) => { const hasUserActions = notification.actions && notification.actions.length > 0; const showActions = hasAttachmentActions || hasClickAction || hasUserActions; - const showSid = notification.id !== notification.sid || notification.history; - return ( @@ -289,7 +289,8 @@ const NotificationItem = (props) => { )} - {date} + {originalDate} + {modifiedDate && ` (${t("notifications_modified", { date: modifiedDate })})`} {[1, 2, 4, 5].includes(notification.priority) && ( { {t("notifications_tags")}: {tags} )} - {showSid && ( - - {t("notifications_sid")}: {notification.sid} - - )} - {notification.history && ( - - {t("notifications_revisions")}: {notification.history.length + 1} - - )} {showActions && ( From f51e99dc803ced7399730fd2560c3db00a979a57 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 5 Jan 2026 21:55:07 -0500 Subject: [PATCH 337/378] Remove modified --- web/public/static/langs/en.json | 1 - web/src/app/SubscriptionManager.js | 19 ++----------------- web/src/components/Notifications.jsx | 7 ++----- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 0895b2eb..b0d3c545 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -70,7 +70,6 @@ "notifications_delete": "Delete", "notifications_copied_to_clipboard": "Copied to clipboard", "notifications_tags": "Tags", - "notifications_modified": "modified {{date}}", "notifications_priority_x": "Priority {{priority}}", "notifications_new_indicator": "New notification", "notifications_attachment_image": "Attachment image", diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 086fc048..ccce5ccc 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -175,31 +175,16 @@ class SubscriptionManager { } // Collapse notification updates based on sids, keeping only the latest version - // Also tracks the original time (earliest) for each sequence groupNotificationsBySID(notifications) { const latestBySid = {}; - const originalTimeBySid = {}; - notifications.forEach((notification) => { const key = `${notification.subscriptionId}:${notification.sid}`; - - // Track the latest notification for each sid (first one since sorted DESC) + // Keep only the first (latest by time) notification for each sid if (!(key in latestBySid)) { latestBySid[key] = notification; } - - // Track the original (earliest) time for each sid - const currentOriginal = originalTimeBySid[key]; - if (currentOriginal === undefined || notification.time < currentOriginal) { - originalTimeBySid[key] = notification.time; - } }); - - // Return latest notifications with originalTime set - return Object.entries(latestBySid).map(([key, notification]) => ({ - ...notification, - originalTime: originalTimeBySid[key], - })); + return Object.values(latestBySid); } /** Adds notification, or returns false if it already exists */ diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 343a284a..94185b7c 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -236,9 +236,7 @@ const NotificationItem = (props) => { const { t, i18n } = useTranslation(); const { notification } = props; const { attachment } = notification; - const isModified = notification.originalTime && notification.originalTime !== notification.time; - const originalDate = formatShortDateTime(notification.originalTime || notification.time, i18n.language); - const modifiedDate = isModified ? formatShortDateTime(notification.time, i18n.language) : null; + const date = formatShortDateTime(notification.time, i18n.language); const otherTags = unmatchedTags(notification.tags); const tags = otherTags.length > 0 ? otherTags.join(", ") : null; const handleDelete = async () => { @@ -289,8 +287,7 @@ const NotificationItem = (props) => { )} - {originalDate} - {modifiedDate && ` (${t("notifications_modified", { date: modifiedDate })})`} + {date} {[1, 2, 4, 5].includes(notification.priority) && ( Date: Mon, 5 Jan 2026 06:28:56 +0100 Subject: [PATCH 338/378] Translated using Weblate (Macedonian) Currently translated at 13.5% (55 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index b1caef44..b407c512 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -50,5 +50,8 @@ "nav_topics_title": "Претплатени теми", "nav_button_all_notifications": "Сите нотификации", "nav_button_publish_message": "Објави нотификација", - "nav_button_subscribe": "Претплати се на тема" + "nav_button_subscribe": "Претплати се на тема", + "action_bar_unmute_notifications": "Одглуши ги нотификациите", + "action_bar_toggle_mute": "Заглуши/Загуши ги нотификациите", + "message_bar_publish": "Објави порака" } From 2856793effecc7c2038e259c8c26b9348edced70 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 6 Jan 2026 14:22:55 -0500 Subject: [PATCH 339/378] Deleted --- server/message_cache.go | 7 +++-- server/server.go | 48 ++++++++++++++++++++++++++++++ server/types.go | 12 ++++---- web/public/sw.js | 6 ++++ web/src/app/SubscriptionManager.js | 4 ++- web/src/app/db.js | 7 +++++ 6 files changed, 74 insertions(+), 10 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index bd4dddd4..589d06f8 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -75,7 +75,7 @@ const ( deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics - selectMessagesByIDQuery = ` + selectMessagesByIDQuery = ` SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE mid = ? @@ -431,7 +431,7 @@ func (c *messageCache) addMessages(ms []*message) error { m.ContentType, m.Encoding, published, - 0, + m.Deleted, ) if err != nil { return err @@ -719,8 +719,9 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 - var priority, deleted int + var priority int var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var deleted bool err := rows.Scan( &id, &sid, diff --git a/server/server.go b/server/server.go index 9c612ebd..67fe328d 100644 --- a/server/server.go +++ b/server/server.go @@ -547,6 +547,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.transformMatrixJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublishMatrix)))(w, r, v) } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updatePathRegex.MatchString(r.URL.Path)) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) + } else if r.Method == http.MethodDelete && updatePathRegex.MatchString(r.URL.Path) { + return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleDelete))(w, r, v) } else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) { @@ -902,6 +904,52 @@ func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v * return writeMatrixSuccess(w) } +func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { + t, err := fromContext[*topic](r, contextTopic) + if err != nil { + return err + } + vrate, err := fromContext[*visitor](r, contextRateVisitor) + if err != nil { + return err + } + if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { + return errHTTPTooManyRequestsLimitMessages.With(t) + } + sid, e := s.sidFromPath(r.URL.Path) + if e != nil { + return e.With(t) + } + // Create a delete message: empty body, same SID, deleted flag set + m := newDefaultMessage(t.ID, "") + m.SID = sid + m.Deleted = true + m.Sender = v.IP() + m.User = v.MaybeUserID() + m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() + // Publish to subscribers + if err := t.Publish(v, m); err != nil { + return err + } + // Send to Firebase for Android clients + if s.firebaseClient != nil { + go s.sendToFirebase(v, m) + } + // Send to web push endpoints + if s.config.WebPushPublicKey != "" { + go s.publishToWebPushEndpoints(v, m) + } + // Add to message cache + if err := s.messageCache.AddMessage(m); err != nil { + return err + } + logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with SID %s", sid) + s.mu.Lock() + s.messages++ + s.mu.Unlock() + return s.writeJSON(w, m.forJSON()) +} + func (s *Server) sendToFirebase(v *visitor, m *message) { logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase") if err := s.firebaseClient.Send(v, m); err != nil { diff --git a/server/types.go b/server/types.go index 467e80f5..88110b0d 100644 --- a/server/types.go +++ b/server/types.go @@ -24,14 +24,14 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) - Time int64 `json:"time"` // Unix time in seconds + ID string `json:"id"` // Random message ID + SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) + Time int64 `json:"time"` // Unix time in seconds Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) Event string `json:"event"` // One of the above Topic string `json:"topic"` Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` + Message string `json:"message"` // Allow empty message body Priority int `json:"priority,omitempty"` Tags []string `json:"tags,omitempty"` Click string `json:"click,omitempty"` @@ -40,10 +40,10 @@ type message struct { Attachment *attachment `json:"attachment,omitempty"` PollID string `json:"poll_id,omitempty"` ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown - Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes + Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes + Deleted bool `json:"deleted,omitempty"` // True if message is marked as deleted Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting User string `json:"-"` // UserID of the uploader, used to associated attachments - Deleted int `json:"deleted,omitempty"` } func (m *message) Context() log.Context { diff --git a/web/public/sw.js b/web/public/sw.js index e010e4d4..33e97da8 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -57,6 +57,12 @@ const handlePushMessage = async (data) => { broadcastChannel.postMessage(message); // To potentially play sound await addNotification({ subscriptionId, message }); + + // Don't show a notification for deleted messages + if (message.deleted) { + return; + } + await self.registration.showNotification( ...toNotificationParams({ subscriptionId, diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index ccce5ccc..7d917e2d 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -175,6 +175,7 @@ class SubscriptionManager { } // Collapse notification updates based on sids, keeping only the latest version + // Filters out notifications where the latest in the sequence is deleted groupNotificationsBySID(notifications) { const latestBySid = {}; notifications.forEach((notification) => { @@ -184,7 +185,8 @@ class SubscriptionManager { latestBySid[key] = notification; } }); - return Object.values(latestBySid); + // Filter out notifications where the latest is deleted + return Object.values(latestBySid).filter((n) => !n.deleted); } /** Adds notification, or returns false if it already exists */ diff --git a/web/src/app/db.js b/web/src/app/db.js index f11a5d0b..0391388d 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -18,6 +18,13 @@ const createDatabase = (username) => { prefs: "&key", }); + db.version(5).stores({ + subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", + notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new]", // added deleted index + users: "&baseUrl,username", + prefs: "&key", + }); + return db; }; From 2dd152df3f198f524c31bc2f42242ef6eb61fb81 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 6 Jan 2026 18:02:08 -0500 Subject: [PATCH 340/378] Manual fixes for AI slop --- go.mod | 14 +- go.sum | 28 +-- server/server.go | 5 +- web/package-lock.json | 257 ++++++++++++++++----------- web/public/sw.js | 19 +- web/src/app/SubscriptionManager.js | 56 +++--- web/src/app/db.js | 12 +- web/src/app/notificationUtils.js | 10 +- web/src/app/utils.js | 7 + web/src/components/Notifications.jsx | 28 +-- web/src/components/hooks.js | 15 +- 11 files changed, 255 insertions(+), 196 deletions(-) diff --git a/go.mod b/go.mod index e6d91101..2b80f31b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/emersion/go-smtp v0.18.0 github.com/gabriel-vasile/mimetype v1.4.12 github.com/gorilla/websocket v1.5.3 - github.com/mattn/go-sqlite3 v1.14.32 + github.com/mattn/go-sqlite3 v1.14.33 github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 @@ -21,7 +21,7 @@ require ( golang.org/x/sync v0.19.0 golang.org/x/term v0.38.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.258.0 + google.golang.org/api v0.259.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -76,7 +76,7 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -95,10 +95,10 @@ require ( golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect - google.golang.org/grpc v1.77.0 // indirect + google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index babf7049..56e08d3b 100644 --- a/go.sum +++ b/go.sum @@ -112,8 +112,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -131,8 +131,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -263,18 +263,18 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc= -google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww= +google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ= +google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 h1:stRtB2UVzFOWnorVuwF0BVVEjQ3AN6SjHWdg811UIQM= -google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= -google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= -google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b h1:kqShdsddZrS6q+DGBCA73CzHsKDu5vW4qw78tFnbVvY= +google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:gw1DtiPCt5uh/HV9STVEeaO00S5ATsJiJ2LsZV8lcDI= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/server/server.go b/server/server.go index 67fe328d..40a15e30 100644 --- a/server/server.go +++ b/server/server.go @@ -139,7 +139,8 @@ var ( const ( firebaseControlTopic = "~control" // See Android if changed firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now) - emptyMessageBody = "triggered" // Used if message body is empty + emptyMessageBody = "triggered" // Used when a message body is empty + deletedMessageBody = "deleted" // Used when a message is deleted newMessageBody = "New message" // Used in poll requests as generic message defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages @@ -921,7 +922,7 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor return e.With(t) } // Create a delete message: empty body, same SID, deleted flag set - m := newDefaultMessage(t.ID, "") + m := newDefaultMessage(t.ID, deletedMessageBody) m.SID = sid m.Deleted = true m.Sender = v.IP() diff --git a/web/package-lock.json b/web/package-lock.json index 789b95f7..75bdd453 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2198,9 +2198,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2798,9 +2798,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "cpu": [ "arm" ], @@ -2812,9 +2812,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "cpu": [ "arm64" ], @@ -2826,9 +2826,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "cpu": [ "arm64" ], @@ -2840,9 +2840,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "cpu": [ "x64" ], @@ -2854,9 +2854,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "cpu": [ "arm64" ], @@ -2868,9 +2868,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "cpu": [ "x64" ], @@ -2882,9 +2882,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "cpu": [ "arm" ], @@ -2896,9 +2896,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "cpu": [ "arm" ], @@ -2910,9 +2910,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -2924,9 +2924,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "cpu": [ "arm64" ], @@ -2938,9 +2938,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", "cpu": [ "loong64" ], @@ -2952,9 +2966,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", "cpu": [ "ppc64" ], @@ -2966,9 +2994,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", "cpu": [ "riscv64" ], @@ -2980,9 +3008,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "cpu": [ "riscv64" ], @@ -2994,9 +3022,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "cpu": [ "s390x" ], @@ -3008,9 +3036,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -3022,9 +3050,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -3035,10 +3063,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", "cpu": [ "arm64" ], @@ -3050,9 +3092,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "cpu": [ "arm64" ], @@ -3064,9 +3106,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "cpu": [ "ia32" ], @@ -3078,9 +3120,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "cpu": [ "x64" ], @@ -3092,9 +3134,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "cpu": [ "x64" ], @@ -3566,9 +3608,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -3781,9 +3823,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "dev": true, "funding": [ { @@ -4872,9 +4914,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4969,9 +5011,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -7586,9 +7628,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", "dependencies": { @@ -7602,28 +7644,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, diff --git a/web/public/sw.js b/web/public/sw.js index 33e97da8..3370cd83 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -8,6 +8,7 @@ import { dbAsync } from "../src/app/db"; import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; +import { messageWithSID } from "../src/app/utils"; /** * General docs for service workers and PWAs: @@ -23,19 +24,17 @@ const broadcastChannel = new BroadcastChannel("web-push-broadcast"); const addNotification = async ({ subscriptionId, message }) => { const db = await dbAsync(); - const populatedMessage = message; - if (!("sid" in populatedMessage)) { - populatedMessage.sid = message.id; - } + // Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too + // Add notification to database await db.notifications.add({ - ...populatedMessage, + ...messageWithSID(message), subscriptionId, - // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation - new: 1, + new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); + // Update subscription last message id (for ?since=... queries) await db.subscriptions.update(subscriptionId, { last: message.id, }); @@ -54,8 +53,7 @@ const addNotification = async ({ subscriptionId, message }) => { const handlePushMessage = async (data) => { const { subscription_id: subscriptionId, message } = data; - broadcastChannel.postMessage(message); // To potentially play sound - + // Add notification to database await addNotification({ subscriptionId, message }); // Don't show a notification for deleted messages @@ -63,6 +61,9 @@ const handlePushMessage = async (data) => { return; } + // Broadcast the message to potentially play a sound + broadcastChannel.postMessage(message); + await self.registration.showNotification( ...toNotificationParams({ subscriptionId, diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 7d917e2d..f5ea5a53 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -2,7 +2,7 @@ import api from "./Api"; import notifier from "./Notifier"; import prefs from "./Prefs"; import db from "./db"; -import { topicUrl } from "./utils"; +import { messageWithSID, topicUrl } from "./utils"; class SubscriptionManager { constructor(dbImpl) { @@ -15,7 +15,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() })) ); } @@ -48,16 +48,17 @@ class SubscriptionManager { } async notify(subscriptionId, notification) { + if (notification.deleted) { + return; + } const subscription = await this.get(subscriptionId); if (subscription.mutedUntil > 0) { return; } - const priority = notification.priority ?? 3; if (priority < (await prefs.minPriority())) { return; } - await notifier.notify(subscription, notification); } @@ -82,7 +83,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null, + last: null }; await this.db.subscriptions.put(subscription); @@ -100,7 +101,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation, // May be null! + reservation // May be null! }); return local.id; @@ -196,19 +197,20 @@ class SubscriptionManager { return false; } try { - const populatedNotification = notification; - if (!("sid" in populatedNotification)) { - populatedNotification.sid = notification.id; - } - // sw.js duplicates this logic, so if you change it here, change it there too + // Note: Service worker (sw.js) and addNotifications() duplicates this logic, + // so if you change it here, change it there too. + + // Add notification to database await this.db.notifications.add({ - ...populatedNotification, + ...messageWithSID(notification), subscriptionId, - // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation - new: 1, - }); // FIXME consider put() for double tab + new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + }); + + // FIXME consider put() for double tab + // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id, + last: notification.id }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -219,16 +221,12 @@ class SubscriptionManager { /** Adds/replaces notifications, will not throw if they exist */ async addNotifications(subscriptionId, notifications) { const notificationsWithSubscriptionId = notifications.map((notification) => { - const populatedNotification = notification; - if (!("sid" in populatedNotification)) { - populatedNotification.sid = notification.id; - } - return { ...populatedNotification, subscriptionId }; + return { ...messageWithSID(notification), subscriptionId }; }); const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId, + last: lastNotificationId }); } @@ -249,6 +247,10 @@ class SubscriptionManager { await this.db.notifications.delete(notificationId); } + async deleteNotificationBySid(subscriptionId, sid) { + await this.db.notifications.where({ subscriptionId, sid }).delete(); + } + async deleteNotifications(subscriptionId) { await this.db.notifications.where({ subscriptionId }).delete(); } @@ -257,25 +259,29 @@ class SubscriptionManager { await this.db.notifications.where({ id: notificationId }).modify({ new: 0 }); } + async markNotificationReadBySid(subscriptionId, sid) { + await this.db.notifications.where({ subscriptionId, sid }).modify({ new: 0 }); + } + async markNotificationsRead(subscriptionId) { await this.db.notifications.where({ subscriptionId, new: 1 }).modify({ new: 0 }); } async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil, + mutedUntil }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName, + displayName }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation, + reservation }); } diff --git a/web/src/app/db.js b/web/src/app/db.js index 0391388d..1bda553f 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -11,16 +11,10 @@ const createDatabase = (username) => { const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user const db = new Dexie(dbName); - db.version(4).stores({ + db.version(6).stores({ + // FIXME Should be 3 subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,sid,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance - users: "&baseUrl,username", - prefs: "&key", - }); - - db.version(5).stores({ - subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new]", // added deleted index + notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sid]", users: "&baseUrl,username", prefs: "&key", }); diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 55d398c9..6cb8bc37 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -53,14 +53,6 @@ export const badge = "/static/images/mask-icon.svg"; export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { const image = isImage(message.attachment) ? message.attachment.url : undefined; - let tag; - - if (message.sid) { - tag = message.sid; - } else { - tag = subscriptionId; - } - // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API return [ formatTitleWithDefault(message, defaultTitle), @@ -70,7 +62,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to icon, image, timestamp: message.time * 1000, - tag, + tag: message.sid || message.id, // Update notification if there is a sequence ID renotify: true, silent: false, // This is used by the notification onclick event diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 935f2024..d9f851b4 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -103,6 +103,13 @@ export const maybeActionErrors = (notification) => { return actionErrors; }; +export const messageWithSID = (message) => { + if (!message.sid) { + message.sid = message.id; + } + return message; +}; + export const shuffle = (arr) => { const returnArr = [...arr]; diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 94185b7c..53c9085f 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -240,22 +240,22 @@ const NotificationItem = (props) => { const otherTags = unmatchedTags(notification.tags); const tags = otherTags.length > 0 ? otherTags.join(", ") : null; const handleDelete = async () => { - console.log(`[Notifications] Deleting notification ${notification.id}`); - await subscriptionManager.deleteNotification(notification.id); - notification.history?.forEach(async (revision) => { - console.log(`[Notifications] Deleting revision ${revision.id}`); - await subscriptionManager.deleteNotification(revision.id); - }); + if (notification.sid) { + console.log(`[Notifications] Deleting all notifications with sid ${notification.sid}`); + await subscriptionManager.deleteNotificationBySid(notification.subscriptionId, notification.sid); + } else { + console.log(`[Notifications] Deleting notification ${notification.id}`); + await subscriptionManager.deleteNotification(notification.id); + } }; const handleMarkRead = async () => { - console.log(`[Notifications] Marking notification ${notification.id} as read`); - await subscriptionManager.markNotificationRead(notification.id); - notification.history - ?.filter((revision) => revision.new === 1) - .forEach(async (revision) => { - console.log(`[Notifications] Marking revision ${revision.id} as read`); - await subscriptionManager.markNotificationRead(revision.id); - }); + if (notification.sid) { + console.log(`[Notifications] Marking notification with sid ${notification.sid} as read`); + await subscriptionManager.markNotificationReadBySid(notification.subscriptionId, notification.sid); + } else { + console.log(`[Notifications] Marking notification ${notification.id} as read`); + await subscriptionManager.markNotificationRead(notification.id); + } }; const handleCopy = (s) => { copyToClipboard(s); diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 519d4c6a..4d852140 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -50,12 +50,23 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop }; const handleNotification = async (subscriptionId, notification) => { + if (notification.deleted && notification.sid) { + return handleDeletedNotification(subscriptionId, notification); + } + return handleNewOrUpdatedNotification(subscriptionId, notification); + }; + + const handleNewOrUpdatedNotification = async (subscriptionId, notification) => { const added = await subscriptionManager.addNotification(subscriptionId, notification); if (added) { await subscriptionManager.notify(subscriptionId, notification); } }; + const handleDeletedNotification = async (subscriptionId, notification) => { + await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); + }; + const handleMessage = async (subscriptionId, message) => { const subscription = await subscriptionManager.get(subscriptionId); @@ -231,7 +242,9 @@ export const useIsLaunchedPWA = () => { useEffect(() => { if (isIOSStandalone) { - return () => {}; // No need to listen for events on iOS + return () => { + // No need to listen for events on iOS + }; } const handler = (evt) => { console.log(`[useIsLaunchedPWA] App is now running ${evt.matches ? "standalone" : "in the browser"}`); From 544f7ef1cbcb2ad475b868b299c8c687ac23b29c Mon Sep 17 00:00:00 2001 From: cyberboh Date: Tue, 6 Jan 2026 14:49:16 +0100 Subject: [PATCH 341/378] Translated using Weblate (Indonesian) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/ --- web/public/static/langs/id.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json index a149e570..c63d9a12 100644 --- a/web/public/static/langs/id.json +++ b/web/public/static/langs/id.json @@ -50,7 +50,7 @@ "publish_dialog_progress_uploading": "Mengunggah …", "notifications_more_details": "Untuk informasi lanjut, lihat situs web atau dokumentasi.", "publish_dialog_progress_uploading_detail": "Mengunggah {{loaded}}/{{total}} ({{percent}}%) …", - "publish_dialog_message_published": "Notifikasi terpublikasi", + "publish_dialog_message_published": "Notifikasi dipublikasi", "notifications_loading": "Memuat notifikasi …", "publish_dialog_base_url_label": "URL Layanan", "publish_dialog_title_placeholder": "Judul notifikasi, mis. Peringatan ruang disk", @@ -71,9 +71,9 @@ "publish_dialog_priority_high": "Prioritas tinggi", "publish_dialog_priority_max": "Prioritas maksimal", "publish_dialog_topic_label": "Nama topik", - "publish_dialog_message_placeholder": "Ketik sebuah pesan di sini", + "publish_dialog_message_placeholder": "Tulis pesan di sini", "publish_dialog_click_label": "Klik URL", - "publish_dialog_tags_placeholder": "Daftar tanda yang dipisah dengan koma, mis. peringatan, cadangan-srv1", + "publish_dialog_tags_placeholder": "Daftar label yang dipisah dengan tanda koma, contoh: peringatan, cadangan-srv1", "publish_dialog_click_placeholder": "URL yang dibuka ketika notifikasi diklik", "publish_dialog_email_label": "Email", "publish_dialog_email_placeholder": "Alamat untuk meneruskan notifikasi, mis. andi@contoh.com", @@ -404,5 +404,7 @@ "web_push_subscription_expiring_title": "Notifikasi akan dijeda", "web_push_subscription_expiring_body": "Buka ntfy untuk terus menerima notifikasi", "web_push_unknown_notification_title": "Notifikasi yang tidak diketahui diterima dari server", - "web_push_unknown_notification_body": "Anda mungkin harus memperbarui ntfy dengan membuka aplikasi web" + "web_push_unknown_notification_body": "Anda mungkin harus memperbarui ntfy dengan membuka aplikasi web", + "account_basics_cannot_edit_or_delete_provisioned_user": "Pengguna yang telah ditetapkan tidak dapat diedit atau dihapus", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Tidak dapat mengedit atau menghapus token yang disediakan" } From b3721c1b7148bc65d7c94ac5fac2576f50306181 Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Wed, 7 Jan 2026 07:29:49 +0100 Subject: [PATCH 342/378] Translated using Weblate (Macedonian) Currently translated at 14.7% (60 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index b407c512..56417357 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -53,5 +53,10 @@ "nav_button_subscribe": "Претплати се на тема", "action_bar_unmute_notifications": "Одглуши ги нотификациите", "action_bar_toggle_mute": "Заглуши/Загуши ги нотификациите", - "message_bar_publish": "Објави порака" + "message_bar_publish": "Објави порака", + "nav_button_connecting": "се конектира", + "nav_upgrade_banner_label": "Надградете на ntfy Pro", + "nav_upgrade_banner_description": "Резервирајте теми, повеќе пораки и е-пораки и поголеми прилози", + "alert_notification_permission_required_title": "Известувањата се исклучени", + "alert_notification_permission_required_description": "Дајте му дозвола на вашиот прелистувач да прикажува известувања" } From bfbe73aea36f44208f5795849b70ebabcd803a30 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 7 Jan 2026 09:46:08 -0500 Subject: [PATCH 343/378] Update polling --- web/src/app/Poller.js | 36 +++++++++++++++++++++++++----- web/src/app/SubscriptionManager.js | 2 +- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index 2261dddc..8fefc94e 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -42,12 +42,22 @@ class Poller { const since = subscription.last; const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); - if (!notifications || notifications.length === 0) { - console.log(`[Poller] No new notifications found for ${subscription.id}`); - return; + const deletedSids = this.deletedSids(notifications); + const newOrUpdatedNotifications = this.newOrUpdatedNotifications(notifications, deletedSids); + + // Delete all existing notifications with a deleted sequence ID + if (deletedSids.length > 0) { + console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`); + await Promise.all(deletedSids.map((sid) => subscriptionManager.deleteNotificationBySid(subscription.id, sid))); + } + + // Add new or updated notifications + if (newOrUpdatedNotifications.length > 0) { + console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`); + await subscriptionManager.addNotifications(subscription.id, notifications); + } else { + console.log(`[Poller] No new notifications found for ${subscription.id}`); } - console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`); - await subscriptionManager.addNotifications(subscription.id, notifications); } pollInBackground(subscription) { @@ -59,6 +69,22 @@ class Poller { } })(); } + + deletedSids(notifications) { + return new Set( + notifications + .filter(n => n.sid && n.deleted) + .map(n => n.sid) + ); + } + + newOrUpdatedNotifications(notifications, deletedSids) { + return notifications + .filter((notification) => { + const sid = notification.sid || notification.id; + return !deletedSids.has(notification.id) && !deletedSids.has(sid) && !notification.deleted; + }); + } } const poller = new Poller(); diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index f5ea5a53..2b9260f7 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -193,7 +193,7 @@ class SubscriptionManager { /** Adds notification, or returns false if it already exists */ async addNotification(subscriptionId, notification) { const exists = await this.db.notifications.get(notification.id); - if (exists) { + if (exists || notification.deleted) { return false; } try { From 75abf2e245e7c5fa22b55f9cf437c58b180753e0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 10:28:02 -0500 Subject: [PATCH 344/378] Delete old messages with SID when new messages arrive --- web/public/sw.js | 5 ++++ web/src/app/Poller.js | 42 ++++++++++++++++-------------- web/src/app/SubscriptionManager.js | 26 ++++++++---------- web/src/components/hooks.js | 5 ++++ 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/web/public/sw.js b/web/public/sw.js index 3370cd83..3a67da58 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -27,6 +27,11 @@ const addNotification = async ({ subscriptionId, message }) => { // Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too + // Delete existing notification with same SID (if any) + if (message.sid) { + await db.notifications.where({ subscriptionId, sid: message.sid }).delete(); + } + // Add notification to database await db.notifications.add({ ...messageWithSID(message), diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index 8fefc94e..9f3ff6a0 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -42,19 +42,22 @@ class Poller { const since = subscription.last; const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); - const deletedSids = this.deletedSids(notifications); - const newOrUpdatedNotifications = this.newOrUpdatedNotifications(notifications, deletedSids); + const latestBySid = this.latestNotificationsBySid(notifications); // Delete all existing notifications with a deleted sequence ID + const deletedSids = Object.entries(latestBySid) + .filter(([, notification]) => notification.deleted) + .map(([sid]) => sid); if (deletedSids.length > 0) { console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`); await Promise.all(deletedSids.map((sid) => subscriptionManager.deleteNotificationBySid(subscription.id, sid))); } - // Add new or updated notifications - if (newOrUpdatedNotifications.length > 0) { - console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`); - await subscriptionManager.addNotifications(subscription.id, notifications); + // Add only the latest notification for each non-deleted sequence + const notificationsToAdd = Object.values(latestBySid).filter((n) => !n.deleted); + if (notificationsToAdd.length > 0) { + console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`); + await subscriptionManager.addNotifications(subscription.id, notificationsToAdd); } else { console.log(`[Poller] No new notifications found for ${subscription.id}`); } @@ -70,20 +73,19 @@ class Poller { })(); } - deletedSids(notifications) { - return new Set( - notifications - .filter(n => n.sid && n.deleted) - .map(n => n.sid) - ); - } - - newOrUpdatedNotifications(notifications, deletedSids) { - return notifications - .filter((notification) => { - const sid = notification.sid || notification.id; - return !deletedSids.has(notification.id) && !deletedSids.has(sid) && !notification.deleted; - }); + /** + * Groups notifications by sid and returns only the latest (highest time) for each sequence. + * Returns an object mapping sid -> latest notification. + */ + latestNotificationsBySid(notifications) { + const latestBySid = {}; + notifications.forEach((notification) => { + const sid = notification.sid || notification.id; + if (!(sid in latestBySid) || notification.time >= latestBySid[sid].time) { + latestBySid[sid] = notification; + } + }); + return latestBySid; } } diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 2b9260f7..40cc475a 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -15,7 +15,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), })) ); } @@ -83,7 +83,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null + last: null, }; await this.db.subscriptions.put(subscription); @@ -101,7 +101,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation // May be null! + reservation, // May be null! }); return local.id; @@ -157,22 +157,18 @@ class SubscriptionManager { // It's actually fine, because the reading and filtering is quite fast. The rendering is what's // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach - const notifications = await this.db.notifications + return this.db.notifications .orderBy("time") // Sort by time .filter((n) => n.subscriptionId === subscriptionId) .reverse() .toArray(); - - return this.groupNotificationsBySID(notifications); } async getAllNotifications() { - const notifications = await this.db.notifications + return this.db.notifications .orderBy("time") // Efficient, see docs .reverse() .toArray(); - - return this.groupNotificationsBySID(notifications); } // Collapse notification updates based on sids, keeping only the latest version @@ -204,13 +200,13 @@ class SubscriptionManager { await this.db.notifications.add({ ...messageWithSID(notification), subscriptionId, - new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); // FIXME consider put() for double tab // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id + last: notification.id, }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -226,7 +222,7 @@ class SubscriptionManager { const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId + last: lastNotificationId, }); } @@ -269,19 +265,19 @@ class SubscriptionManager { async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil + mutedUntil, }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName + displayName, }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation + reservation, }); } diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 4d852140..588697fc 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -57,6 +57,11 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop }; const handleNewOrUpdatedNotification = async (subscriptionId, notification) => { + // Delete existing notification with same sid, if any + if (notification.sid) { + await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); + } + // Add notification to database const added = await subscriptionManager.addNotification(subscriptionId, notification); if (added) { await subscriptionManager.notify(subscriptionId, notification); From 239959e2a4bc2a3f39b407414cf8d4a22c0706aa Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 11:19:53 -0500 Subject: [PATCH 345/378] Revert some changes; make poller respect deleteAfter pref --- web/src/app/Poller.js | 14 +++++++++--- web/src/app/SubscriptionManager.js | 33 ++++++++-------------------- web/src/components/Notifications.jsx | 18 ++++----------- 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index 9f3ff6a0..5c7d2e2d 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -1,4 +1,5 @@ import api from "./Api"; +import prefs from "./Prefs"; import subscriptionManager from "./SubscriptionManager"; const delayMillis = 2000; // 2 seconds @@ -42,14 +43,21 @@ class Poller { const since = subscription.last; const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); - const latestBySid = this.latestNotificationsBySid(notifications); - // Delete all existing notifications with a deleted sequence ID + // Filter out notifications older than the prune threshold + const deleteAfterSeconds = await prefs.deleteAfter(); + const pruneThresholdTimestamp = deleteAfterSeconds > 0 ? Math.round(Date.now() / 1000) - deleteAfterSeconds : 0; + const recentNotifications = pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications; + + // Find the latest notification for each sequence ID + const latestBySid = this.latestNotificationsBySid(recentNotifications); + + // Delete all existing notifications for which the latest notification is marked as deleted const deletedSids = Object.entries(latestBySid) .filter(([, notification]) => notification.deleted) .map(([sid]) => sid); if (deletedSids.length > 0) { - console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`); + console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSids); await Promise.all(deletedSids.map((sid) => subscriptionManager.deleteNotificationBySid(subscription.id, sid))); } diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 40cc475a..4a4c6f54 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -15,7 +15,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() })) ); } @@ -83,7 +83,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null, + last: null }; await this.db.subscriptions.put(subscription); @@ -101,7 +101,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation, // May be null! + reservation // May be null! }); return local.id; @@ -171,21 +171,6 @@ class SubscriptionManager { .toArray(); } - // Collapse notification updates based on sids, keeping only the latest version - // Filters out notifications where the latest in the sequence is deleted - groupNotificationsBySID(notifications) { - const latestBySid = {}; - notifications.forEach((notification) => { - const key = `${notification.subscriptionId}:${notification.sid}`; - // Keep only the first (latest by time) notification for each sid - if (!(key in latestBySid)) { - latestBySid[key] = notification; - } - }); - // Filter out notifications where the latest is deleted - return Object.values(latestBySid).filter((n) => !n.deleted); - } - /** Adds notification, or returns false if it already exists */ async addNotification(subscriptionId, notification) { const exists = await this.db.notifications.get(notification.id); @@ -200,13 +185,13 @@ class SubscriptionManager { await this.db.notifications.add({ ...messageWithSID(notification), subscriptionId, - new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); // FIXME consider put() for double tab // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id, + last: notification.id }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -222,7 +207,7 @@ class SubscriptionManager { const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId, + last: lastNotificationId }); } @@ -265,19 +250,19 @@ class SubscriptionManager { async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil, + mutedUntil }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName, + displayName }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation, + reservation }); } diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 53c9085f..449b238b 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -240,22 +240,12 @@ const NotificationItem = (props) => { const otherTags = unmatchedTags(notification.tags); const tags = otherTags.length > 0 ? otherTags.join(", ") : null; const handleDelete = async () => { - if (notification.sid) { - console.log(`[Notifications] Deleting all notifications with sid ${notification.sid}`); - await subscriptionManager.deleteNotificationBySid(notification.subscriptionId, notification.sid); - } else { - console.log(`[Notifications] Deleting notification ${notification.id}`); - await subscriptionManager.deleteNotification(notification.id); - } + console.log(`[Notifications] Deleting notification ${notification.id}`); + await subscriptionManager.deleteNotification(notification.id); }; const handleMarkRead = async () => { - if (notification.sid) { - console.log(`[Notifications] Marking notification with sid ${notification.sid} as read`); - await subscriptionManager.markNotificationReadBySid(notification.subscriptionId, notification.sid); - } else { - console.log(`[Notifications] Marking notification ${notification.id} as read`); - await subscriptionManager.markNotificationRead(notification.id); - } + console.log(`[Notifications] Marking notification ${notification.id} as read`); + await subscriptionManager.markNotificationRead(notification.id); }; const handleCopy = (s) => { copyToClipboard(s); From dffee9ea7d64cfb928ed31d1efb61490252c6d57 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 11:39:32 -0500 Subject: [PATCH 346/378] Remove forJSON --- server/message_cache.go | 4 ++++ server/server.go | 8 ++++---- server/types.go | 12 +----------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index 589d06f8..1752c2c9 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -773,6 +773,10 @@ func readMessage(rows *sql.Rows) (*message, error) { URL: attachmentURL, } } + // Clear SID if it equals ID (we do not want the SID in the message output) + if sid == id { + sid = "" + } return &message{ ID: id, SID: sid, diff --git a/server/server.go b/server/server.go index 40a15e30..edae9f2a 100644 --- a/server/server.go +++ b/server/server.go @@ -877,7 +877,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } minc(metricMessagesPublishedSuccess) - return s.writeJSON(w, m.forJSON()) + return s.writeJSON(w, m) } func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error { @@ -948,7 +948,7 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor s.mu.Lock() s.messages++ s.mu.Unlock() - return s.writeJSON(w, m.forJSON()) + return s.writeJSON(w, m) } func (s *Server) sendToFirebase(v *visitor, m *message) { @@ -1340,7 +1340,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { + if err := json.NewEncoder(&buf).Encode(msg); err != nil { return "", err } return buf.String(), nil @@ -1351,7 +1351,7 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v * func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { + if err := json.NewEncoder(&buf).Encode(msg); err != nil { return "", err } if msg.Event != messageEvent { diff --git a/server/types.go b/server/types.go index 88110b0d..b68721a3 100644 --- a/server/types.go +++ b/server/types.go @@ -64,16 +64,6 @@ func (m *message) Context() log.Context { return fields } -// forJSON returns a copy of the message prepared for JSON output. -// It clears SID if it equals ID (to avoid redundant output). -func (m *message) forJSON() *message { - msg := *m - if msg.SID == msg.ID { - msg.SID = "" // Will be omitted due to omitempty - } - return &msg -} - type attachment struct { Name string `json:"name"` Type string `json:"type,omitempty"` @@ -560,7 +550,7 @@ func newWebPushPayload(subscriptionID string, message *message) *webPushPayload return &webPushPayload{ Event: webPushMessageEvent, SubscriptionID: subscriptionID, - Message: message.forJSON(), + Message: message, } } From 7cffbfcd6d78c02df0e9b1da2353451f359ed8f1 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 13:43:57 -0500 Subject: [PATCH 347/378] Simplify handleNotifications --- web/src/components/hooks.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 588697fc..9f836156 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -50,26 +50,18 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop }; const handleNotification = async (subscriptionId, notification) => { - if (notification.deleted && notification.sid) { - return handleDeletedNotification(subscriptionId, notification); - } - return handleNewOrUpdatedNotification(subscriptionId, notification); - }; - - const handleNewOrUpdatedNotification = async (subscriptionId, notification) => { // Delete existing notification with same sid, if any if (notification.sid) { await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); } - // Add notification to database - const added = await subscriptionManager.addNotification(subscriptionId, notification); - if (added) { - await subscriptionManager.notify(subscriptionId, notification); - } - }; - const handleDeletedNotification = async (subscriptionId, notification) => { - await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); + // Add notification to database + if (!notification.deleted) { + const added = await subscriptionManager.addNotification(subscriptionId, notification); + if (added) { + await subscriptionManager.notify(subscriptionId, notification); + } + } }; const handleMessage = async (subscriptionId, message) => { From fd8cd5ca91560dee8c63cdff5bf96f1914129a4c Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 13:45:05 -0500 Subject: [PATCH 348/378] Comment --- web/src/components/hooks.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 9f836156..3b171eab 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -50,11 +50,13 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop }; const handleNotification = async (subscriptionId, notification) => { + // Note: This logic is duplicated in the Android app in SubscriberService::onNotificationReceived() + // and FirebaseService::handleMessage(). + // Delete existing notification with same sid, if any if (notification.sid) { await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); } - // Add notification to database if (!notification.deleted) { const added = await subscriptionManager.addNotification(subscriptionId, notification); From 1ab7ca876c978f230dd0cd0ed26ffd394b781116 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 14:27:18 -0500 Subject: [PATCH 349/378] Rename to sequence_id --- server/errors.go | 2 +- server/message_cache.go | 38 ++++++++++---------- server/message_cache_test.go | 14 ++++---- server/server.go | 26 +++++++------- server/server_test.go | 10 +++--- server/types.go | 56 +++++++++++++++--------------- web/public/sw.js | 11 +++--- web/src/app/Poller.js | 35 ++++++++++--------- web/src/app/SubscriptionManager.js | 32 ++++++++--------- web/src/app/db.js | 7 ++-- web/src/app/notificationUtils.js | 2 +- web/src/app/utils.js | 6 ++-- web/src/components/hooks.js | 7 ++-- 13 files changed, 125 insertions(+), 121 deletions(-) diff --git a/server/errors.go b/server/errors.go index 302c06a7..950d23fc 100644 --- a/server/errors.go +++ b/server/errors.go @@ -125,7 +125,7 @@ var ( errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} - errHTTPBadRequestSIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: SID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} + errHTTPBadRequestSIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/message_cache.go b/server/message_cache.go index 1752c2c9..c00c67c8 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -29,7 +29,7 @@ const ( CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, mid TEXT NOT NULL, - sid TEXT NOT NULL, + sequence_id TEXT NOT NULL, time INT NOT NULL, expires INT NOT NULL, topic TEXT NOT NULL, @@ -54,7 +54,7 @@ const ( deleted INT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); - CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); + CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); CREATE INDEX IF NOT EXISTS idx_time ON messages (time); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires); @@ -69,50 +69,50 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) + INSERT INTO messages (mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesLatestQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 ` selectMessagesDueQuery = ` - SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -267,9 +267,9 @@ const ( //13 -> 14 migrate13To14AlterMessagesTableQuery = ` - ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT(''); + ALTER TABLE messages ADD COLUMN sequence_id TEXT NOT NULL DEFAULT(''); ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0'); - CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid); + CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); ` ) @@ -409,7 +409,7 @@ func (c *messageCache) addMessages(ms []*message) error { } _, err := stmt.Exec( m.ID, - m.SID, + m.SequenceID, m.Time, m.Expires, m.Topic, @@ -720,11 +720,11 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var id, sequenceID, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string var deleted bool err := rows.Scan( &id, - &sid, + &sequenceID, ×tamp, &expires, &topic, @@ -773,13 +773,13 @@ func readMessage(rows *sql.Rows) (*message, error) { URL: attachmentURL, } } - // Clear SID if it equals ID (we do not want the SID in the message output) - if sid == id { - sid = "" + // Clear SequenceID if it equals ID (we do not want the SequenceID in the message output) + if sequenceID == id { + sequenceID = "" } return &message{ ID: id, - SID: sid, + SequenceID: sequenceID, Time: timestamp, Expires: expires, Event: messageEvent, diff --git a/server/message_cache_test.go b/server/message_cache_test.go index 64203136..1e285605 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -319,7 +319,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires1 := time.Now().Add(-4 * time.Hour).Unix() // Expired m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" - m.SID = "m1" + m.SequenceID = "m1" m.Sender = netip.MustParseAddr("1.2.3.4") m.Attachment = &attachment{ Name: "flower.jpg", @@ -333,7 +333,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires2 := time.Now().Add(2 * time.Hour).Unix() // Future m = newDefaultMessage("mytopic", "sending you a car") m.ID = "m2" - m.SID = "m2" + m.SequenceID = "m2" m.Sender = netip.MustParseAddr("1.2.3.4") m.Attachment = &attachment{ Name: "car.jpg", @@ -347,7 +347,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires3 := time.Now().Add(1 * time.Hour).Unix() // Future m = newDefaultMessage("another-topic", "sending you another car") m.ID = "m3" - m.SID = "m3" + m.SequenceID = "m3" m.User = "u_BAsbaAa" m.Sender = netip.MustParseAddr("5.6.7.8") m.Attachment = &attachment{ @@ -403,13 +403,13 @@ func TestMemCache_Attachments_Expired(t *testing.T) { func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" - m.SID = "m1" + m.SequenceID = "m1" m.Expires = time.Now().Add(time.Hour).Unix() require.Nil(t, c.AddMessage(m)) m = newDefaultMessage("mytopic", "message with attachment") m.ID = "m2" - m.SID = "m2" + m.SequenceID = "m2" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "car.jpg", @@ -422,7 +422,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m = newDefaultMessage("mytopic", "message with external attachment") m.ID = "m3" - m.SID = "m3" + m.SequenceID = "m3" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "car.jpg", @@ -434,7 +434,7 @@ func testCacheAttachmentsExpired(t *testing.T, c *messageCache) { m = newDefaultMessage("mytopic2", "message with expired attachment") m.ID = "m4" - m.SID = "m4" + m.SequenceID = "m4" m.Expires = time.Now().Add(2 * time.Hour).Unix() m.Attachment = &attachment{ Name: "expired-car.jpg", diff --git a/server/server.go b/server/server.go index edae9f2a..c60154ec 100644 --- a/server/server.go +++ b/server/server.go @@ -917,13 +917,13 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { return errHTTPTooManyRequestsLimitMessages.With(t) } - sid, e := s.sidFromPath(r.URL.Path) + sequenceID, e := s.sequenceIDFromPath(r.URL.Path) if e != nil { return e.With(t) } - // Create a delete message: empty body, same SID, deleted flag set + // Create a delete message: empty body, same SequenceID, deleted flag set m := newDefaultMessage(t.ID, deletedMessageBody) - m.SID = sid + m.SequenceID = sequenceID m.Deleted = true m.Sender = v.IP() m.User = v.MaybeUserID() @@ -944,7 +944,7 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor if err := s.messageCache.AddMessage(m); err != nil { return err } - logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with SID %s", sid) + logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with sequence ID %s", sequenceID) s.mu.Lock() s.messages++ s.mu.Unlock() @@ -1009,21 +1009,21 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) { if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) { - pathSID, err := s.sidFromPath(r.URL.Path) + pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path) if err != nil { return false, false, "", "", "", false, err } - m.SID = pathSID + m.SequenceID = pathSequenceID } else { - sid := readParam(r, "x-sequence-id", "sequence-id", "sid") - if sid != "" { - if sidRegex.MatchString(sid) { - m.SID = sid + sequenceID := readParam(r, "x-sequence-id", "sequence-id", "sid") + if sequenceID != "" { + if sidRegex.MatchString(sequenceID) { + m.SequenceID = sequenceID } else { return false, false, "", "", "", false, errHTTPBadRequestSIDInvalid } } else { - m.SID = m.ID + m.SequenceID = m.ID } } cache = readBoolParam(r, true, "x-cache", "cache") @@ -1764,8 +1764,8 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { return topics, parts[1], nil } -// sidFromPath returns the SID from a POST path like /mytopic/sidHere -func (s *Server) sidFromPath(path string) (string, *errHTTP) { +// sequenceIDFromPath returns the sequence ID from a POST path like /mytopic/sequenceIdHere +func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) { parts := strings.Split(path, "/") if len(parts) != 3 { return "", errHTTPBadRequestSIDInvalid diff --git a/server/server_test.go b/server/server_test.go index c1b78c63..2978947f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -684,7 +684,7 @@ func TestServer_PublishWithSIDInPath(t *testing.T) { response := request(t, s, "POST", "/mytopic/sid", "message", nil) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid", msg.SID) + require.Equal(t, "sid", msg.SequenceID) } func TestServer_PublishWithSIDInHeader(t *testing.T) { @@ -695,7 +695,7 @@ func TestServer_PublishWithSIDInHeader(t *testing.T) { }) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid", msg.SID) + require.Equal(t, "sid", msg.SequenceID) } func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) { @@ -706,7 +706,7 @@ func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) { }) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SID) // SID in path has priority over SID in header + require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header } func TestServer_PublishWithSIDInQuery(t *testing.T) { @@ -715,7 +715,7 @@ func TestServer_PublishWithSIDInQuery(t *testing.T) { response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SID) + require.Equal(t, "sid1", msg.SequenceID) } func TestServer_PublishWithSIDViaGet(t *testing.T) { @@ -724,7 +724,7 @@ func TestServer_PublishWithSIDViaGet(t *testing.T) { response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil) msg := toMessage(t, response.Body.String()) require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SID) + require.Equal(t, "sid1", msg.SequenceID) } func TestServer_PublishWithInvalidSIDInPath(t *testing.T) { diff --git a/server/types.go b/server/types.go index b68721a3..e9c0fdb8 100644 --- a/server/types.go +++ b/server/types.go @@ -24,11 +24,11 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) - Time int64 `json:"time"` // Unix time in seconds - Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) - Event string `json:"event"` // One of the above + ID string `json:"id"` // Random message ID + SequenceID string `json:"sequence_id,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID) + Time int64 `json:"time"` // Unix time in seconds + Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) + Event string `json:"event"` // One of the above Topic string `json:"topic"` Title string `json:"title,omitempty"` Message string `json:"message"` // Allow empty message body @@ -48,12 +48,12 @@ type message struct { func (m *message) Context() log.Context { fields := map[string]any{ - "topic": m.Topic, - "message_id": m.ID, - "message_sid": m.SID, - "message_time": m.Time, - "message_event": m.Event, - "message_body_size": len(m.Message), + "topic": m.Topic, + "message_id": m.ID, + "message_sequence_id": m.SequenceID, + "message_time": m.Time, + "message_event": m.Event, + "message_body_size": len(m.Message), } if m.Sender.IsValid() { fields["message_sender"] = m.Sender.String() @@ -94,23 +94,23 @@ func newAction() *action { // publishMessage is used as input when publishing as JSON type publishMessage struct { - Topic string `json:"topic"` - SID string `json:"sid"` - Title string `json:"title"` - Message string `json:"message"` - Priority int `json:"priority"` - Tags []string `json:"tags"` - Click string `json:"click"` - Icon string `json:"icon"` - Actions []action `json:"actions"` - Attach string `json:"attach"` - Markdown bool `json:"markdown"` - Filename string `json:"filename"` - Email string `json:"email"` - Call string `json:"call"` - Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead) - Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead) - Delay string `json:"delay"` + Topic string `json:"topic"` + SequenceID string `json:"sequence_id"` + Title string `json:"title"` + Message string `json:"message"` + Priority int `json:"priority"` + Tags []string `json:"tags"` + Click string `json:"click"` + Icon string `json:"icon"` + Actions []action `json:"actions"` + Attach string `json:"attach"` + Markdown bool `json:"markdown"` + Filename string `json:"filename"` + Email string `json:"email"` + Call string `json:"call"` + Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead) + Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead) + Delay string `json:"delay"` } // messageEncoder is a function that knows how to encode a message diff --git a/web/public/sw.js b/web/public/sw.js index 3a67da58..38dbc9c1 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -8,7 +8,7 @@ import { dbAsync } from "../src/app/db"; import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; -import { messageWithSID } from "../src/app/utils"; +import { messageWithSequenceId } from "../src/app/utils"; /** * General docs for service workers and PWAs: @@ -27,14 +27,15 @@ const addNotification = async ({ subscriptionId, message }) => { // Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too - // Delete existing notification with same SID (if any) - if (message.sid) { - await db.notifications.where({ subscriptionId, sid: message.sid }).delete(); + // Delete existing notification with same sequence ID (if any) + const sequenceId = message.sequence_id || message.id; + if (sequenceId) { + await db.notifications.where({ subscriptionId, sequenceId }).delete(); } // Add notification to database await db.notifications.add({ - ...messageWithSID(message), + ...messageWithSequenceId(message), subscriptionId, new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index 5c7d2e2d..aa0e6dba 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -47,22 +47,25 @@ class Poller { // Filter out notifications older than the prune threshold const deleteAfterSeconds = await prefs.deleteAfter(); const pruneThresholdTimestamp = deleteAfterSeconds > 0 ? Math.round(Date.now() / 1000) - deleteAfterSeconds : 0; - const recentNotifications = pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications; + const recentNotifications = + pruneThresholdTimestamp > 0 ? notifications.filter((n) => n.time >= pruneThresholdTimestamp) : notifications; // Find the latest notification for each sequence ID - const latestBySid = this.latestNotificationsBySid(recentNotifications); + const latestBySequenceId = this.latestNotificationsBySequenceId(recentNotifications); // Delete all existing notifications for which the latest notification is marked as deleted - const deletedSids = Object.entries(latestBySid) + const deletedSequenceIds = Object.entries(latestBySequenceId) .filter(([, notification]) => notification.deleted) - .map(([sid]) => sid); - if (deletedSids.length > 0) { - console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSids); - await Promise.all(deletedSids.map((sid) => subscriptionManager.deleteNotificationBySid(subscription.id, sid))); + .map(([sequenceId]) => sequenceId); + if (deletedSequenceIds.length > 0) { + console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds); + await Promise.all( + deletedSequenceIds.map((sequenceId) => subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId)) + ); } // Add only the latest notification for each non-deleted sequence - const notificationsToAdd = Object.values(latestBySid).filter((n) => !n.deleted); + const notificationsToAdd = Object.values(latestBySequenceId).filter((n) => !n.deleted); if (notificationsToAdd.length > 0) { console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`); await subscriptionManager.addNotifications(subscription.id, notificationsToAdd); @@ -82,18 +85,18 @@ class Poller { } /** - * Groups notifications by sid and returns only the latest (highest time) for each sequence. - * Returns an object mapping sid -> latest notification. + * Groups notifications by sequenceId and returns only the latest (highest time) for each sequence. + * Returns an object mapping sequenceId -> latest notification. */ - latestNotificationsBySid(notifications) { - const latestBySid = {}; + latestNotificationsBySequenceId(notifications) { + const latestBySequenceId = {}; notifications.forEach((notification) => { - const sid = notification.sid || notification.id; - if (!(sid in latestBySid) || notification.time >= latestBySid[sid].time) { - latestBySid[sid] = notification; + const sequenceId = notification.sequence_id || notification.id; + if (!(sequenceId in latestBySequenceId) || notification.time >= latestBySequenceId[sequenceId].time) { + latestBySequenceId[sequenceId] = notification; } }); - return latestBySid; + return latestBySequenceId; } } diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 4a4c6f54..772b30a7 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -2,7 +2,7 @@ import api from "./Api"; import notifier from "./Notifier"; import prefs from "./Prefs"; import db from "./db"; -import { messageWithSID, topicUrl } from "./utils"; +import { messageWithSequenceId, topicUrl } from "./utils"; class SubscriptionManager { constructor(dbImpl) { @@ -15,7 +15,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), })) ); } @@ -83,7 +83,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null + last: null, }; await this.db.subscriptions.put(subscription); @@ -101,7 +101,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation // May be null! + reservation, // May be null! }); return local.id; @@ -183,15 +183,15 @@ class SubscriptionManager { // Add notification to database await this.db.notifications.add({ - ...messageWithSID(notification), + ...messageWithSequenceId(notification), subscriptionId, - new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); // FIXME consider put() for double tab // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id + last: notification.id, }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -202,12 +202,12 @@ class SubscriptionManager { /** Adds/replaces notifications, will not throw if they exist */ async addNotifications(subscriptionId, notifications) { const notificationsWithSubscriptionId = notifications.map((notification) => { - return { ...messageWithSID(notification), subscriptionId }; + return { ...messageWithSequenceId(notification), subscriptionId }; }); const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId + last: lastNotificationId, }); } @@ -228,8 +228,8 @@ class SubscriptionManager { await this.db.notifications.delete(notificationId); } - async deleteNotificationBySid(subscriptionId, sid) { - await this.db.notifications.where({ subscriptionId, sid }).delete(); + async deleteNotificationBySequenceId(subscriptionId, sequenceId) { + await this.db.notifications.where({ subscriptionId, sequenceId }).delete(); } async deleteNotifications(subscriptionId) { @@ -240,8 +240,8 @@ class SubscriptionManager { await this.db.notifications.where({ id: notificationId }).modify({ new: 0 }); } - async markNotificationReadBySid(subscriptionId, sid) { - await this.db.notifications.where({ subscriptionId, sid }).modify({ new: 0 }); + async markNotificationReadBySequenceId(subscriptionId, sequenceId) { + await this.db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 }); } async markNotificationsRead(subscriptionId) { @@ -250,19 +250,19 @@ class SubscriptionManager { async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil + mutedUntil, }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName + displayName, }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation + reservation, }); } diff --git a/web/src/app/db.js b/web/src/app/db.js index 1bda553f..7e3c47e3 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -11,12 +11,11 @@ const createDatabase = (username) => { const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user const db = new Dexie(dbName); - db.version(6).stores({ - // FIXME Should be 3 + db.version(3).stores({ subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,sid,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sid]", + notifications: "&id,sequenceId,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sequenceId]", users: "&baseUrl,username", - prefs: "&key", + prefs: "&key" }); return db; diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 6cb8bc37..65b5bd3d 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -62,7 +62,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to icon, image, timestamp: message.time * 1000, - tag: message.sid || message.id, // Update notification if there is a sequence ID + tag: message.sequence_id || message.id, // Update notification if there is a sequence ID renotify: true, silent: false, // This is used by the notification onclick event diff --git a/web/src/app/utils.js b/web/src/app/utils.js index d9f851b4..9aeada05 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -103,9 +103,9 @@ export const maybeActionErrors = (notification) => { return actionErrors; }; -export const messageWithSID = (message) => { - if (!message.sid) { - message.sid = message.id; +export const messageWithSequenceId = (message) => { + if (!message.sequenceId) { + message.sequenceId = message.sequence_id || message.id; } return message; }; diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 3b171eab..5b50f0a8 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -53,9 +53,10 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop // Note: This logic is duplicated in the Android app in SubscriberService::onNotificationReceived() // and FirebaseService::handleMessage(). - // Delete existing notification with same sid, if any - if (notification.sid) { - await subscriptionManager.deleteNotificationBySid(subscriptionId, notification.sid); + // Delete existing notification with same sequenceId, if any + const sequenceId = notification.sequence_id || notification.id; + if (sequenceId) { + await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId); } // Add notification to database if (!notification.deleted) { From 66ea25c18b76fe5ba167b052caddcc57fd3219a4 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 15:45:50 -0500 Subject: [PATCH 350/378] Add JSON publishing support --- server/server.go | 3 +++ server/server_test.go | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/server/server.go b/server/server.go index c60154ec..635258fa 100644 --- a/server/server.go +++ b/server/server.go @@ -2027,6 +2027,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Firebase != "" { r.Header.Set("X-Firebase", m.Firebase) } + if m.SequenceID != "" { + r.Header.Set("X-Sequence-ID", m.SequenceID) + } return next(w, r, v) } } diff --git a/server/server_test.go b/server/server_test.go index 2978947f..964d6156 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -727,6 +727,18 @@ func TestServer_PublishWithSIDViaGet(t *testing.T) { require.Equal(t, "sid1", msg.SequenceID) } +func TestServer_PublishAsJSON_WithSequenceID(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + body := `{"topic":"mytopic","message":"A message","sequence_id":"my-sequence-123"}` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 200, response.Code) + + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "my-sequence-123", msg.SequenceID) +} + func TestServer_PublishWithInvalidSIDInPath(t *testing.T) { s := newTestServer(t, newTestConfig(t)) From 5ad3de290400af8db89d0602977ca50bea91fa7e Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 20:50:23 -0500 Subject: [PATCH 351/378] Switch to event type --- server/errors.go | 2 +- server/message_cache.go | 39 +++++++++--------- server/server.go | 64 +++++++++++++++++++++++++----- server/types.go | 20 +++++++--- web/public/sw.js | 57 +++++++++++++++++++++++--- web/src/app/Connection.js | 6 ++- web/src/app/Poller.js | 7 +++- web/src/app/SubscriptionManager.js | 23 ++++++----- web/src/app/db.js | 2 +- web/src/app/events.js | 14 +++++++ web/src/components/hooks.js | 20 ++++++---- 11 files changed, 187 insertions(+), 67 deletions(-) create mode 100644 web/src/app/events.js diff --git a/server/errors.go b/server/errors.go index 950d23fc..e8f58d75 100644 --- a/server/errors.go +++ b/server/errors.go @@ -125,7 +125,7 @@ var ( errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} - errHTTPBadRequestSIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} + errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/message_cache.go b/server/message_cache.go index c00c67c8..396cd7a2 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -51,7 +51,7 @@ const ( content_type TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL, - deleted INT NOT NULL + event TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); @@ -69,58 +69,57 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted) + INSERT INTO messages (mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, event) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesLatestQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 ` selectMessagesDueQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted + SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id ` - selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1` - updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` - updateMessageDeletedQuery = `UPDATE messages SET deleted = 1 WHERE mid = ?` - selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` + selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1` + updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` + selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic` selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic` @@ -268,7 +267,7 @@ const ( //13 -> 14 migrate13To14AlterMessagesTableQuery = ` ALTER TABLE messages ADD COLUMN sequence_id TEXT NOT NULL DEFAULT(''); - ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0'); + ALTER TABLE messages ADD COLUMN event TEXT NOT NULL DEFAULT('message'); CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); ` ) @@ -381,7 +380,7 @@ func (c *messageCache) addMessages(ms []*message) error { } defer stmt.Close() for _, m := range ms { - if m.Event != messageEvent { + if m.Event != messageEvent && m.Event != messageDeleteEvent && m.Event != messageReadEvent { return errUnexpectedMessageType } published := m.Time <= time.Now().Unix() @@ -431,7 +430,7 @@ func (c *messageCache) addMessages(ms []*message) error { m.ContentType, m.Encoding, published, - m.Deleted, + m.Event, ) if err != nil { return err @@ -720,8 +719,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, sequenceID, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string - var deleted bool + var id, sequenceID, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding, event string err := rows.Scan( &id, &sequenceID, @@ -744,7 +742,7 @@ func readMessage(rows *sql.Rows) (*message, error) { &user, &contentType, &encoding, - &deleted, + &event, ) if err != nil { return nil, err @@ -782,7 +780,7 @@ func readMessage(rows *sql.Rows) (*message, error) { SequenceID: sequenceID, Time: timestamp, Expires: expires, - Event: messageEvent, + Event: event, Topic: topic, Message: msg, Title: title, @@ -796,7 +794,6 @@ func readMessage(rows *sql.Rows) (*message, error) { User: user, ContentType: contentType, Encoding: encoding, - Deleted: deleted, }, nil } diff --git a/server/server.go b/server/server.go index 635258fa..8aff7f7e 100644 --- a/server/server.go +++ b/server/server.go @@ -80,8 +80,9 @@ var ( wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`) authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`) publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) - sidRegex = topicRegex updatePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}$`) + markReadPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}/read$`) + sequenceIDRegex = topicRegex webConfigPath = "/config.js" webManifestPath = "/manifest.webmanifest" @@ -140,7 +141,6 @@ const ( firebaseControlTopic = "~control" // See Android if changed firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now) emptyMessageBody = "triggered" // Used when a message body is empty - deletedMessageBody = "deleted" // Used when a message is deleted newMessageBody = "New message" // Used in poll requests as generic message defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages @@ -550,6 +550,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodDelete && updatePathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleDelete))(w, r, v) + } else if r.Method == http.MethodPut && markReadPathRegex.MatchString(r.URL.Path) { + return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleMarkRead))(w, r, v) } else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) { @@ -921,10 +923,8 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor if e != nil { return e.With(t) } - // Create a delete message: empty body, same SequenceID, deleted flag set - m := newDefaultMessage(t.ID, deletedMessageBody) - m.SequenceID = sequenceID - m.Deleted = true + // Create a delete message with event type message_delete + m := newActionMessage(messageDeleteEvent, t.ID, sequenceID) m.Sender = v.IP() m.User = v.MaybeUserID() m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() @@ -951,6 +951,50 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor return s.writeJSON(w, m) } +func (s *Server) handleMarkRead(w http.ResponseWriter, r *http.Request, v *visitor) error { + t, err := fromContext[*topic](r, contextTopic) + if err != nil { + return err + } + vrate, err := fromContext[*visitor](r, contextRateVisitor) + if err != nil { + return err + } + if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { + return errHTTPTooManyRequestsLimitMessages.With(t) + } + sequenceID, e := s.sequenceIDFromPath(r.URL.Path) + if e != nil { + return e.With(t) + } + // Create a read message with event type message_read + m := newActionMessage(messageReadEvent, t.ID, sequenceID) + m.Sender = v.IP() + m.User = v.MaybeUserID() + m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() + // Publish to subscribers + if err := t.Publish(v, m); err != nil { + return err + } + // Send to Firebase for Android clients + if s.firebaseClient != nil { + go s.sendToFirebase(v, m) + } + // Send to web push endpoints + if s.config.WebPushPublicKey != "" { + go s.publishToWebPushEndpoints(v, m) + } + // Add to message cache + if err := s.messageCache.AddMessage(m); err != nil { + return err + } + logvrm(v, r, m).Tag(tagPublish).Debug("Marked message as read with sequence ID %s", sequenceID) + s.mu.Lock() + s.messages++ + s.mu.Unlock() + return s.writeJSON(w, m) +} + func (s *Server) sendToFirebase(v *visitor, m *message) { logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase") if err := s.firebaseClient.Send(v, m); err != nil { @@ -1017,10 +1061,10 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } else { sequenceID := readParam(r, "x-sequence-id", "sequence-id", "sid") if sequenceID != "" { - if sidRegex.MatchString(sequenceID) { + if sequenceIDRegex.MatchString(sequenceID) { m.SequenceID = sequenceID } else { - return false, false, "", "", "", false, errHTTPBadRequestSIDInvalid + return false, false, "", "", "", false, errHTTPBadRequestSequenceIDInvalid } } else { m.SequenceID = m.ID @@ -1767,8 +1811,8 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { // sequenceIDFromPath returns the sequence ID from a POST path like /mytopic/sequenceIdHere func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) { parts := strings.Split(path, "/") - if len(parts) != 3 { - return "", errHTTPBadRequestSIDInvalid + if len(parts) < 3 { + return "", errHTTPBadRequestSequenceIDInvalid } return parts[2], nil } diff --git a/server/types.go b/server/types.go index e9c0fdb8..5f9917d1 100644 --- a/server/types.go +++ b/server/types.go @@ -12,10 +12,12 @@ import ( // List of possible events const ( - openEvent = "open" - keepaliveEvent = "keepalive" - messageEvent = "message" - pollRequestEvent = "poll_request" + openEvent = "open" + keepaliveEvent = "keepalive" + messageEvent = "message" + messageDeleteEvent = "message_delete" + messageReadEvent = "message_read" + pollRequestEvent = "poll_request" ) const ( @@ -41,7 +43,6 @@ type message struct { PollID string `json:"poll_id,omitempty"` ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes - Deleted bool `json:"deleted,omitempty"` // True if message is marked as deleted Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting User string `json:"-"` // UserID of the uploader, used to associated attachments } @@ -149,6 +150,13 @@ func newPollRequestMessage(topic, pollID string) *message { return m } +// newActionMessage creates a new action message (message_delete or message_read) +func newActionMessage(event, topic, sequenceID string) *message { + m := newMessage(event, topic, "") + m.SequenceID = sequenceID + return m +} + func validMessageID(s string) bool { return util.ValidRandomString(s, messageIDLength) } @@ -227,7 +235,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) { } func (q *queryFilter) Pass(msg *message) bool { - if msg.Event != messageEvent { + if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageReadEvent { return true // filters only apply to messages } else if q.ID != "" && msg.ID != q.ID { return false diff --git a/web/public/sw.js b/web/public/sw.js index 38dbc9c1..7d6441e3 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -9,6 +9,7 @@ import { dbAsync } from "../src/app/db"; import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; import { messageWithSequenceId } from "../src/app/utils"; +import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_READ } from "../src/app/events"; /** * General docs for service workers and PWAs: @@ -62,11 +63,6 @@ const handlePushMessage = async (data) => { // Add notification to database await addNotification({ subscriptionId, message }); - // Don't show a notification for deleted messages - if (message.deleted) { - return; - } - // Broadcast the message to potentially play a sound broadcastChannel.postMessage(message); @@ -80,6 +76,51 @@ const handlePushMessage = async (data) => { ); }; +/** + * Handle a message_delete event: delete the notification from the database. + */ +const handlePushMessageDelete = async (data) => { + const { subscription_id: subscriptionId, message } = data; + const db = await dbAsync(); + + // Delete notification with the same sequence_id + const sequenceId = message.sequence_id; + if (sequenceId) { + console.log("[ServiceWorker] Deleting notification with sequenceId", { subscriptionId, sequenceId }); + await db.notifications.where({ subscriptionId, sequenceId }).delete(); + } + + // Update subscription last message id (for ?since=... queries) + await db.subscriptions.update(subscriptionId, { + last: message.id, + }); +}; + +/** + * Handle a message_read event: mark the notification as read. + */ +const handlePushMessageRead = async (data) => { + const { subscription_id: subscriptionId, message } = data; + const db = await dbAsync(); + + // Mark notification as read (set new = 0) + const sequenceId = message.sequence_id; + if (sequenceId) { + console.log("[ServiceWorker] Marking notification as read", { subscriptionId, sequenceId }); + await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 }); + } + + // Update subscription last message id (for ?since=... queries) + await db.subscriptions.update(subscriptionId, { + last: message.id, + }); + + // Update badge count + const badgeCount = await db.notifications.where({ new: 1 }).count(); + console.log("[ServiceWorker] Setting new app badge count", { badgeCount }); + self.navigator.setAppBadge?.(badgeCount); +}; + /** * Handle a received web push subscription expiring. */ @@ -114,8 +155,12 @@ const handlePushUnknown = async (data) => { * @param {object} data see server/types.go, type webPushPayload */ const handlePush = async (data) => { - if (data.event === "message") { + if (data.event === EVENT_MESSAGE) { await handlePushMessage(data); + } else if (data.event === EVENT_MESSAGE_DELETE) { + await handlePushMessageDelete(data); + } else if (data.event === EVENT_MESSAGE_READ) { + await handlePushMessageRead(data); } else if (data.event === "subscription_expiring") { await handlePushSubscriptionExpiring(data); } else { diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index 5358cdde..06043acc 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils"; +import { EVENT_OPEN, isNotificationEvent } from "./events"; const retryBackoffSeconds = [5, 10, 20, 30, 60, 120]; @@ -48,10 +49,11 @@ class Connection { console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`); try { const data = JSON.parse(event.data); - if (data.event === "open") { + if (data.event === EVENT_OPEN) { return; } - const relevantAndValid = data.event === "message" && "id" in data && "time" in data && "message" in data; + // Accept message, message_delete, and message_read events + const relevantAndValid = isNotificationEvent(data.event) && "id" in data && "time" in data; if (!relevantAndValid) { console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`); return; diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index aa0e6dba..56415b76 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -1,6 +1,7 @@ import api from "./Api"; import prefs from "./Prefs"; import subscriptionManager from "./SubscriptionManager"; +import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE } from "./events"; const delayMillis = 2000; // 2 seconds const intervalMillis = 300000; // 5 minutes @@ -55,7 +56,7 @@ class Poller { // Delete all existing notifications for which the latest notification is marked as deleted const deletedSequenceIds = Object.entries(latestBySequenceId) - .filter(([, notification]) => notification.deleted) + .filter(([, notification]) => notification.event === EVENT_MESSAGE_DELETE) .map(([sequenceId]) => sequenceId); if (deletedSequenceIds.length > 0) { console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds); @@ -65,7 +66,9 @@ class Poller { } // Add only the latest notification for each non-deleted sequence - const notificationsToAdd = Object.values(latestBySequenceId).filter((n) => !n.deleted); + const notificationsToAdd = Object + .values(latestBySequenceId) + .filter(n => n.event === EVENT_MESSAGE); if (notificationsToAdd.length > 0) { console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`); await subscriptionManager.addNotifications(subscription.id, notificationsToAdd); diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 772b30a7..2eecde28 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -3,6 +3,7 @@ import notifier from "./Notifier"; import prefs from "./Prefs"; import db from "./db"; import { messageWithSequenceId, topicUrl } from "./utils"; +import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_READ } from "./events"; class SubscriptionManager { constructor(dbImpl) { @@ -15,7 +16,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() })) ); } @@ -48,7 +49,7 @@ class SubscriptionManager { } async notify(subscriptionId, notification) { - if (notification.deleted) { + if (notification.event !== EVENT_MESSAGE) { return; } const subscription = await this.get(subscriptionId); @@ -83,7 +84,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null, + last: null }; await this.db.subscriptions.put(subscription); @@ -101,7 +102,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation, // May be null! + reservation // May be null! }); return local.id; @@ -174,7 +175,7 @@ class SubscriptionManager { /** Adds notification, or returns false if it already exists */ async addNotification(subscriptionId, notification) { const exists = await this.db.notifications.get(notification.id); - if (exists || notification.deleted) { + if (exists || notification.event === EVENT_MESSAGE_DELETE || notification.event === EVENT_MESSAGE_READ) { return false; } try { @@ -185,13 +186,13 @@ class SubscriptionManager { await this.db.notifications.add({ ...messageWithSequenceId(notification), subscriptionId, - new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); // FIXME consider put() for double tab // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id, + last: notification.id }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -207,7 +208,7 @@ class SubscriptionManager { const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId, + last: lastNotificationId }); } @@ -250,19 +251,19 @@ class SubscriptionManager { async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil, + mutedUntil }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName, + displayName }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation, + reservation }); } diff --git a/web/src/app/db.js b/web/src/app/db.js index 7e3c47e3..cb65c0b6 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -13,7 +13,7 @@ const createDatabase = (username) => { db.version(3).stores({ subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,sequenceId,subscriptionId,time,new,deleted,[subscriptionId+new],[subscriptionId+sequenceId]", + notifications: "&id,sequenceId,subscriptionId,time,new,[subscriptionId+new],[subscriptionId+sequenceId]", users: "&baseUrl,username", prefs: "&key" }); diff --git a/web/src/app/events.js b/web/src/app/events.js new file mode 100644 index 00000000..55dc262c --- /dev/null +++ b/web/src/app/events.js @@ -0,0 +1,14 @@ +// Event types for ntfy messages +// These correspond to the server event types in server/types.go + +export const EVENT_OPEN = "open"; +export const EVENT_KEEPALIVE = "keepalive"; +export const EVENT_MESSAGE = "message"; +export const EVENT_MESSAGE_DELETE = "message_delete"; +export const EVENT_MESSAGE_READ = "message_read"; +export const EVENT_POLL_REQUEST = "poll_request"; + +// Check if an event is a notification event (message, delete, or read) +export const isNotificationEvent = (event) => + event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_READ; + diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 5b50f0a8..5e271b35 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -12,6 +12,7 @@ import accountApi from "../app/AccountApi"; import { UnauthorizedError } from "../app/errors"; import notifier from "../app/Notifier"; import prefs from "../app/Prefs"; +import { EVENT_MESSAGE_DELETE, EVENT_MESSAGE_READ } from "../app/events"; /** * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection @@ -53,13 +54,18 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop // Note: This logic is duplicated in the Android app in SubscriberService::onNotificationReceived() // and FirebaseService::handleMessage(). - // Delete existing notification with same sequenceId, if any - const sequenceId = notification.sequence_id || notification.id; - if (sequenceId) { - await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId); - } - // Add notification to database - if (!notification.deleted) { + if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) { + // Handle delete: remove notification from database + await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id); + } else if (notification.event === EVENT_MESSAGE_READ && notification.sequence_id) { + // Handle read: mark notification as read + await subscriptionManager.markNotificationReadBySequenceId(subscriptionId, notification.sequence_id); + } else { + // Regular message: delete existing and add new + const sequenceId = notification.sequence_id || notification.id; + if (sequenceId) { + await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId); + } const added = await subscriptionManager.addNotification(subscriptionId, notification); if (added) { await subscriptionManager.notify(subscriptionId, notification); From 37d71051de90ee02c978ff17e26fe7da7fb49e0e Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 8 Jan 2026 20:57:55 -0500 Subject: [PATCH 352/378] Refine --- server/server.go | 75 +++++++++++++--------------------------- server/server_webpush.go | 2 +- server/types.go | 11 ++++++ 3 files changed, 36 insertions(+), 52 deletions(-) diff --git a/server/server.go b/server/server.go index 8aff7f7e..e111434d 100644 --- a/server/server.go +++ b/server/server.go @@ -879,7 +879,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } minc(metricMessagesPublishedSuccess) - return s.writeJSON(w, m) + return s.writeJSON(w, m.forJSON()) } func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error { @@ -908,50 +908,14 @@ func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v * } func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { - t, err := fromContext[*topic](r, contextTopic) - if err != nil { - return err - } - vrate, err := fromContext[*visitor](r, contextRateVisitor) - if err != nil { - return err - } - if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { - return errHTTPTooManyRequestsLimitMessages.With(t) - } - sequenceID, e := s.sequenceIDFromPath(r.URL.Path) - if e != nil { - return e.With(t) - } - // Create a delete message with event type message_delete - m := newActionMessage(messageDeleteEvent, t.ID, sequenceID) - m.Sender = v.IP() - m.User = v.MaybeUserID() - m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() - // Publish to subscribers - if err := t.Publish(v, m); err != nil { - return err - } - // Send to Firebase for Android clients - if s.firebaseClient != nil { - go s.sendToFirebase(v, m) - } - // Send to web push endpoints - if s.config.WebPushPublicKey != "" { - go s.publishToWebPushEndpoints(v, m) - } - // Add to message cache - if err := s.messageCache.AddMessage(m); err != nil { - return err - } - logvrm(v, r, m).Tag(tagPublish).Debug("Deleted message with sequence ID %s", sequenceID) - s.mu.Lock() - s.messages++ - s.mu.Unlock() - return s.writeJSON(w, m) + return s.handleActionMessage(w, r, v, messageDeleteEvent, s.sequenceIDFromPath) } func (s *Server) handleMarkRead(w http.ResponseWriter, r *http.Request, v *visitor) error { + return s.handleActionMessage(w, r, v, messageReadEvent, s.sequenceIDFromMarkReadPath) +} + +func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v *visitor, event string, extractSequenceID func(string) (string, *errHTTP)) error { t, err := fromContext[*topic](r, contextTopic) if err != nil { return err @@ -963,12 +927,12 @@ func (s *Server) handleMarkRead(w http.ResponseWriter, r *http.Request, v *visit if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { return errHTTPTooManyRequestsLimitMessages.With(t) } - sequenceID, e := s.sequenceIDFromPath(r.URL.Path) + sequenceID, e := extractSequenceID(r.URL.Path) if e != nil { return e.With(t) } - // Create a read message with event type message_read - m := newActionMessage(messageReadEvent, t.ID, sequenceID) + // Create an action message with the given event type + m := newActionMessage(event, t.ID, sequenceID) m.Sender = v.IP() m.User = v.MaybeUserID() m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() @@ -988,11 +952,11 @@ func (s *Server) handleMarkRead(w http.ResponseWriter, r *http.Request, v *visit if err := s.messageCache.AddMessage(m); err != nil { return err } - logvrm(v, r, m).Tag(tagPublish).Debug("Marked message as read with sequence ID %s", sequenceID) + logvrm(v, r, m).Tag(tagPublish).Debug("Published %s for sequence ID %s", event, sequenceID) s.mu.Lock() s.messages++ s.mu.Unlock() - return s.writeJSON(w, m) + return s.writeJSON(w, m.forJSON()) } func (s *Server) sendToFirebase(v *visitor, m *message) { @@ -1384,7 +1348,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(msg); err != nil { + if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { return "", err } return buf.String(), nil @@ -1395,10 +1359,10 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v * func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(msg); err != nil { + if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { return "", err } - if msg.Event != messageEvent { + if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageReadEvent { return fmt.Sprintf("event: %s\ndata: %s\n", msg.Event, buf.String()), nil // Browser's .onmessage() does not fire on this! } return fmt.Sprintf("data: %s\n", buf.String()), nil @@ -1808,7 +1772,7 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { return topics, parts[1], nil } -// sequenceIDFromPath returns the sequence ID from a POST path like /mytopic/sequenceIdHere +// sequenceIDFromPath returns the sequence ID from a path like /mytopic/sequenceIdHere func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) { parts := strings.Split(path, "/") if len(parts) < 3 { @@ -1817,6 +1781,15 @@ func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) { return parts[2], nil } +// sequenceIDFromMarkReadPath returns the sequence ID from a path like /mytopic/sequenceIdHere/read +func (s *Server) sequenceIDFromMarkReadPath(path string) (string, *errHTTP) { + parts := strings.Split(path, "/") + if len(parts) < 4 || parts[3] != "read" { + return "", errHTTPBadRequestSequenceIDInvalid + } + return parts[2], nil +} + // topicsFromIDs returns the topics with the given IDs, creating them if they don't exist. func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) { s.mu.Lock() diff --git a/server/server_webpush.go b/server/server_webpush.go index 526e06f2..d3f09bd9 100644 --- a/server/server_webpush.go +++ b/server/server_webpush.go @@ -89,7 +89,7 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) { return } log.Tag(tagWebPush).With(v, m).Debug("Publishing web push message to %d subscribers", len(subscriptions)) - payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m)) + payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m.forJSON())) if err != nil { log.Tag(tagWebPush).Err(err).With(v, m).Warn("Unable to marshal expiring payload") return diff --git a/server/types.go b/server/types.go index 5f9917d1..ec682133 100644 --- a/server/types.go +++ b/server/types.go @@ -65,6 +65,17 @@ func (m *message) Context() log.Context { return fields } +// forJSON returns a copy of the message suitable for JSON output. +// It clears the SequenceID if it equals the ID to reduce redundancy. +func (m *message) forJSON() *message { + if m.SequenceID == m.ID { + clone := *m + clone.SequenceID = "" + return &clone + } + return m +} + type attachment struct { Name string `json:"name"` Type string `json:"type,omitempty"` From 13768d42896d9583f540b4e3c86bd2fc7743e1f0 Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Fri, 9 Jan 2026 04:25:51 +0100 Subject: [PATCH 353/378] Translated using Weblate (Macedonian) Currently translated at 16.4% (67 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index 56417357..aeaff7cb 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -58,5 +58,12 @@ "nav_upgrade_banner_label": "Надградете на ntfy Pro", "nav_upgrade_banner_description": "Резервирајте теми, повеќе пораки и е-пораки и поголеми прилози", "alert_notification_permission_required_title": "Известувањата се исклучени", - "alert_notification_permission_required_description": "Дајте му дозвола на вашиот прелистувач да прикажува известувања" + "alert_notification_permission_required_description": "Дајте му дозвола на вашиот прелистувач да прикажува известувања", + "nav_button_muted": "Известувањата се загушени", + "alert_not_supported_title": "Известувањата не се поддржани", + "alert_not_supported_description": "Известувањата не се поддржани во вашиот прелистувач", + "alert_not_supported_context_description": "Известувањата се поддржани само преку HTTPS. Ова е ограничување на Notifications API .", + "notifications_list": "Список на известувања", + "notifications_list_item": "Известување", + "notifications_mark_read": "Означи како прочитано" } From 7bc8edf7c3a6736dcf17d53bc55b2a306cf42920 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 10 Jan 2026 20:47:59 -0500 Subject: [PATCH 354/378] Bump, release notes --- docs/releases.md | 56 ++++---- go.mod | 26 ++-- go.sum | 27 ++++ web/package-lock.json | 293 ++++++++++++++++++++++++------------------ 4 files changed, 242 insertions(+), 160 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index afcd849a..8df4d1b4 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -4,14 +4,41 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Current stable releases -| Component | Version | Release date | -|------------------------------------------|---------|--------------| -| ntfy server | v2.15.0 | Nov 16, 2025 | -| ntfy Android app (_is being rolled out_) | v1.20.0 | Dec 28, 2025 | -| ntfy iOS app | v1.3 | Nov 26, 2023 | +| Component | Version | Release date | +|------------------|---------|--------------| +| ntfy server | v2.15.0 | Nov 16, 2025 | +| ntfy Android app | v1.21.1 | Jan 6, 2025 | +| ntfy iOS app | v1.3 | Nov 26, 2023 | Please check out the release notes for [upcoming releases](#not-released-yet) below. +## ntfy Android app v1.21.1 +Released January 6, 2026 + +This is the first feature release in a long time. After all the SDK updates, fixes to comply with the Google Play policies +and the framework updates, this release ships a lot of highly requested features: Sending messages through the app (WhatsApp-style), +support for passing headers to your proxy, an in-app language switcher, and more. + +If you are waiting for a feature, please 👍 the corresponding [GitHub issue](https://github.com/binwiederhier/ntfy/issues?q=is%3Aissue%20state%3Aopen%20sort%3Areactions-%2B1-desc). +If you like ntfy, please consider purchasing [ntfy Pro](https://ntfy.sh/app) to support us. + +**Features:** + +* Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) +* Define custom HTTP headers to support authenticated proxies, tunnels and SSO ([ntfy-android#116](https://github.com/binwiederhier/ntfy-android/issues/116), [#1018](https://github.com/binwiederhier/ntfy/issues/1018), [ntfy-android#132](https://github.com/binwiederhier/ntfy-android/pull/132), [ntfy-android#146](https://github.com/binwiederhier/ntfy-android/pull/146), thanks to [@CrazyWolf13](https://github.com/CrazyWolf13)) +* Implement UnifiedPush "raise to foreground" requirement ([ntfy-android#98](https://github.com/binwiederhier/ntfy-android/pull/98), [ntfy-android#148](https://github.com/binwiederhier/ntfy-android/pull/148), thanks to [@p1gp1g](https://github.com/p1gp1g)) +* Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting) +* Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting) +* Support for port and display name in [ntfy://](subscribe/phone.md#ntfy-links) links ([ntfy-android#130](https://github.com/binwiederhier/ntfy-android/pull/130), thanks to [@godovski](https://github.com/godovski)) + +**Bug fixes + maintenance:** + +* Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco)) +* Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel)) +* Fix crash in user add dialog (onAddUser) +* Fix ForegroundServiceDidNotStartInTimeException (attempt 2, see [#1520](https://github.com/binwiederhier/ntfy/issues/1520)) +* Hide "Exact alarms" setting if battery optimization exemption has been granted ([#1456](https://github.com/binwiederhier/ntfy/issues/1456), thanks for reporting [@HappyLer](https://github.com/HappyLer)) + ## ntfy Android app v1.20.0 Released December 28, 2025 @@ -1572,21 +1599,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy Android app v1.21.1-rc1 (IN TESTING) - -**Features:** - -* Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144)) -* Define custom HTTP headers to support authenticated proxies, tunnels and SSO ([ntfy-android#116](https://github.com/binwiederhier/ntfy-android/issues/116), [#1018](https://github.com/binwiederhier/ntfy/issues/1018), [ntfy-android#132](https://github.com/binwiederhier/ntfy-android/pull/132), [ntfy-android#146](https://github.com/binwiederhier/ntfy-android/pull/146), thanks to [@CrazyWolf13](https://github.com/CrazyWolf13)) -* Implement UnifiedPush "raise to foreground" requirement ([ntfy-android#98](https://github.com/binwiederhier/ntfy-android/pull/98), [ntfy-android#148](https://github.com/binwiederhier/ntfy-android/pull/148), thanks to [@p1gp1g](https://github.com/p1gp1g)) -* Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting) -* Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting) -* Support for port and display name in [ntfy://](subscribe/phone.md#ntfy-links) links ([ntfy-android#130](https://github.com/binwiederhier/ntfy-android/pull/130), thanks to [@godovski](https://github.com/godovski)) - -**Bug fixes + maintenance:** - -* Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco)) -* Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel)) -* Fix crash in user add dialog (onAddUser) -* Fix ForegroundServiceDidNotStartInTimeException (attempt 2, see [#1520](https://github.com/binwiederhier/ntfy/issues/1520)) -* Hide "Exact alarms" setting if battery optimization exemption has been granted ([#1456](https://github.com/binwiederhier/ntfy/issues/1456), thanks for reporting [@HappyLer](https://github.com/HappyLer)) +Nothing to see here yet. \ No newline at end of file diff --git a/go.mod b/go.mod index e6d91101..aa88fd68 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,22 @@ toolchain go1.24.5 require ( cloud.google.com/go/firestore v1.20.0 // indirect - cloud.google.com/go/storage v1.58.0 // indirect + cloud.google.com/go/storage v1.59.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 github.com/gabriel-vasile/mimetype v1.4.12 github.com/gorilla/websocket v1.5.3 - github.com/mattn/go-sqlite3 v1.14.32 + github.com/mattn/go-sqlite3 v1.14.33 github.com/olebedev/when v1.1.0 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 golang.org/x/crypto v0.46.0 golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 - golang.org/x/term v0.38.0 + golang.org/x/term v0.39.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.258.0 + google.golang.org/api v0.259.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -35,7 +35,7 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.23.2 github.com/stripe/stripe-go/v74 v74.30.0 - golang.org/x/text v0.32.0 + golang.org/x/text v0.33.0 ) require ( @@ -45,7 +45,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.7.0 // indirect + cloud.google.com/go/longrunning v0.8.0 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect github.com/AlekSi/pointer v1.2.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect @@ -69,14 +69,14 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -93,12 +93,12 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect - google.golang.org/grpc v1.77.0 // indirect + google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index babf7049..72888b0f 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,14 @@ cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3Q cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= +cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= @@ -98,6 +102,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU= +github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -114,6 +120,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -133,6 +141,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -227,6 +237,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -238,6 +250,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -251,6 +265,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -265,16 +281,27 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc= google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww= +google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ= +google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 h1:stRtB2UVzFOWnorVuwF0BVVEjQ3AN6SjHWdg811UIQM= google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b h1:kqShdsddZrS6q+DGBCA73CzHsKDu5vW4qw78tFnbVvY= +google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:gw1DtiPCt5uh/HV9STVEeaO00S5ATsJiJ2LsZV8lcDI= google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/web/package-lock.json b/web/package-lock.json index 789b95f7..a1bba16c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2198,9 +2198,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2711,9 +2711,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2798,9 +2798,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "cpu": [ "arm" ], @@ -2812,9 +2812,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "cpu": [ "arm64" ], @@ -2826,9 +2826,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "cpu": [ "arm64" ], @@ -2840,9 +2840,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "cpu": [ "x64" ], @@ -2854,9 +2854,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "cpu": [ "arm64" ], @@ -2868,9 +2868,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "cpu": [ "x64" ], @@ -2882,9 +2882,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "cpu": [ "arm" ], @@ -2896,9 +2896,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "cpu": [ "arm" ], @@ -2910,9 +2910,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -2924,9 +2924,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "cpu": [ "arm64" ], @@ -2938,9 +2938,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", "cpu": [ "loong64" ], @@ -2952,9 +2966,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", "cpu": [ "ppc64" ], @@ -2966,9 +2994,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", "cpu": [ "riscv64" ], @@ -2980,9 +3008,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "cpu": [ "riscv64" ], @@ -2994,9 +3022,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "cpu": [ "s390x" ], @@ -3008,9 +3036,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -3022,9 +3050,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -3035,10 +3063,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", "cpu": [ "arm64" ], @@ -3050,9 +3092,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "cpu": [ "arm64" ], @@ -3064,9 +3106,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "cpu": [ "ia32" ], @@ -3078,9 +3120,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "cpu": [ "x64" ], @@ -3092,9 +3134,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "cpu": [ "x64" ], @@ -3206,9 +3248,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", "peer": true, "dependencies": { @@ -3566,9 +3608,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -3660,9 +3702,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3781,9 +3823,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "version": "1.0.30001763", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", "dev": true, "funding": [ { @@ -4872,9 +4914,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4969,9 +5011,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -7329,12 +7371,12 @@ } }, "node_modules/react-router": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -7344,13 +7386,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1", - "react-router": "6.30.2" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -7586,9 +7628,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", "dependencies": { @@ -7602,28 +7644,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, From 090cd720c12d7749c212ceace312397246ea64df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cant=C3=BA?= Date: Sat, 10 Jan 2026 22:03:41 -0500 Subject: [PATCH 355/378] fix typos --- docs/develop.md | 2 +- docs/publish/template-functions.md | 2 +- docs/subscribe/phone.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/develop.md b/docs/develop.md index 4ddff5ec..ecf35e1d 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -441,6 +441,6 @@ To have instant notifications/better notification delivery when using firebase, 1. In XCode, find the NTFY app target. **Not** the NSE app target. 1. Find the Asset/ folder in the project navigator 1. Drag the `GoogleService-Info.plist` file into the Asset/ folder that you get from the firebase console. It can be - found in the "Project settings" > "General" > "Your apps" with a button labled "GoogleService-Info.plist" + found in the "Project settings" > "General" > "Your apps" with a button labeled "GoogleService-Info.plist" After that, you should be all set! diff --git a/docs/publish/template-functions.md b/docs/publish/template-functions.md index 79848080..53026627 100644 --- a/docs/publish/template-functions.md +++ b/docs/publish/template-functions.md @@ -1174,7 +1174,7 @@ keys $myDict | sortAlpha ``` When supplying multiple dictionaries, the keys will be concatenated. Use the `uniq` -function along with `sortAlpha` to get a unqiue, sorted list of keys. +function along with `sortAlpha` to get a unique, sorted list of keys. ``` keys $myDict $myOtherDict | uniq | sortAlpha diff --git a/docs/subscribe/phone.md b/docs/subscribe/phone.md index 3015be88..602a0d45 100644 --- a/docs/subscribe/phone.md +++ b/docs/subscribe/phone.md @@ -100,7 +100,7 @@ The reason for this is [Firebase Cloud Messaging (FCM)](https://firebase.google. notifications. Firebase is overall pretty bad at delivering messages in time, but on Android, most apps are stuck with it. The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in the Google Play flavor of the app. -It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor. +It won't use Firebase for any self-hosted servers, and not at all in the F-Droid flavor. ## Share to topic _Supported on:_ :material-android: From a7603d1dfb88e00441e5eb306f6c28b7c0d7a326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Sun, 11 Jan 2026 01:48:53 +0100 Subject: [PATCH 356/378] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hans/ --- web/public/static/langs/zh_Hans.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json index d421bc35..fbbc1a2f 100644 --- a/web/public/static/langs/zh_Hans.json +++ b/web/public/static/langs/zh_Hans.json @@ -182,7 +182,7 @@ "subscribe_dialog_subscribe_topic_placeholder": "主题名,例如 phil_alerts", "notifications_no_subscriptions_description": "单击 \"{{linktext}}\" 链接以创建或订阅主题。之后,您可以使用 PUT 或 POST 发送消息,您将在这里收到通知。", "publish_dialog_attachment_limits_file_reached": "超过 {{fileSizeLimit}} 文件限制", - "publish_dialog_title_placeholder": "主题标题,例如 磁盘空间告警", + "publish_dialog_title_placeholder": "通知标题,如磁盘空间告警", "publish_dialog_email_label": "电子邮件", "publish_dialog_button_send": "发送", "publish_dialog_checkbox_markdown": "格式化为 Markdown", @@ -206,7 +206,7 @@ "publish_dialog_tags_placeholder": "英文逗号分隔的标签列表,例如 warning, srv1-backup", "publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅文档。", "subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易被猜中的名字。订阅后,您可以使用 PUT/POST 通知。", - "publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(仅限英语)", + "publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}} 或 {{naturalLanguage}} (仅限英语)", "account_usage_basis_ip_description": "此账户的使用统计信息和限制基于您的 IP 地址,因此可能会与其他用户共享。上面显示的限制是基于现有速率限制的近似值。", "account_usage_cannot_create_portal_session": "无法打开计费门户", "account_delete_title": "删除账户", From 88016a2549ee7c855fd036ce438333d59bbe3df8 Mon Sep 17 00:00:00 2001 From: Shoshin Akamine Date: Sun, 11 Jan 2026 04:31:27 +0100 Subject: [PATCH 357/378] Translated using Weblate (Japanese) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/ --- web/public/static/langs/ja.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json index ebd76b54..1f672b2c 100644 --- a/web/public/static/langs/ja.json +++ b/web/public/static/langs/ja.json @@ -14,7 +14,7 @@ "publish_dialog_title_no_topic": "通知を送信", "publish_dialog_progress_uploading": "アップロード中…", "publish_dialog_progress_uploading_detail": "アップロード中 {{loaded}}/{{total}} ({{percent}}%) …", - "publish_dialog_message_published": "通知を送信しました", + "publish_dialog_message_published": "通知送信済み", "publish_dialog_title_label": "タイトル", "publish_dialog_filename_label": "ファイル名", "subscribe_dialog_login_description": "このトピックはログインする必要があります。ユーザー名とパスワードを入力してください。", @@ -69,10 +69,10 @@ "publish_dialog_attachment_limits_quota_reached": "クォータを超過しました、残り{{remainingBytes}}", "publish_dialog_priority_high": "優先度 高", "publish_dialog_topic_placeholder": "トピック名の例 phil_alerts", - "publish_dialog_title_placeholder": "通知タイトル 例: ディスクスペース警告", + "publish_dialog_title_placeholder": "通知タイトル、例: ディスクスペース警告", "publish_dialog_message_placeholder": "メッセージ本文を入力してください", "publish_dialog_tags_label": "タグ", - "publish_dialog_tags_placeholder": "コンマ区切りでタグを列挙してください 例: warning, srv1-backup", + "publish_dialog_tags_placeholder": "コンマ区切りでタグを列挙してください、例: warning, srv1-backup", "publish_dialog_topic_label": "トピック名", "publish_dialog_delay_label": "遅延", "publish_dialog_click_placeholder": "通知をクリックしたときに開くURL", From f581af6d27765ae8d6fc3bb982e326947cb1f6dd Mon Sep 17 00:00:00 2001 From: "Kristijan \\\"Fremen\\\" Velkovski" Date: Sun, 11 Jan 2026 02:41:56 +0100 Subject: [PATCH 358/378] Translated using Weblate (Macedonian) Currently translated at 23.0% (94 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/mk/ --- web/public/static/langs/mk.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/mk.json b/web/public/static/langs/mk.json index aeaff7cb..bb19fe1a 100644 --- a/web/public/static/langs/mk.json +++ b/web/public/static/langs/mk.json @@ -65,5 +65,32 @@ "alert_not_supported_context_description": "Известувањата се поддржани само преку HTTPS. Ова е ограничување на Notifications API .", "notifications_list": "Список на известувања", "notifications_list_item": "Известување", - "notifications_mark_read": "Означи како прочитано" + "notifications_mark_read": "Означи како прочитано", + "publish_dialog_attached_file_filename_placeholder": "Име на фајл за прилог", + "notifications_attachment_file_app": "Фајл со апликација за Android", + "notifications_attachment_file_document": "друг документ", + "alert_notification_permission_required_button": "Дајте дозвола сега", + "alert_notification_permission_denied_title": "Известувањата се блокирани", + "alert_notification_permission_denied_description": "Ве молиме повторно овозможете ги во вашиот пребарувач", + "alert_notification_ios_install_required_title": "Потребна е инсталација на iOS", + "alert_notification_ios_install_required_description": "Кликнете на иконата Сподели и Додај на почетниот екран за да овозможите известувања на iOS", + "notifications_delete": "Избриши", + "notifications_copied_to_clipboard": "Копирано во таблата со исечоци", + "notifications_tags": "Ознаки", + "notifications_priority_x": "Приоритет {{приоритет}}", + "notifications_new_indicator": "Ново известување", + "notifications_attachment_image": "Слика од прилог", + "notifications_attachment_copy_url_title": "Копирај URL-адресата на прилогот во таблата со исечоци", + "notifications_attachment_open_title": "Оди на {{url}}", + "notifications_attachment_open_button": "Отвори го прилогот", + "notifications_attachment_link_expires": "линкот истекува {{date}}", + "notifications_attachment_link_expired": "линкот за преземање е истечен", + "notifications_attachment_file_image": "слика фајл", + "notifications_attachment_file_video": "видео фајл", + "notifications_attachment_file_audio": "аудио фајл", + "notifications_click_copy_url_button": "Копирај линк", + "notifications_click_open_button": "Отвори линк", + "notifications_actions_open_url_title": "Оди на {{url}}", + "notifications_actions_not_supported": "Дејството не е поддржано во веб-апликацијата", + "notifications_actions_http_request_title": "Испрати HTTP {{method}} на {{url}}" } From 8960459520b87b0bd3bece181388021ae2c9a599 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 11 Jan 2026 09:26:44 -0500 Subject: [PATCH 359/378] Changelog --- docs/releases.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index 8df4d1b4..ed65d8a4 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1599,4 +1599,15 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -Nothing to see here yet. \ No newline at end of file +### ntfy Android app v1.22.x (UNRELEASED) + +**Features:** + +* Support for self-signed certs and client certs for mTLS (#215, #530, ntfy-android#149) + +**Bug fixes + maintenance:** + +* Use server-specific user for attachment downloads (#1529, thanks to @ManInDark for reporting) +* Fix crash in sharing dialog (thanks to @rogeliodh) +* Fix crash when exiting multi-delete in detail view +* Fix potential crashes with icon downloader and backuper From c0a5a1fb3530c711ccc972f066951ece7928907c Mon Sep 17 00:00:00 2001 From: cyberboh Date: Mon, 12 Jan 2026 13:28:12 +0100 Subject: [PATCH 360/378] Translated using Weblate (Indonesian) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/ --- web/public/static/langs/id.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json index c63d9a12..39c6d1af 100644 --- a/web/public/static/langs/id.json +++ b/web/public/static/langs/id.json @@ -50,10 +50,10 @@ "publish_dialog_progress_uploading": "Mengunggah …", "notifications_more_details": "Untuk informasi lanjut, lihat situs web atau dokumentasi.", "publish_dialog_progress_uploading_detail": "Mengunggah {{loaded}}/{{total}} ({{percent}}%) …", - "publish_dialog_message_published": "Notifikasi dipublikasi", + "publish_dialog_message_published": "Notifikasi dipublikasikan", "notifications_loading": "Memuat notifikasi …", "publish_dialog_base_url_label": "URL Layanan", - "publish_dialog_title_placeholder": "Judul notifikasi, mis. Peringatan ruang disk", + "publish_dialog_title_placeholder": "Judul notifikasi, contoh: Peringatan ruang penyimpanan disk", "publish_dialog_tags_label": "Tanda", "publish_dialog_priority_label": "Prioritas", "publish_dialog_base_url_placeholder": "URL Layanan, mis. https://contoh.com", @@ -73,10 +73,10 @@ "publish_dialog_topic_label": "Nama topik", "publish_dialog_message_placeholder": "Tulis pesan di sini", "publish_dialog_click_label": "Klik URL", - "publish_dialog_tags_placeholder": "Daftar label yang dipisah dengan tanda koma, contoh: peringatan, cadangan-srv1", + "publish_dialog_tags_placeholder": "Daftar label yang dipisahkan koma, contoh: peringatan, cadangan-srv1", "publish_dialog_click_placeholder": "URL yang dibuka ketika notifikasi diklik", "publish_dialog_email_label": "Email", - "publish_dialog_email_placeholder": "Alamat untuk meneruskan notifikasi, mis. andi@contoh.com", + "publish_dialog_email_placeholder": "Alamat untuk meneruskan notifikasi, contoh: phil@example.com", "publish_dialog_attach_label": "URL Lampiran", "publish_dialog_filename_label": "Nama File", "publish_dialog_filename_placeholder": "Nama file lampiran", From a3c16d81f88109196b1ffee9af71fe228c676490 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 13 Jan 2026 16:31:13 -0500 Subject: [PATCH 361/378] Rename to clear --- go.sum | 39 +++++++++++++++++++++++++++--- server/message_cache.go | 8 +++--- server/server.go | 27 +++++++-------------- server/types.go | 8 +++--- web/public/sw.js | 10 ++++---- web/src/app/Connection.js | 2 +- web/src/app/Poller.js | 4 +-- web/src/app/SubscriptionManager.js | 22 ++++++++--------- web/src/app/db.js | 2 +- web/src/app/events.js | 6 ++--- web/src/components/hooks.js | 4 +-- 11 files changed, 76 insertions(+), 56 deletions(-) diff --git a/go.sum b/go.sum index 0915f1cb..241b5556 100644 --- a/go.sum +++ b/go.sum @@ -6,20 +6,22 @@ cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute v1.53.0 h1:dILGanjePNsYfZVYYv6K0d4+IPnKX1gn84Fk8jDPNvs= -cloud.google.com/go/compute v1.53.0/go.mod h1:zdogTa7daHhEtEX92+S5IARtQmi/RNVPUfoI8Jhl8Do= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo= cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= @@ -30,6 +32,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= @@ -47,14 +51,19 @@ github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/Buvy github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -79,6 +88,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -91,6 +104,14 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= @@ -104,6 +125,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= @@ -113,13 +135,17 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= @@ -138,6 +164,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGN go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= @@ -146,6 +174,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -231,9 +261,10 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ= google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 h1:wFALHMUiWKkK/x6rSxm79KpSnUyh7ks2E+mel670Dc4= @@ -249,6 +280,8 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/message_cache.go b/server/message_cache.go index 396cd7a2..ec1a395e 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -117,9 +117,9 @@ const ( WHERE time <= ? AND published = 0 ORDER BY time, id ` - selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1` - updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` - selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` + selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1` + updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?` + selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic` selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic` @@ -380,7 +380,7 @@ func (c *messageCache) addMessages(ms []*message) error { } defer stmt.Close() for _, m := range ms { - if m.Event != messageEvent && m.Event != messageDeleteEvent && m.Event != messageReadEvent { + if m.Event != messageEvent && m.Event != messageDeleteEvent && m.Event != messageClearEvent { return errUnexpectedMessageType } published := m.Time <= time.Now().Unix() diff --git a/server/server.go b/server/server.go index e111434d..7ef43275 100644 --- a/server/server.go +++ b/server/server.go @@ -81,7 +81,7 @@ var ( authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`) publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) updatePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}$`) - markReadPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}/read$`) + clearPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}/(read|clear)$`) sequenceIDRegex = topicRegex webConfigPath = "/config.js" @@ -550,8 +550,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodDelete && updatePathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleDelete))(w, r, v) - } else if r.Method == http.MethodPut && markReadPathRegex.MatchString(r.URL.Path) { - return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleMarkRead))(w, r, v) + } else if r.Method == http.MethodPut && clearPathRegex.MatchString(r.URL.Path) { + return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleClear))(w, r, v) } else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) { return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v) } else if r.Method == http.MethodGet && jsonPathRegex.MatchString(r.URL.Path) { @@ -908,14 +908,14 @@ func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v * } func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { - return s.handleActionMessage(w, r, v, messageDeleteEvent, s.sequenceIDFromPath) + return s.handleActionMessage(w, r, v, messageDeleteEvent) } -func (s *Server) handleMarkRead(w http.ResponseWriter, r *http.Request, v *visitor) error { - return s.handleActionMessage(w, r, v, messageReadEvent, s.sequenceIDFromMarkReadPath) +func (s *Server) handleClear(w http.ResponseWriter, r *http.Request, v *visitor) error { + return s.handleActionMessage(w, r, v, messageClearEvent) } -func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v *visitor, event string, extractSequenceID func(string) (string, *errHTTP)) error { +func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v *visitor, event string) error { t, err := fromContext[*topic](r, contextTopic) if err != nil { return err @@ -927,7 +927,7 @@ func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v * if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { return errHTTPTooManyRequestsLimitMessages.With(t) } - sequenceID, e := extractSequenceID(r.URL.Path) + sequenceID, e := s.sequenceIDFromPath(r.URL.Path) if e != nil { return e.With(t) } @@ -1362,7 +1362,7 @@ func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *v if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil { return "", err } - if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageReadEvent { + if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageClearEvent { return fmt.Sprintf("event: %s\ndata: %s\n", msg.Event, buf.String()), nil // Browser's .onmessage() does not fire on this! } return fmt.Sprintf("data: %s\n", buf.String()), nil @@ -1781,15 +1781,6 @@ func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) { return parts[2], nil } -// sequenceIDFromMarkReadPath returns the sequence ID from a path like /mytopic/sequenceIdHere/read -func (s *Server) sequenceIDFromMarkReadPath(path string) (string, *errHTTP) { - parts := strings.Split(path, "/") - if len(parts) < 4 || parts[3] != "read" { - return "", errHTTPBadRequestSequenceIDInvalid - } - return parts[2], nil -} - // topicsFromIDs returns the topics with the given IDs, creating them if they don't exist. func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) { s.mu.Lock() diff --git a/server/types.go b/server/types.go index ec682133..6464222f 100644 --- a/server/types.go +++ b/server/types.go @@ -16,7 +16,7 @@ const ( keepaliveEvent = "keepalive" messageEvent = "message" messageDeleteEvent = "message_delete" - messageReadEvent = "message_read" + messageClearEvent = "message_clear" pollRequestEvent = "poll_request" ) @@ -33,7 +33,7 @@ type message struct { Event string `json:"event"` // One of the above Topic string `json:"topic"` Title string `json:"title,omitempty"` - Message string `json:"message"` // Allow empty message body + Message string `json:"message,omitempty"` Priority int `json:"priority,omitempty"` Tags []string `json:"tags,omitempty"` Click string `json:"click,omitempty"` @@ -161,7 +161,7 @@ func newPollRequestMessage(topic, pollID string) *message { return m } -// newActionMessage creates a new action message (message_delete or message_read) +// newActionMessage creates a new action message (message_delete or message_clear) func newActionMessage(event, topic, sequenceID string) *message { m := newMessage(event, topic, "") m.SequenceID = sequenceID @@ -246,7 +246,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) { } func (q *queryFilter) Pass(msg *message) bool { - if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageReadEvent { + if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageClearEvent { return true // filters only apply to messages } else if q.ID != "" && msg.ID != q.ID { return false diff --git a/web/public/sw.js b/web/public/sw.js index 7d6441e3..0dc1afef 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -9,7 +9,7 @@ import { dbAsync } from "../src/app/db"; import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; import { messageWithSequenceId } from "../src/app/utils"; -import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_READ } from "../src/app/events"; +import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../src/app/events"; /** * General docs for service workers and PWAs: @@ -97,9 +97,9 @@ const handlePushMessageDelete = async (data) => { }; /** - * Handle a message_read event: mark the notification as read. + * Handle a message_clear event: clear/dismiss the notification. */ -const handlePushMessageRead = async (data) => { +const handlePushMessageClear = async (data) => { const { subscription_id: subscriptionId, message } = data; const db = await dbAsync(); @@ -159,8 +159,8 @@ const handlePush = async (data) => { await handlePushMessage(data); } else if (data.event === EVENT_MESSAGE_DELETE) { await handlePushMessageDelete(data); - } else if (data.event === EVENT_MESSAGE_READ) { - await handlePushMessageRead(data); + } else if (data.event === EVENT_MESSAGE_CLEAR) { + await handlePushMessageClear(data); } else if (data.event === "subscription_expiring") { await handlePushSubscriptionExpiring(data); } else { diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index 06043acc..8e02d6f7 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -52,7 +52,7 @@ class Connection { if (data.event === EVENT_OPEN) { return; } - // Accept message, message_delete, and message_read events + // Accept message, message_delete, and message_clear events const relevantAndValid = isNotificationEvent(data.event) && "id" in data && "time" in data; if (!relevantAndValid) { console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`); diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index 56415b76..b455a308 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -66,9 +66,7 @@ class Poller { } // Add only the latest notification for each non-deleted sequence - const notificationsToAdd = Object - .values(latestBySequenceId) - .filter(n => n.event === EVENT_MESSAGE); + const notificationsToAdd = Object.values(latestBySequenceId).filter((n) => n.event === EVENT_MESSAGE); if (notificationsToAdd.length > 0) { console.log(`[Poller] Adding ${notificationsToAdd.length} notification(s) for ${subscription.id}`); await subscriptionManager.addNotifications(subscription.id, notificationsToAdd); diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 2eecde28..41eeba2f 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -3,7 +3,7 @@ import notifier from "./Notifier"; import prefs from "./Prefs"; import db from "./db"; import { messageWithSequenceId, topicUrl } from "./utils"; -import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_READ } from "./events"; +import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "./events"; class SubscriptionManager { constructor(dbImpl) { @@ -16,7 +16,7 @@ class SubscriptionManager { return Promise.all( subscriptions.map(async (s) => ({ ...s, - new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count() + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), })) ); } @@ -84,7 +84,7 @@ class SubscriptionManager { baseUrl, topic, mutedUntil: 0, - last: null + last: null, }; await this.db.subscriptions.put(subscription); @@ -102,7 +102,7 @@ class SubscriptionManager { const local = await this.add(remote.base_url, remote.topic, { displayName: remote.display_name, // May be undefined - reservation // May be null! + reservation, // May be null! }); return local.id; @@ -175,7 +175,7 @@ class SubscriptionManager { /** Adds notification, or returns false if it already exists */ async addNotification(subscriptionId, notification) { const exists = await this.db.notifications.get(notification.id); - if (exists || notification.event === EVENT_MESSAGE_DELETE || notification.event === EVENT_MESSAGE_READ) { + if (exists || notification.event === EVENT_MESSAGE_DELETE || notification.event === EVENT_MESSAGE_CLEAR) { return false; } try { @@ -186,13 +186,13 @@ class SubscriptionManager { await this.db.notifications.add({ ...messageWithSequenceId(notification), subscriptionId, - new: 1 // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation }); // FIXME consider put() for double tab // Update subscription last message id (for ?since=... queries) await this.db.subscriptions.update(subscriptionId, { - last: notification.id + last: notification.id, }); } catch (e) { console.error(`[SubscriptionManager] Error adding notification`, e); @@ -208,7 +208,7 @@ class SubscriptionManager { const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { - last: lastNotificationId + last: lastNotificationId, }); } @@ -251,19 +251,19 @@ class SubscriptionManager { async setMutedUntil(subscriptionId, mutedUntil) { await this.db.subscriptions.update(subscriptionId, { - mutedUntil + mutedUntil, }); } async setDisplayName(subscriptionId, displayName) { await this.db.subscriptions.update(subscriptionId, { - displayName + displayName, }); } async setReservation(subscriptionId, reservation) { await this.db.subscriptions.update(subscriptionId, { - reservation + reservation, }); } diff --git a/web/src/app/db.js b/web/src/app/db.js index cb65c0b6..172857d9 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -15,7 +15,7 @@ const createDatabase = (username) => { subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", notifications: "&id,sequenceId,subscriptionId,time,new,[subscriptionId+new],[subscriptionId+sequenceId]", users: "&baseUrl,username", - prefs: "&key" + prefs: "&key", }); return db; diff --git a/web/src/app/events.js b/web/src/app/events.js index 55dc262c..48537652 100644 --- a/web/src/app/events.js +++ b/web/src/app/events.js @@ -5,10 +5,8 @@ export const EVENT_OPEN = "open"; export const EVENT_KEEPALIVE = "keepalive"; export const EVENT_MESSAGE = "message"; export const EVENT_MESSAGE_DELETE = "message_delete"; -export const EVENT_MESSAGE_READ = "message_read"; +export const EVENT_MESSAGE_CLEAR = "message_clear"; export const EVENT_POLL_REQUEST = "poll_request"; // Check if an event is a notification event (message, delete, or read) -export const isNotificationEvent = (event) => - event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_READ; - +export const isNotificationEvent = (event) => event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_CLEAR; diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 5e271b35..2d88f2cf 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -12,7 +12,7 @@ import accountApi from "../app/AccountApi"; import { UnauthorizedError } from "../app/errors"; import notifier from "../app/Notifier"; import prefs from "../app/Prefs"; -import { EVENT_MESSAGE_DELETE, EVENT_MESSAGE_READ } from "../app/events"; +import { EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../app/events"; /** * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection @@ -57,7 +57,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) { // Handle delete: remove notification from database await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id); - } else if (notification.event === EVENT_MESSAGE_READ && notification.sequence_id) { + } else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) { // Handle read: mark notification as read await subscriptionManager.markNotificationReadBySequenceId(subscriptionId, notification.sequence_id); } else { From 44f20f6b4c367a7ad6e7709ff35d819c3e93c771 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 13 Jan 2026 20:50:31 -0500 Subject: [PATCH 362/378] Add tests, fix firebase --- server/server_firebase.go | 10 ++ server/server_firebase_test.go | 3 + server/server_test.go | 210 ++++++++++++++++++++++++++++- web/src/app/SubscriptionManager.js | 7 +- web/src/app/notificationUtils.js | 2 +- web/src/app/utils.js | 6 +- 6 files changed, 229 insertions(+), 9 deletions(-) diff --git a/server/server_firebase.go b/server/server_firebase.go index 13e80b93..9fde63a3 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -143,6 +143,15 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro "poll_id": m.PollID, } apnsConfig = createAPNSAlertConfig(m, data) + case messageDeleteEvent, messageClearEvent: + data = map[string]string{ + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": m.Event, + "topic": m.Topic, + "sequence_id": m.SequenceID, + } + apnsConfig = createAPNSBackgroundConfig(data) case messageEvent: if auther != nil { // If "anonymous read" for a topic is not allowed, we cannot send the message along @@ -161,6 +170,7 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro "time": fmt.Sprintf("%d", m.Time), "event": m.Event, "topic": m.Topic, + "sequence_id": m.SequenceID, "priority": fmt.Sprintf("%d", m.Priority), "tags": strings.Join(m.Tags, ","), "click": m.Click, diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 89004cd3..c98f528f 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -177,6 +177,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "time": fmt.Sprintf("%d", m.Time), "event": "message", "topic": "mytopic", + "sequence_id": "", "priority": "4", "tags": strings.Join(m.Tags, ","), "click": "https://google.com", @@ -199,6 +200,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "time": fmt.Sprintf("%d", m.Time), "event": "message", "topic": "mytopic", + "sequence_id": "", "priority": "4", "tags": strings.Join(m.Tags, ","), "click": "https://google.com", @@ -232,6 +234,7 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) { "time": fmt.Sprintf("%d", m.Time), "event": "poll_request", "topic": "mytopic", + "sequence_id": "", "message": "New message", "title": "", "tags": "", diff --git a/server/server_test.go b/server/server_test.go index 964d6156..530d9458 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -8,8 +8,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "golang.org/x/crypto/bcrypt" - "heckel.io/ntfy/v2/user" "io" "net/http" "net/http/httptest" @@ -24,7 +22,9 @@ import ( "time" "github.com/stretchr/testify/require" + "golang.org/x/crypto/bcrypt" "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/util" ) @@ -3289,6 +3289,212 @@ func TestServer_MessageTemplate_Until100_000(t *testing.T) { require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations") } +func TestServer_DeleteMessage(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Publish a message with a sequence ID + response := request(t, s, "PUT", "/mytopic/seq123", "original message", nil) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "seq123", msg.SequenceID) + require.Equal(t, "message", msg.Event) + + // Delete the message using DELETE method + response = request(t, s, "DELETE", "/mytopic/seq123", "", nil) + require.Equal(t, 200, response.Code) + deleteMsg := toMessage(t, response.Body.String()) + require.Equal(t, "seq123", deleteMsg.SequenceID) + require.Equal(t, "message_delete", deleteMsg.Event) + + // Poll and verify both messages are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) + + msg1 := toMessage(t, lines[0]) + msg2 := toMessage(t, lines[1]) + require.Equal(t, "message", msg1.Event) + require.Equal(t, "message_delete", msg2.Event) + require.Equal(t, "seq123", msg1.SequenceID) + require.Equal(t, "seq123", msg2.SequenceID) +} + +func TestServer_ClearMessage(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Publish a message with a sequence ID + response := request(t, s, "PUT", "/mytopic/seq456", "original message", nil) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "seq456", msg.SequenceID) + require.Equal(t, "message", msg.Event) + + // Clear the message using PUT /topic/seq/clear + response = request(t, s, "PUT", "/mytopic/seq456/clear", "", nil) + require.Equal(t, 200, response.Code) + clearMsg := toMessage(t, response.Body.String()) + require.Equal(t, "seq456", clearMsg.SequenceID) + require.Equal(t, "message_clear", clearMsg.Event) + + // Poll and verify both messages are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) + + msg1 := toMessage(t, lines[0]) + msg2 := toMessage(t, lines[1]) + require.Equal(t, "message", msg1.Event) + require.Equal(t, "message_clear", msg2.Event) + require.Equal(t, "seq456", msg1.SequenceID) + require.Equal(t, "seq456", msg2.SequenceID) +} + +func TestServer_ClearMessage_ReadEndpoint(t *testing.T) { + // Test that /topic/seq/read also works + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Publish a message + response := request(t, s, "PUT", "/mytopic/seq789", "original message", nil) + require.Equal(t, 200, response.Code) + + // Clear using /read endpoint + response = request(t, s, "PUT", "/mytopic/seq789/read", "", nil) + require.Equal(t, 200, response.Code) + clearMsg := toMessage(t, response.Body.String()) + require.Equal(t, "seq789", clearMsg.SequenceID) + require.Equal(t, "message_clear", clearMsg.Event) +} + +func TestServer_UpdateMessage(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Publish original message + response := request(t, s, "PUT", "/mytopic/update-seq", "original message", nil) + require.Equal(t, 200, response.Code) + msg1 := toMessage(t, response.Body.String()) + require.Equal(t, "update-seq", msg1.SequenceID) + require.Equal(t, "original message", msg1.Message) + + // Update the message (same sequence ID, new content) + response = request(t, s, "PUT", "/mytopic/update-seq", "updated message", nil) + require.Equal(t, 200, response.Code) + msg2 := toMessage(t, response.Body.String()) + require.Equal(t, "update-seq", msg2.SequenceID) + require.Equal(t, "updated message", msg2.Message) + require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs + + // Poll and verify both versions are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) + + polledMsg1 := toMessage(t, lines[0]) + polledMsg2 := toMessage(t, lines[1]) + require.Equal(t, "original message", polledMsg1.Message) + require.Equal(t, "updated message", polledMsg2.Message) + require.Equal(t, "update-seq", polledMsg1.SequenceID) + require.Equal(t, "update-seq", polledMsg2.SequenceID) +} + +func TestServer_UpdateMessage_UsingMessageID(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Publish original message without a sequence ID + response := request(t, s, "PUT", "/mytopic", "original message", nil) + require.Equal(t, 200, response.Code) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Empty(t, msg1.SequenceID) // No sequence ID provided + require.Equal(t, "original message", msg1.Message) + + // Update the message using the message ID as the sequence ID + response = request(t, s, "PUT", "/mytopic/"+msg1.ID, "updated message", nil) + require.Equal(t, 200, response.Code) + msg2 := toMessage(t, response.Body.String()) + require.Equal(t, msg1.ID, msg2.SequenceID) // Message ID is now used as sequence ID + require.Equal(t, "updated message", msg2.Message) + require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs + + // Poll and verify both versions are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) + + polledMsg1 := toMessage(t, lines[0]) + polledMsg2 := toMessage(t, lines[1]) + require.Equal(t, "original message", polledMsg1.Message) + require.Equal(t, "updated message", polledMsg2.Message) + require.Empty(t, polledMsg1.SequenceID) // Original has no sequence ID + require.Equal(t, msg1.ID, polledMsg2.SequenceID) // Update uses original message ID as sequence ID +} + +func TestServer_DeleteAndClear_InvalidSequenceID(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Test invalid sequence ID for delete (returns 404 because route doesn't match) + response := request(t, s, "DELETE", "/mytopic/invalid*seq", "", nil) + require.Equal(t, 404, response.Code) + + // Test invalid sequence ID for clear (returns 404 because route doesn't match) + response = request(t, s, "PUT", "/mytopic/invalid*seq/clear", "", nil) + require.Equal(t, 404, response.Code) +} + +func TestServer_DeleteMessage_WithFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + // Publish a message + response := request(t, s, "PUT", "/mytopic/firebase-seq", "test message", nil) + require.Equal(t, 200, response.Code) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 1, len(sender.Messages())) + require.Equal(t, "message", sender.Messages()[0].Data["event"]) + + // Delete the message + response = request(t, s, "DELETE", "/mytopic/firebase-seq", "", nil) + require.Equal(t, 200, response.Code) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 2, len(sender.Messages())) + require.Equal(t, "message_delete", sender.Messages()[1].Data["event"]) + require.Equal(t, "firebase-seq", sender.Messages()[1].Data["sequence_id"]) +} + +func TestServer_ClearMessage_WithFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + // Publish a message + response := request(t, s, "PUT", "/mytopic/firebase-clear-seq", "test message", nil) + require.Equal(t, 200, response.Code) + + time.Sleep(100 * time.Millisecond) + require.Equal(t, 1, len(sender.Messages())) + + // Clear the message + response = request(t, s, "PUT", "/mytopic/firebase-clear-seq/clear", "", nil) + require.Equal(t, 200, response.Code) + + time.Sleep(100 * time.Millisecond) + require.Equal(t, 2, len(sender.Messages())) + require.Equal(t, "message_clear", sender.Messages()[1].Data["event"]) + require.Equal(t, "firebase-clear-seq", sender.Messages()[1].Data["sequence_id"]) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 41eeba2f..430c5e2c 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -202,9 +202,10 @@ class SubscriptionManager { /** Adds/replaces notifications, will not throw if they exist */ async addNotifications(subscriptionId, notifications) { - const notificationsWithSubscriptionId = notifications.map((notification) => { - return { ...messageWithSequenceId(notification), subscriptionId }; - }); + const notificationsWithSubscriptionId = notifications.map((notification) => ({ + ...messageWithSequenceId(notification), + subscriptionId, + })); const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 65b5bd3d..2d80e0be 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -50,7 +50,7 @@ export const isImage = (attachment) => { export const icon = "/static/images/ntfy.png"; export const badge = "/static/images/mask-icon.svg"; -export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { +export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => { const image = isImage(message.attachment) ? message.attachment.url : undefined; // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 9aeada05..9e095c7e 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -104,10 +104,10 @@ export const maybeActionErrors = (notification) => { }; export const messageWithSequenceId = (message) => { - if (!message.sequenceId) { - message.sequenceId = message.sequence_id || message.id; + if (message.sequenceId) { + return message; } - return message; + return { ...message, sequenceId: message.sequence_id || message.id }; }; export const shuffle = (arr) => { From dd9b36cf0a1ff1a048579deabad046d20ba10c74 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Tue, 13 Jan 2026 21:29:44 -0500 Subject: [PATCH 363/378] Fix db crash --- web/src/app/db.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/src/app/db.js b/web/src/app/db.js index 172857d9..e088a267 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -18,6 +18,13 @@ const createDatabase = (username) => { prefs: "&key", }); + // When another connection (e.g., service worker or another tab) wants to upgrade, + // close this connection gracefully to allow the upgrade to proceed + db.on("versionchange", () => { + console.log("[db] versionchange event: closing database"); + db.close(); + }); + return db; }; From 96638b516c3c1ee05b7f760523ab3518ca2a3c34 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 14 Jan 2026 20:46:18 -0500 Subject: [PATCH 364/378] Fix service worker handling for updating/deleting --- server/server.go | 6 ++-- web/public/sw.js | 52 +++++++++++++----------------- web/src/app/SubscriptionManager.js | 5 +-- web/src/app/events.js | 1 + web/src/app/notificationUtils.js | 13 ++++++-- web/src/app/utils.js | 7 ---- web/src/registerSW.js | 22 +++++++++++-- 7 files changed, 59 insertions(+), 47 deletions(-) diff --git a/server/server.go b/server/server.go index 7ef43275..3bd53ea6 100644 --- a/server/server.go +++ b/server/server.go @@ -86,8 +86,6 @@ var ( webConfigPath = "/config.js" webManifestPath = "/manifest.webmanifest" - webRootHTMLPath = "/app.html" - webServiceWorkerPath = "/sw.js" accountPath = "/account" matrixPushPath = "/_matrix/push/v1/notify" metricsPath = "/metrics" @@ -111,7 +109,7 @@ var ( apiAccountBillingSubscriptionCheckoutSuccessTemplate = "/v1/account/billing/subscription/success/{CHECKOUT_SESSION_ID}" apiAccountBillingSubscriptionCheckoutSuccessRegex = regexp.MustCompile(`/v1/account/billing/subscription/success/(.+)$`) apiAccountReservationSingleRegex = regexp.MustCompile(`/v1/account/reservation/([-_A-Za-z0-9]{1,64})$`) - staticRegex = regexp.MustCompile(`^/static/.+`) + staticRegex = regexp.MustCompile(`^/(static/.+|app.html|sw.js|sw.js.map)$`) docsRegex = regexp.MustCompile(`^/docs(|/.*)$`) fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) urlRegex = regexp.MustCompile(`^https?://`) @@ -534,7 +532,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.handleMatrixDiscovery(w) } else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil { return s.handleMetrics(w, r, v) - } else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) { + } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { return s.ensureWebEnabled(s.handleStatic)(w, r, v) } else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) { return s.ensureWebEnabled(s.handleDocs)(w, r, v) diff --git a/web/public/sw.js b/web/public/sw.js index 0dc1afef..07a89415 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -3,13 +3,10 @@ import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from import { NavigationRoute, registerRoute } from "workbox-routing"; import { NetworkFirst } from "workbox-strategies"; import { clientsClaim } from "workbox-core"; - import { dbAsync } from "../src/app/db"; - -import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; +import { badge, icon, messageWithSequenceId, toNotificationParams } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; -import { messageWithSequenceId } from "../src/app/utils"; -import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../src/app/events"; +import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE, EVENT_SUBSCRIPTION_EXPIRING } from "../src/app/events"; /** * General docs for service workers and PWAs: @@ -23,10 +20,17 @@ import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../src const broadcastChannel = new BroadcastChannel("web-push-broadcast"); -const addNotification = async ({ subscriptionId, message }) => { +/** + * Handle a received web push message and show notification. + * + * Since the service worker cannot play a sound, we send a broadcast to the web app, which (if it is running) + * receives the broadcast and plays a sound (see web/src/app/WebPush.js). + */ +const handlePushMessage = async (data) => { + const { subscription_id: subscriptionId, message } = data; const db = await dbAsync(); - // Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too + console.log("[ServiceWorker] Message received", data); // Delete existing notification with same sequence ID (if any) const sequenceId = message.sequence_id || message.id; @@ -46,22 +50,9 @@ const addNotification = async ({ subscriptionId, message }) => { last: message.id, }); + // Update badge in PWA const badgeCount = await db.notifications.where({ new: 1 }).count(); - console.log("[ServiceWorker] Setting new app badge count", { badgeCount }); self.navigator.setAppBadge?.(badgeCount); -}; - -/** - * Handle a received web push message and show notification. - * - * Since the service worker cannot play a sound, we send a broadcast to the web app, which (if it is running) - * receives the broadcast and plays a sound (see web/src/app/WebPush.js). - */ -const handlePushMessage = async (data) => { - const { subscription_id: subscriptionId, message } = data; - - // Add notification to database - await addNotification({ subscriptionId, message }); // Broadcast the message to potentially play a sound broadcastChannel.postMessage(message); @@ -82,11 +73,11 @@ const handlePushMessage = async (data) => { const handlePushMessageDelete = async (data) => { const { subscription_id: subscriptionId, message } = data; const db = await dbAsync(); + console.log("[ServiceWorker] Deleting notification sequence", data); // Delete notification with the same sequence_id const sequenceId = message.sequence_id; if (sequenceId) { - console.log("[ServiceWorker] Deleting notification with sequenceId", { subscriptionId, sequenceId }); await db.notifications.where({ subscriptionId, sequenceId }).delete(); } @@ -102,11 +93,11 @@ const handlePushMessageDelete = async (data) => { const handlePushMessageClear = async (data) => { const { subscription_id: subscriptionId, message } = data; const db = await dbAsync(); + console.log("[ServiceWorker] Marking notification as read", data); // Mark notification as read (set new = 0) const sequenceId = message.sequence_id; if (sequenceId) { - console.log("[ServiceWorker] Marking notification as read", { subscriptionId, sequenceId }); await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 }); } @@ -126,6 +117,7 @@ const handlePushMessageClear = async (data) => { */ const handlePushSubscriptionExpiring = async (data) => { const t = await initI18n(); + console.log("[ServiceWorker] Handling incoming subscription expiring event", data); await self.registration.showNotification(t("web_push_subscription_expiring_title"), { body: t("web_push_subscription_expiring_body"), @@ -141,6 +133,7 @@ const handlePushSubscriptionExpiring = async (data) => { */ const handlePushUnknown = async (data) => { const t = await initI18n(); + console.log("[ServiceWorker] Unknown event received", data); await self.registration.showNotification(t("web_push_unknown_notification_title"), { body: t("web_push_unknown_notification_body"), @@ -155,13 +148,15 @@ const handlePushUnknown = async (data) => { * @param {object} data see server/types.go, type webPushPayload */ const handlePush = async (data) => { - if (data.event === EVENT_MESSAGE) { + const { message } = data; + + if (message.event === EVENT_MESSAGE) { await handlePushMessage(data); - } else if (data.event === EVENT_MESSAGE_DELETE) { + } else if (message.event === EVENT_MESSAGE_DELETE) { await handlePushMessageDelete(data); - } else if (data.event === EVENT_MESSAGE_CLEAR) { + } else if (message.event === EVENT_MESSAGE_CLEAR) { await handlePushMessageClear(data); - } else if (data.event === "subscription_expiring") { + } else if (message.event === EVENT_SUBSCRIPTION_EXPIRING) { await handlePushSubscriptionExpiring(data); } else { await handlePushUnknown(data); @@ -176,10 +171,8 @@ const handleClick = async (event) => { const t = await initI18n(); const clients = await self.clients.matchAll({ type: "window" }); - const rootUrl = new URL(self.location.origin); const rootClient = clients.find((client) => client.url === rootUrl.toString()); - // perhaps open on another topic const fallbackClient = clients[0]; if (!event.notification.data?.message) { @@ -295,6 +288,7 @@ precacheAndRoute( // Claim all open windows clientsClaim(); + // Delete any cached old dist files from previous service worker versions cleanupOutdatedCaches(); diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 430c5e2c..f909778e 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -2,8 +2,9 @@ import api from "./Api"; import notifier from "./Notifier"; import prefs from "./Prefs"; import db from "./db"; -import { messageWithSequenceId, topicUrl } from "./utils"; -import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "./events"; +import { topicUrl } from "./utils"; +import { messageWithSequenceId } from "./notificationUtils"; +import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE } from "./events"; class SubscriptionManager { constructor(dbImpl) { diff --git a/web/src/app/events.js b/web/src/app/events.js index 48537652..94d7dc79 100644 --- a/web/src/app/events.js +++ b/web/src/app/events.js @@ -7,6 +7,7 @@ export const EVENT_MESSAGE = "message"; export const EVENT_MESSAGE_DELETE = "message_delete"; export const EVENT_MESSAGE_CLEAR = "message_clear"; export const EVENT_POLL_REQUEST = "poll_request"; +export const EVENT_SUBSCRIPTION_EXPIRING = "subscription_expiring"; // Check if an event is a notification event (message, delete, or read) export const isNotificationEvent = (event) => event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_CLEAR; diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 2d80e0be..a9b8f8ff 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -25,13 +25,13 @@ const formatTitleWithDefault = (m, fallback) => { export const formatMessage = (m) => { if (m.title) { - return m.message; + return m.message || ""; } const emojiList = toEmojis(m.tags); if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.message}`; + return `${emojiList.join(" ")} ${m.message || ""}`; } - return m.message; + return m.message || ""; }; const imageRegex = /\.(png|jpe?g|gif|webp)$/i; @@ -79,3 +79,10 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => { }, ]; }; + +export const messageWithSequenceId = (message) => { + if (message.sequenceId) { + return message; + } + return { ...message, sequenceId: message.sequence_id || message.id }; +}; diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 9e095c7e..935f2024 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -103,13 +103,6 @@ export const maybeActionErrors = (notification) => { return actionErrors; }; -export const messageWithSequenceId = (message) => { - if (message.sequenceId) { - return message; - } - return { ...message, sequenceId: message.sequence_id || message.id }; -}; - export const shuffle = (arr) => { const returnArr = [...arr]; diff --git a/web/src/registerSW.js b/web/src/registerSW.js index adef4746..5ae85628 100644 --- a/web/src/registerSW.js +++ b/web/src/registerSW.js @@ -5,10 +5,21 @@ import { registerSW as viteRegisterSW } from "virtual:pwa-register"; const intervalMS = 60 * 60 * 1000; // https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html -const registerSW = () => +const registerSW = () => { + console.log("[ServiceWorker] Registering service worker"); + console.log("[ServiceWorker] serviceWorker in navigator:", "serviceWorker" in navigator); + + if (!("serviceWorker" in navigator)) { + console.warn("[ServiceWorker] Service workers not supported"); + return; + } + viteRegisterSW({ onRegisteredSW(swUrl, registration) { + console.log("[ServiceWorker] Registered:", { swUrl, registration }); + if (!registration) { + console.warn("[ServiceWorker] No registration returned"); return; } @@ -23,9 +34,16 @@ const registerSW = () => }, }); - if (resp?.status === 200) await registration.update(); + if (resp?.status === 200) { + console.log("[ServiceWorker] Updating service worker"); + await registration.update(); + } }, intervalMS); }, + onRegisterError(error) { + console.error("[ServiceWorker] Registration error:", error); + }, }); +}; export default registerSW; From ff2b8167b4d663b7bd2ed2348a6b8db2803b960f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 14 Jan 2026 21:17:21 -0500 Subject: [PATCH 365/378] Make closing notifications work --- web/public/sw.js | 15 ++++++++++++++- web/src/app/Notifier.js | 15 +++++++++++++++ web/src/components/hooks.js | 4 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/web/public/sw.js b/web/public/sw.js index 07a89415..db87e7f8 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -81,6 +81,13 @@ const handlePushMessageDelete = async (data) => { await db.notifications.where({ subscriptionId, sequenceId }).delete(); } + // Close browser notification with matching tag + const tag = message.sequence_id || message.id; + if (tag) { + const notifications = await self.registration.getNotifications({ tag }); + notifications.forEach((notification) => notification.close()); + } + // Update subscription last message id (for ?since=... queries) await db.subscriptions.update(subscriptionId, { last: message.id, @@ -101,6 +108,13 @@ const handlePushMessageClear = async (data) => { await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 }); } + // Close browser notification with matching tag + const tag = message.sequence_id || message.id; + if (tag) { + const notifications = await self.registration.getNotifications({ tag }); + notifications.forEach((notification) => notification.close()); + } + // Update subscription last message id (for ?since=... queries) await db.subscriptions.update(subscriptionId, { last: message.id, @@ -108,7 +122,6 @@ const handlePushMessageClear = async (data) => { // Update badge count const badgeCount = await db.notifications.where({ new: 1 }).count(); - console.log("[ServiceWorker] Setting new app badge count", { badgeCount }); self.navigator.setAppBadge?.(badgeCount); }; diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js index 77bbdb1e..79089749 100644 --- a/web/src/app/Notifier.js +++ b/web/src/app/Notifier.js @@ -31,6 +31,21 @@ class Notifier { ); } + async cancel(notification) { + if (!this.supported()) { + return; + } + try { + const tag = notification.sequence_id || notification.id; + console.log(`[Notifier] Cancelling notification with ${tag}`); + const registration = await this.serviceWorkerRegistration(); + const notifications = await registration.getNotifications({ tag }); + notifications.forEach((notification) => notification.close()); + } catch (e) { + console.log(`[Notifier] Error cancelling notification`, e); + } + } + async playSound() { // Play sound const sound = await prefs.sound(); diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 2d88f2cf..1c9c2bff 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -55,11 +55,11 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop // and FirebaseService::handleMessage(). if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) { - // Handle delete: remove notification from database await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id); + await notifier.cancel(notification); } else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) { - // Handle read: mark notification as read await subscriptionManager.markNotificationReadBySequenceId(subscriptionId, notification.sequence_id); + await notifier.cancel(notification); } else { // Regular message: delete existing and add new const sequenceId = notification.sequence_id || notification.id; From 2e39a1329cdf7e30a4abff0a23ac523a6d09820d Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 14 Jan 2026 21:48:21 -0500 Subject: [PATCH 366/378] AI docs, needs work --- docs/faq.md | 4 +- docs/publish.md | 161 ++++++++++++++++++++++++++++++++++++---- docs/releases.md | 10 +++ docs/subscribe/api.md | 29 ++++---- web/src/app/Notifier.js | 6 +- 5 files changed, 175 insertions(+), 35 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 5fa5252c..5153c700 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -96,8 +96,8 @@ appreciated. ## Can I email you? Can I DM you on Discord/Matrix? For community support, please use the public channels listed on the [contact page](contact.md). I generally **do not respond to direct messages** about ntfy, unless you are paying for a [ntfy Pro](https://ntfy.sh/#pricing) -plan (see [paid support](contact.md#paid-support-ntfy-pro-subscribers)), or you are inquiring about business -opportunities (see [general inquiries](contact.md#general-inquiries)). +plan (see [paid support](contact.md#paid-support)), or you are inquiring about business +opportunities (see [other inquiries](contact.md#other-inquiries)). I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions in public forums benefits others, since I can link to the discussion at a later point in time, or other users diff --git a/docs/publish.md b/docs/publish.md index 9c409523..fb0b38fb 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -1418,22 +1418,23 @@ The JSON message format closely mirrors the format of the message you can consum (see [JSON message format](subscribe/api.md#json-message-format) for details), but is not exactly identical. Here's an overview of all the supported fields: -| Field | Required | Type | Example | Description | -|------------|----------|----------------------------------|-------------------------------------------|-----------------------------------------------------------------------| -| `topic` | ✔️ | *string* | `topic1` | Target topic name | -| `message` | - | *string* | `Some message` | Message body; set to `triggered` if empty or not passed | -| `title` | - | *string* | `Some title` | Message [title](#message-title) | -| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](#tags-emojis) that may or not map to emojis | -| `priority` | - | *int (one of: 1, 2, 3, 4, or 5)* | `4` | Message [priority](#message-priority) with 1=min, 3=default and 5=max | -| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications | -| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) | -| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-a-url) | -| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted | -| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) | -| `filename` | - | *string* | `file.jpg` | File name of the attachment | -| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery | -| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications | -| `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) | +| Field | Required | Type | Example | Description | +|---------------|----------|----------------------------------|-------------------------------------------|-------------------------------------------------------------------------------------------| +| `topic` | ✔️ | *string* | `topic1` | Target topic name | +| `message` | - | *string* | `Some message` | Message body; set to `triggered` if empty or not passed | +| `title` | - | *string* | `Some title` | Message [title](#message-title) | +| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](#tags-emojis) that may or not map to emojis | +| `priority` | - | *int (one of: 1, 2, 3, 4, or 5)* | `4` | Message [priority](#message-priority) with 1=min, 3=default and 5=max | +| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications | +| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) | +| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-a-url) | +| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted | +| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) | +| `filename` | - | *string* | `file.jpg` | File name of the attachment | +| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery | +| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications | +| `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) | +| `sequence_id` | - | *string* | `my-sequence-123` | Sequence ID for [updating/deleting notifications](#updating-deleting-notifications) | ## Action buttons _Supported on:_ :material-android: :material-apple: :material-firefox: @@ -2694,6 +2695,134 @@ Here's an example that will open Reddit when the notification is clicked: ])); ``` +## Updating + deleting notifications +_Supported on:_ :material-android: :material-firefox: + +You can update, clear (mark as read), or delete notifications that have already been delivered. This is useful for scenarios +like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant. + +The key concept is the **sequence ID** (`sequence_id` or `sid`): notifications with the same sequence ID are treated as +belonging to the same sequence, and clients will update/replace the notification accordingly. + +### Updating notifications +To update an existing notification, publish a new message with the same sequence ID. Clients will replace the previous +notification with the new one. + +You can either: + +1. **Use the message ID**: First publish without a sequence ID, then use the returned message `id` as the sequence ID for updates +2. **Use a custom sequence ID**: Publish directly to `//` with your own identifier + +=== "Using the message ID" + ```bash + # First, publish a message and capture the message ID + $ curl -d "Downloading file..." ntfy.sh/mytopic + {"id":"xE73Iyuabi","time":1673542291,...} + + # Then use the message ID to update it + $ curl -d "Download complete!" ntfy.sh/mytopic/xE73Iyuabi + ``` + +=== "Using a custom sequence ID" + ```bash + # Publish with a custom sequence ID + $ curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123 + + # Update using the same sequence ID + $ curl -d "Download complete!" ntfy.sh/mytopic/my-download-123 + ``` + +=== "Using the X-Sequence-ID header" + ```bash + # Publish with a sequence ID via header + $ curl -H "X-Sequence-ID: my-download-123" -d "Downloading..." ntfy.sh/mytopic + + # Update using the same sequence ID + $ curl -H "X-Sequence-ID: my-download-123" -d "Done!" ntfy.sh/mytopic + ``` + +You can also set the sequence ID via the `sid` query parameter or when [publishing as JSON](#publish-as-json) using the +`sequence_id` field. + +### Clearing notifications +To clear a notification (mark it as read and dismiss it from the notification drawer), send a PUT request to +`///clear` (or `///read` as an alias): + +=== "Command line (curl)" + ```bash + curl -X PUT ntfy.sh/mytopic/my-download-123/clear + ``` + +=== "HTTP" + ```http + PUT /mytopic/my-download-123/clear HTTP/1.1 + Host: ntfy.sh + ``` + +This publishes a `message_clear` event, which tells clients to: + +- Mark the notification as read in the app +- Dismiss the browser/Android notification + +### Deleting notifications +To delete a notification entirely, send a DELETE request to `//`: + +=== "Command line (curl)" + ```bash + curl -X DELETE ntfy.sh/mytopic/my-download-123 + ``` + +=== "HTTP" + ```http + DELETE /mytopic/my-download-123 HTTP/1.1 + Host: ntfy.sh + ``` + +This publishes a `message_delete` event, which tells clients to: + +- Delete the notification from the database +- Dismiss the browser/Android notification + +!!! info + Deleted sequences can be revived by publishing a new message with the same sequence ID. The notification will + reappear as a new message. + +### Full example +Here's a complete example showing the lifecycle of a notification with updates, clearing, and deletion: + +```bash +# 1. Create a notification with a custom sequence ID +$ curl -d "Starting backup..." ntfy.sh/mytopic/backup-2024 + +# 2. Update the notification with progress +$ curl -d "Backup 50% complete..." ntfy.sh/mytopic/backup-2024 + +# 3. Update again when complete +$ curl -d "Backup finished successfully!" ntfy.sh/mytopic/backup-2024 + +# 4. Clear the notification (dismiss from notification drawer) +$ curl -X PUT ntfy.sh/mytopic/backup-2024/clear + +# 5. Later, delete the notification entirely +$ curl -X DELETE ntfy.sh/mytopic/backup-2024 +``` + +When polling the topic, you'll see the complete sequence of events: + +```bash +$ curl -s "ntfy.sh/mytopic/json?poll=1&since=all" | jq . +``` + +```json +{"id":"abc123","time":1673542291,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Starting backup..."} +{"id":"def456","time":1673542295,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Backup 50% complete..."} +{"id":"ghi789","time":1673542300,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Backup finished successfully!"} +{"id":"jkl012","time":1673542305,"event":"message_clear","topic":"mytopic","sequence_id":"backup-2024"} +{"id":"mno345","time":1673542400,"event":"message_delete","topic":"mytopic","sequence_id":"backup-2024"} +``` + +Clients process these events in order, keeping only the latest state for each sequence ID. + ## Attachments _Supported on:_ :material-android: :material-firefox: diff --git a/docs/releases.md b/docs/releases.md index 20759f15..1d80a039 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1599,12 +1599,22 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet +### ntfy server v2.16.x (UNRELEASED) + +**Features:** + +* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications): You can now update, + clear (mark as read), or delete notifications using a sequence ID. This enables use cases like progress updates, + replacing outdated notifications, or dismissing notifications from all clients. + ### ntfy Android app v1.22.x (UNRELEASED) **Features:** * Support for self-signed certs and client certs for mTLS (#215, #530, ntfy-android#149) * Connection error dialog to help diagnose connection issues +* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications): Notifications with + the same sequence ID are updated in place, and `message_delete`/`message_clear` events dismiss notifications **Bug fixes + maintenance:** diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index a52e17f6..a549885b 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -324,20 +324,21 @@ format of the message. It's very straight forward: **Message**: -| Field | Required | Type | Example | Description | -|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier | -| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp | -| `expires` | (✔)️ | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted, not set if `Cache: no` is sent | -| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` | -| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events | -| `message` | - | *string* | `Some message` | Message body; always present in `message` events | -| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/` | -| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis | -| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max | -| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) | -| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification | -| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) | +| Field | Required | Type | Example | Description | +|---------------|----------|---------------------------------------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier | +| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp | +| `expires` | (✔)️ | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted, not set if `Cache: no` is sent | +| `event` | ✔️ | `open`, `keepalive`, `message`, `message_delete`, `message_clear`, `poll_request` | `message` | Message type, typically you'd be only interested in `message` | +| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events | +| `sequence_id` | - | *string* | `my-sequence-123` | Sequence ID for [updating/deleting notifications](../publish.md#updating-deleting-notifications) | +| `message` | - | *string* | `Some message` | Message body; always present in `message` events | +| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/` | +| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis | +| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max | +| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) | +| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification | +| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) | **Attachment** (part of the message, see [attachments](../publish.md#attachments) for details): diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js index 79089749..723ec43d 100644 --- a/web/src/app/Notifier.js +++ b/web/src/app/Notifier.js @@ -26,7 +26,7 @@ class Notifier { subscriptionId: subscription.id, message: notification, defaultTitle, - topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(), + topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString() }) ); } @@ -40,7 +40,7 @@ class Notifier { console.log(`[Notifier] Cancelling notification with ${tag}`); const registration = await this.serviceWorkerRegistration(); const notifications = await registration.getNotifications({ tag }); - notifications.forEach((notification) => notification.close()); + notifications.forEach(n => n.close()); } catch (e) { console.log(`[Notifier] Error cancelling notification`, e); } @@ -72,7 +72,7 @@ class Notifier { if (hasWebPushTopics) { return pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlB64ToUint8Array(config.web_push_public_key), + applicationServerKey: urlB64ToUint8Array(config.web_push_public_key) }); } From ea3008c7071491f0dbeb3a10f497c53111c45e2e Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 14 Jan 2026 22:18:57 -0500 Subject: [PATCH 367/378] Move, add screenshots --- docs/publish.md | 264 +++++++++--------- docs/static/css/extra.css | 18 +- ...droid-screenshot-notification-update-1.png | Bin 0 -> 82085 bytes ...droid-screenshot-notification-update-2.png | Bin 0 -> 81609 bytes 4 files changed, 145 insertions(+), 137 deletions(-) create mode 100644 docs/static/img/android-screenshot-notification-update-1.png create mode 100644 docs/static/img/android-screenshot-notification-update-2.png diff --git a/docs/publish.md b/docs/publish.md index fb0b38fb..6d5dca26 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -937,6 +937,142 @@ Here's an example with a custom message, tags and a priority: file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull'); ``` +## Updating + deleting notifications +_Supported on:_ :material-android: :material-firefox: + +!!! info + **This feature is not yet released.** It will be available in ntfy v2.16.x and later and ntfy Android v1.22.x and later. + +You can **update, clear, or delete notifications** that have already been delivered. This is useful for scenarios +like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant. + +The key concept is the **sequence ID** (`sequence_id` or `sid`): notifications with the same sequence ID are treated as +belonging to the same sequence, and clients will update/replace the notification accordingly. + +
+ + +
+ +### Updating notifications +To update an existing notification, publish a new message with the same sequence ID. Clients will replace the previous +notification with the new one. + +You can either: + +1. **Use the message ID**: First publish without a sequence ID, then use the returned message `id` as the sequence ID for updates +2. **Use a custom sequence ID**: Publish directly to `//` with your own identifier + +=== "Using the message ID" + ```bash + # First, publish a message and capture the message ID + $ curl -d "Downloading file..." ntfy.sh/mytopic + {"id":"xE73Iyuabi","time":1673542291,...} + + # Then use the message ID to update it + $ curl -d "Download complete!" ntfy.sh/mytopic/xE73Iyuabi + ``` + +=== "Using a custom sequence ID" + ```bash + # Publish with a custom sequence ID + $ curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123 + + # Update using the same sequence ID + $ curl -d "Download complete!" ntfy.sh/mytopic/my-download-123 + ``` + +=== "Using the X-Sequence-ID header" + ```bash + # Publish with a sequence ID via header + $ curl -H "X-Sequence-ID: my-download-123" -d "Downloading..." ntfy.sh/mytopic + + # Update using the same sequence ID + $ curl -H "X-Sequence-ID: my-download-123" -d "Done!" ntfy.sh/mytopic + ``` + +You can also set the sequence ID via the `sid` query parameter or when [publishing as JSON](#publish-as-json) using the +`sequence_id` field. + +### Clearing notifications +To clear a notification (mark it as read and dismiss it from the notification drawer), send a PUT request to +`///clear` (or `///read` as an alias): + +=== "Command line (curl)" + ```bash + curl -X PUT ntfy.sh/mytopic/my-download-123/clear + ``` + +=== "HTTP" + ```http + PUT /mytopic/my-download-123/clear HTTP/1.1 + Host: ntfy.sh + ``` + +This publishes a `message_clear` event, which tells clients to: + +- Mark the notification as read in the app +- Dismiss the browser/Android notification + +### Deleting notifications +To delete a notification entirely, send a DELETE request to `//`: + +=== "Command line (curl)" + ```bash + curl -X DELETE ntfy.sh/mytopic/my-download-123 + ``` + +=== "HTTP" + ```http + DELETE /mytopic/my-download-123 HTTP/1.1 + Host: ntfy.sh + ``` + +This publishes a `message_delete` event, which tells clients to: + +- Delete the notification from the database +- Dismiss the browser/Android notification + +!!! info + Deleted sequences can be revived by publishing a new message with the same sequence ID. The notification will + reappear as a new message. + +### Full example +Here's a complete example showing the lifecycle of a notification with updates, clearing, and deletion: + +```bash +# 1. Create a notification with a custom sequence ID +$ curl -d "Starting backup..." ntfy.sh/mytopic/backup-2024 + +# 2. Update the notification with progress +$ curl -d "Backup 50% complete..." ntfy.sh/mytopic/backup-2024 + +# 3. Update again when complete +$ curl -d "Backup finished successfully!" ntfy.sh/mytopic/backup-2024 + +# 4. Clear the notification (dismiss from notification drawer) +$ curl -X PUT ntfy.sh/mytopic/backup-2024/clear + +# 5. Later, delete the notification entirely +$ curl -X DELETE ntfy.sh/mytopic/backup-2024 +``` + +When polling the topic, you'll see the complete sequence of events: + +```bash +$ curl -s "ntfy.sh/mytopic/json?poll=1&since=all" | jq . +``` + +```json +{"id":"abc123","time":1673542291,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Starting backup..."} +{"id":"def456","time":1673542295,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Backup 50% complete..."} +{"id":"ghi789","time":1673542300,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Backup finished successfully!"} +{"id":"jkl012","time":1673542305,"event":"message_clear","topic":"mytopic","sequence_id":"backup-2024"} +{"id":"mno345","time":1673542400,"event":"message_delete","topic":"mytopic","sequence_id":"backup-2024"} +``` + +Clients process these events in order, keeping only the latest state for each sequence ID. + ## Message templating _Supported on:_ :material-android: :material-apple: :material-firefox: @@ -2695,134 +2831,6 @@ Here's an example that will open Reddit when the notification is clicked: ])); ``` -## Updating + deleting notifications -_Supported on:_ :material-android: :material-firefox: - -You can update, clear (mark as read), or delete notifications that have already been delivered. This is useful for scenarios -like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant. - -The key concept is the **sequence ID** (`sequence_id` or `sid`): notifications with the same sequence ID are treated as -belonging to the same sequence, and clients will update/replace the notification accordingly. - -### Updating notifications -To update an existing notification, publish a new message with the same sequence ID. Clients will replace the previous -notification with the new one. - -You can either: - -1. **Use the message ID**: First publish without a sequence ID, then use the returned message `id` as the sequence ID for updates -2. **Use a custom sequence ID**: Publish directly to `//` with your own identifier - -=== "Using the message ID" - ```bash - # First, publish a message and capture the message ID - $ curl -d "Downloading file..." ntfy.sh/mytopic - {"id":"xE73Iyuabi","time":1673542291,...} - - # Then use the message ID to update it - $ curl -d "Download complete!" ntfy.sh/mytopic/xE73Iyuabi - ``` - -=== "Using a custom sequence ID" - ```bash - # Publish with a custom sequence ID - $ curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123 - - # Update using the same sequence ID - $ curl -d "Download complete!" ntfy.sh/mytopic/my-download-123 - ``` - -=== "Using the X-Sequence-ID header" - ```bash - # Publish with a sequence ID via header - $ curl -H "X-Sequence-ID: my-download-123" -d "Downloading..." ntfy.sh/mytopic - - # Update using the same sequence ID - $ curl -H "X-Sequence-ID: my-download-123" -d "Done!" ntfy.sh/mytopic - ``` - -You can also set the sequence ID via the `sid` query parameter or when [publishing as JSON](#publish-as-json) using the -`sequence_id` field. - -### Clearing notifications -To clear a notification (mark it as read and dismiss it from the notification drawer), send a PUT request to -`///clear` (or `///read` as an alias): - -=== "Command line (curl)" - ```bash - curl -X PUT ntfy.sh/mytopic/my-download-123/clear - ``` - -=== "HTTP" - ```http - PUT /mytopic/my-download-123/clear HTTP/1.1 - Host: ntfy.sh - ``` - -This publishes a `message_clear` event, which tells clients to: - -- Mark the notification as read in the app -- Dismiss the browser/Android notification - -### Deleting notifications -To delete a notification entirely, send a DELETE request to `//`: - -=== "Command line (curl)" - ```bash - curl -X DELETE ntfy.sh/mytopic/my-download-123 - ``` - -=== "HTTP" - ```http - DELETE /mytopic/my-download-123 HTTP/1.1 - Host: ntfy.sh - ``` - -This publishes a `message_delete` event, which tells clients to: - -- Delete the notification from the database -- Dismiss the browser/Android notification - -!!! info - Deleted sequences can be revived by publishing a new message with the same sequence ID. The notification will - reappear as a new message. - -### Full example -Here's a complete example showing the lifecycle of a notification with updates, clearing, and deletion: - -```bash -# 1. Create a notification with a custom sequence ID -$ curl -d "Starting backup..." ntfy.sh/mytopic/backup-2024 - -# 2. Update the notification with progress -$ curl -d "Backup 50% complete..." ntfy.sh/mytopic/backup-2024 - -# 3. Update again when complete -$ curl -d "Backup finished successfully!" ntfy.sh/mytopic/backup-2024 - -# 4. Clear the notification (dismiss from notification drawer) -$ curl -X PUT ntfy.sh/mytopic/backup-2024/clear - -# 5. Later, delete the notification entirely -$ curl -X DELETE ntfy.sh/mytopic/backup-2024 -``` - -When polling the topic, you'll see the complete sequence of events: - -```bash -$ curl -s "ntfy.sh/mytopic/json?poll=1&since=all" | jq . -``` - -```json -{"id":"abc123","time":1673542291,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Starting backup..."} -{"id":"def456","time":1673542295,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Backup 50% complete..."} -{"id":"ghi789","time":1673542300,"event":"message","topic":"mytopic","sequence_id":"backup-2024","message":"Backup finished successfully!"} -{"id":"jkl012","time":1673542305,"event":"message_clear","topic":"mytopic","sequence_id":"backup-2024"} -{"id":"mno345","time":1673542400,"event":"message_delete","topic":"mytopic","sequence_id":"backup-2024"} -``` - -Clients process these events in order, keeping only the latest state for each sequence ID. - ## Attachments _Supported on:_ :material-android: :material-firefox: diff --git a/docs/static/css/extra.css b/docs/static/css/extra.css index 3c53aed6..4577ccce 100644 --- a/docs/static/css/extra.css +++ b/docs/static/css/extra.css @@ -1,10 +1,10 @@ :root > * { - --md-primary-fg-color: #338574; + --md-primary-fg-color: #338574; --md-primary-fg-color--light: #338574; - --md-primary-fg-color--dark: #338574; - --md-footer-bg-color: #353744; - --md-text-font: "Roboto"; - --md-code-font: "Roboto Mono"; + --md-primary-fg-color--dark: #338574; + --md-footer-bg-color: #353744; + --md-text-font: "Roboto"; + --md-code-font: "Roboto Mono"; } .md-header__button.md-logo :is(img, svg) { @@ -34,7 +34,7 @@ figure img, figure video { } header { - background: linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%); + background: linear-gradient(150deg, rgba(51, 133, 116, 1) 0%, rgba(86, 189, 168, 1) 100%); } body[data-md-color-scheme="default"] header { @@ -93,7 +93,7 @@ figure video { .screenshots img { max-height: 230px; - max-width: 300px; + max-width: 350px; margin: 3px; border-radius: 5px; filter: drop-shadow(2px 2px 2px #ddd); @@ -107,7 +107,7 @@ figure video { opacity: 0; visibility: hidden; position: fixed; - left:0; + left: 0; right: 0; top: 0; bottom: 0; @@ -119,7 +119,7 @@ figure video { } .lightbox.show { - background-color: rgba(0,0,0, 0.75); + background-color: rgba(0, 0, 0, 0.75); opacity: 1; visibility: visible; z-index: 1000; diff --git a/docs/static/img/android-screenshot-notification-update-1.png b/docs/static/img/android-screenshot-notification-update-1.png new file mode 100644 index 0000000000000000000000000000000000000000..16320de4c4e83752b4cb16da0f14e1d929c00852 GIT binary patch literal 82085 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

    {9WD`1bWDJg_I6BUrvjPvt4k4kP^n3r@vy5d7ol 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 L%FxnR#zFrN;V5PV literal 0 HcmV?d00001 From a712d78e4c889754d938c5f4e87e4970c9e5c7b1 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 14 Jan 2026 13:26:24 +0100 Subject: [PATCH 368/378] Translated using Weblate (Czech) Currently translated at 100.0% (407 of 407 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/cs/ --- web/public/static/langs/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/cs.json b/web/public/static/langs/cs.json index 2279cad1..dde71051 100644 --- a/web/public/static/langs/cs.json +++ b/web/public/static/langs/cs.json @@ -403,5 +403,7 @@ "prefs_appearance_theme_light": "Světlý režim", "web_push_subscription_expiring_title": "Oznámení budou pozastavena", "web_push_unknown_notification_title": "Neznámé oznámení přijaté ze serveru", - "web_push_unknown_notification_body": "Možná bude nutné aktualizovat ntfy otevřením webové aplikace" + "web_push_unknown_notification_body": "Možná bude nutné aktualizovat ntfy otevřením webové aplikace", + "account_basics_cannot_edit_or_delete_provisioned_user": "Přiděleného uživatele nelze upravovat ani odstranit", + "account_tokens_table_cannot_delete_or_edit_provisioned_token": "Nelze upravit ani odstranit přidělený token" } From 3d54260f7932024f99bee94ccfb6828629a51d55 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 15 Jan 2026 09:30:37 -0500 Subject: [PATCH 369/378] Docs --- client/options.go | 5 ++ cmd/publish.go | 6 ++ docs/publish.md | 159 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 150 insertions(+), 20 deletions(-) diff --git a/client/options.go b/client/options.go index f4711834..b99f1673 100644 --- a/client/options.go +++ b/client/options.go @@ -88,6 +88,11 @@ func WithFilename(filename string) PublishOption { return WithHeader("X-Filename", filename) } +// WithSequenceID sets a sequence ID for the message, allowing updates to existing notifications +func WithSequenceID(sequenceID string) PublishOption { + return WithHeader("X-Sequence-ID", sequenceID) +} + // WithEmail instructs the server to also send the message to the given e-mail address func WithEmail(email string) PublishOption { return WithHeader("X-Email", email) diff --git a/cmd/publish.go b/cmd/publish.go index f3139a63..c80c140b 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -34,6 +34,7 @@ var flagsPublish = append( &cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"}, &cli.StringFlag{Name: "template", Aliases: []string{"tpl"}, EnvVars: []string{"NTFY_TEMPLATE"}, Usage: "use templates to transform JSON message body"}, &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"}, + &cli.StringFlag{Name: "sequence-id", Aliases: []string{"sequence_id", "sid", "S"}, EnvVars: []string{"NTFY_SEQUENCE_ID"}, Usage: "sequence ID for updating notifications"}, &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, @@ -70,6 +71,7 @@ Examples: ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment + ntfy pub -S my-id mytopic 'Update me' # Send with sequence ID for updates echo 'message' | ntfy publish mytopic # Send message from stdin ntfy pub -u phil:mypass secret Psst # Publish with username/password ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing @@ -101,6 +103,7 @@ func execPublish(c *cli.Context) error { markdown := c.Bool("markdown") template := c.String("template") filename := c.String("filename") + sequenceID := c.String("sequence-id") file := c.String("file") email := c.String("email") user := c.String("user") @@ -154,6 +157,9 @@ func execPublish(c *cli.Context) error { if filename != "" { options = append(options, client.WithFilename(filename)) } + if sequenceID != "" { + options = append(options, client.WithSequenceID(sequenceID)) + } if email != "" { options = append(options, client.WithEmail(email)) } diff --git a/docs/publish.md b/docs/publish.md index 6d5dca26..b8afb092 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -963,36 +963,95 @@ You can either: 1. **Use the message ID**: First publish without a sequence ID, then use the returned message `id` as the sequence ID for updates 2. **Use a custom sequence ID**: Publish directly to `//` with your own identifier -=== "Using the message ID" - ```bash - # First, publish a message and capture the message ID - $ curl -d "Downloading file..." ntfy.sh/mytopic - {"id":"xE73Iyuabi","time":1673542291,...} +Here's an example using a custom sequence ID to update a notification: - # Then use the message ID to update it - $ curl -d "Download complete!" ntfy.sh/mytopic/xE73Iyuabi - ``` - -=== "Using a custom sequence ID" +=== "Command line (curl)" ```bash # Publish with a custom sequence ID - $ curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123 + curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123 # Update using the same sequence ID - $ curl -d "Download complete!" ntfy.sh/mytopic/my-download-123 + curl -d "Download complete!" ntfy.sh/mytopic/my-download-123 ``` -=== "Using the X-Sequence-ID header" +=== "ntfy CLI" ```bash - # Publish with a sequence ID via header - $ curl -H "X-Sequence-ID: my-download-123" -d "Downloading..." ntfy.sh/mytopic + # Publish with a sequence ID + ntfy pub --sequence-id=my-download-123 mytopic "Downloading file..." # Update using the same sequence ID - $ curl -H "X-Sequence-ID: my-download-123" -d "Done!" ntfy.sh/mytopic + ntfy pub --sequence-id=my-download-123 mytopic "Download complete!" ``` -You can also set the sequence ID via the `sid` query parameter or when [publishing as JSON](#publish-as-json) using the -`sequence_id` field. +=== "HTTP" + ``` http + POST /mytopic/my-download-123 HTTP/1.1 + Host: ntfy.sh + + Downloading file... + ``` + +=== "JavaScript" + ``` javascript + // First message + await fetch('https://ntfy.sh/mytopic/my-download-123', { + method: 'POST', + body: 'Downloading file...' + }); + + // Update with same sequence ID + await fetch('https://ntfy.sh/mytopic/my-download-123', { + method: 'POST', + body: 'Download complete!' + }); + ``` + +=== "Go" + ``` go + // Publish with sequence ID in URL path + http.Post("https://ntfy.sh/mytopic/my-download-123", "text/plain", + strings.NewReader("Downloading file...")) + + // Update with same sequence ID + http.Post("https://ntfy.sh/mytopic/my-download-123", "text/plain", + strings.NewReader("Download complete!")) + ``` + +=== "PowerShell" + ``` powershell + # Publish with sequence ID + Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Downloading file..." + + # Update with same sequence ID + Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Download complete!" + ``` + +=== "Python" + ``` python + import requests + + # Publish with sequence ID + requests.post("https://ntfy.sh/mytopic/my-download-123", data="Downloading file...") + + # Update with same sequence ID + requests.post("https://ntfy.sh/mytopic/my-download-123", data="Download complete!") + ``` + +=== "PHP" + ``` php-inline + // Publish with sequence ID + file_get_contents('https://ntfy.sh/mytopic/my-download-123', false, stream_context_create([ + 'http' => ['method' => 'POST', 'content' => 'Downloading file...'] + ])); + + // Update with same sequence ID + file_get_contents('https://ntfy.sh/mytopic/my-download-123', false, stream_context_create([ + 'http' => ['method' => 'POST', 'content' => 'Download complete!'] + ])); + ``` + +You can also set the sequence ID via the `X-Sequence-ID` header, the `sid` query parameter, or when +[publishing as JSON](#publish-as-json) using the `sequence_id` field. ### Clearing notifications To clear a notification (mark it as read and dismiss it from the notification drawer), send a PUT request to @@ -1004,11 +1063,41 @@ To clear a notification (mark it as read and dismiss it from the notification dr ``` === "HTTP" - ```http + ``` http PUT /mytopic/my-download-123/clear HTTP/1.1 Host: ntfy.sh ``` +=== "JavaScript" + ``` javascript + await fetch('https://ntfy.sh/mytopic/my-download-123/clear', { + method: 'PUT' + }); + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("PUT", "https://ntfy.sh/mytopic/my-download-123/clear", nil) + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + Invoke-RestMethod -Method PUT -Uri "https://ntfy.sh/mytopic/my-download-123/clear" + ``` + +=== "Python" + ``` python + requests.put("https://ntfy.sh/mytopic/my-download-123/clear") + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.sh/mytopic/my-download-123/clear', false, stream_context_create([ + 'http' => ['method' => 'PUT'] + ])); + ``` + This publishes a `message_clear` event, which tells clients to: - Mark the notification as read in the app @@ -1023,11 +1112,41 @@ To delete a notification entirely, send a DELETE request to `// ['method' => 'DELETE'] + ])); + ``` + This publishes a `message_delete` event, which tells clients to: - Delete the notification from the database From db4a4776d3639d696011b907645112aa37903db7 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 15 Jan 2026 10:07:05 -0500 Subject: [PATCH 370/378] Reword --- docs/publish.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index b8afb092..55788dcd 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -946,7 +946,11 @@ _Supported on:_ :material-android: :material-firefox: You can **update, clear, or delete notifications** that have already been delivered. This is useful for scenarios like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant. -The key concept is the **sequence ID** (`sequence_id` or `sid`): notifications with the same sequence ID are treated as +The way this works is that when you publish a message with a specific **sequence ID**, clients that receive the +notification will treat it as part of a sequence. When a new message with the same sequence ID is published, clients +will update the existing notification instead of creating a new one. You can also + +The key concept is the **sequence ID**: notifications with the same sequence ID are treated as belonging to the same sequence, and clients will update/replace the notification accordingly.
    @@ -963,7 +967,87 @@ You can either: 1. **Use the message ID**: First publish without a sequence ID, then use the returned message `id` as the sequence ID for updates 2. **Use a custom sequence ID**: Publish directly to `//` with your own identifier -Here's an example using a custom sequence ID to update a notification: +If you don't know the sequence ID ahead of time, you can publish a message first and then use the returned +message `id` to update it. The message ID of the first message will then serve as the sequence ID for subsequent updates: + +=== "Command line (curl)" + ```bash + # First, publish a message and capture the message ID + curl -d "Downloading file..." ntfy.sh/mytopic + # Returns: {"id":"xE73Iyuabi","time":1673542291,...} + + # Then use the message ID to update it + curl -d "Download complete!" ntfy.sh/mytopic/xE73Iyuabi + ``` + +=== "ntfy CLI" + ```bash + # First, publish a message and capture the message ID + ntfy pub mytopic "Downloading file..." + # Returns: {"id":"xE73Iyuabi","time":1673542291,...} + + # Then use the message ID to update it + ntfy pub --sequence-id=xE73Iyuabi mytopic "Download complete!" + ``` + +=== "JavaScript" + ``` javascript + // First, publish and get the message ID + const response = await fetch('https://ntfy.sh/mytopic', { + method: 'POST', + body: 'Downloading file...' + }); + const { id } = await response.json(); + + // Then use the message ID to update + await fetch(`https://ntfy.sh/mytopic/${id}`, { + method: 'POST', + body: 'Download complete!' + }); + ``` + +=== "Go" + ``` go + // Publish and parse the response to get the message ID + resp, _ := http.Post("https://ntfy.sh/mytopic", "text/plain", + strings.NewReader("Downloading file...")) + var msg struct { ID string `json:"id"` } + json.NewDecoder(resp.Body).Decode(&msg) + + // Update using the message ID + http.Post("https://ntfy.sh/mytopic/"+msg.ID, "text/plain", + strings.NewReader("Download complete!")) + ``` + +=== "Python" + ``` python + import requests + + # Publish and get the message ID + response = requests.post("https://ntfy.sh/mytopic", data="Downloading file...") + message_id = response.json()["id"] + + # Update using the message ID + requests.post(f"https://ntfy.sh/mytopic/{message_id}", data="Download complete!") + ``` + +=== "PHP" + ``` php-inline + // Publish and get the message ID + $response = file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ + 'http' => ['method' => 'POST', 'content' => 'Downloading file...'] + ])); + $messageId = json_decode($response)->id; + + // Update using the message ID + file_get_contents("https://ntfy.sh/mytopic/$messageId", false, stream_context_create([ + 'http' => ['method' => 'POST', 'content' => 'Download complete!'] + ])); + ``` + +You can also use a custom sequence ID (e.g., a download ID, job ID, etc.) when publishing the first message. This is +less cumbersome, since you don't need to capture the message ID first. Just publish directly to +`//`: === "Command line (curl)" ```bash From e81be48bf302a6b2e40d47a77f370f8009cb48b5 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 15 Jan 2026 10:32:48 -0500 Subject: [PATCH 371/378] MOre docs update --- docs/publish.md | 183 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 148 insertions(+), 35 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 55788dcd..0652fbd8 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -943,16 +943,17 @@ _Supported on:_ :material-android: :material-firefox: !!! info **This feature is not yet released.** It will be available in ntfy v2.16.x and later and ntfy Android v1.22.x and later. -You can **update, clear, or delete notifications** that have already been delivered. This is useful for scenarios +You can **update, clear (mark as read and dismiss), or delete notifications** that have already been delivered. This is useful for scenarios like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant. +* Updating notifications will alter the content of an existing notification. +* Clearing notifications will mark them as read and dismiss them from the notification drawer. +* Deleting notifications will remove them from the notification drawer and remove them in the clients as well (if supported). + The way this works is that when you publish a message with a specific **sequence ID**, clients that receive the notification will treat it as part of a sequence. When a new message with the same sequence ID is published, clients will update the existing notification instead of creating a new one. You can also -The key concept is the **sequence ID**: notifications with the same sequence ID are treated as -belonging to the same sequence, and clients will update/replace the notification accordingly. -
    @@ -960,12 +961,11 @@ belonging to the same sequence, and clients will update/replace the notification ### Updating notifications To update an existing notification, publish a new message with the same sequence ID. Clients will replace the previous -notification with the new one. +notification with the new one. You can either: -You can either: - -1. **Use the message ID**: First publish without a sequence ID, then use the returned message `id` as the sequence ID for updates -2. **Use a custom sequence ID**: Publish directly to `//` with your own identifier +1. **Use the message ID**: First publish like normal to `POST /` without a sequence ID, then use the returned message `id` as the sequence ID for updates +2. **Use a custom sequence ID**: Publish directly to `POST //` with your own identifier, or use `POST /` with the + `X-Sequence-ID` header (or any of its aliases: `Sequence-ID` or`SID`) If you don't know the sequence ID ahead of time, you can publish a message first and then use the returned message `id` to update it. The message ID of the first message will then serve as the sequence ID for subsequent updates: @@ -976,8 +976,11 @@ message `id` to update it. The message ID of the first message will then serve a curl -d "Downloading file..." ntfy.sh/mytopic # Returns: {"id":"xE73Iyuabi","time":1673542291,...} - # Then use the message ID to update it - curl -d "Download complete!" ntfy.sh/mytopic/xE73Iyuabi + # Then use the message ID to update it (via URL path) + curl -d "Download 50% ..." ntfy.sh/mytopic/xE73Iyuabi + + # Or update using the X-Sequence-ID header + curl -H "X-Sequence-ID: xE73Iyuabi" -d "Download complete" ntfy.sh/mytopic ``` === "ntfy CLI" @@ -987,7 +990,34 @@ message `id` to update it. The message ID of the first message will then serve a # Returns: {"id":"xE73Iyuabi","time":1673542291,...} # Then use the message ID to update it - ntfy pub --sequence-id=xE73Iyuabi mytopic "Download complete!" + ntfy pub --sequence-id=xE73Iyuabi mytopic "Download 50% ..." + + # Update again with the same sequence ID + ntfy pub -S xE73Iyuabi mytopic "Download complete" + ``` + +=== "HTTP" + ``` http + # First, publish a message and capture the message ID + POST /mytopic HTTP/1.1 + Host: ntfy.sh + + Downloading file... + + # Returns: {"id":"xE73Iyuabi","time":1673542291,...} + + # Then use the message ID to update it + POST /mytopic/xE73Iyuabi HTTP/1.1 + Host: ntfy.sh + + Download 50% ... + + # Update again with the same sequence ID, this time using the header + POST /mytopic HTTP/1.1 + Host: ntfy.sh + X-Sequence-ID: xE73Iyuabi + + Download complete ``` === "JavaScript" @@ -999,10 +1029,17 @@ message `id` to update it. The message ID of the first message will then serve a }); const { id } = await response.json(); - // Then use the message ID to update + // Update via URL path await fetch(`https://ntfy.sh/mytopic/${id}`, { method: 'POST', - body: 'Download complete!' + body: 'Download 50% ...' + }); + + // Or update using the X-Sequence-ID header + await fetch('https://ntfy.sh/mytopic', { + method: 'POST', + headers: { 'X-Sequence-ID': id }, + body: 'Download complete' }); ``` @@ -1014,9 +1051,29 @@ message `id` to update it. The message ID of the first message will then serve a var msg struct { ID string `json:"id"` } json.NewDecoder(resp.Body).Decode(&msg) - // Update using the message ID + // Update via URL path http.Post("https://ntfy.sh/mytopic/"+msg.ID, "text/plain", - strings.NewReader("Download complete!")) + strings.NewReader("Download 50% ...")) + + // Or update using the X-Sequence-ID header + req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", + strings.NewReader("Download complete")) + req.Header.Set("X-Sequence-ID", msg.ID) + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + # Publish and get the message ID + $response = Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic" -Body "Downloading file..." + $messageId = $response.id + + # Update via URL path + Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/$messageId" -Body "Download 50% ..." + + # Or update using the X-Sequence-ID header + Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic" ` + -Headers @{"X-Sequence-ID"=$messageId} -Body "Download complete" ``` === "Python" @@ -1027,8 +1084,12 @@ message `id` to update it. The message ID of the first message will then serve a response = requests.post("https://ntfy.sh/mytopic", data="Downloading file...") message_id = response.json()["id"] - # Update using the message ID - requests.post(f"https://ntfy.sh/mytopic/{message_id}", data="Download complete!") + # Update via URL path + requests.post(f"https://ntfy.sh/mytopic/{message_id}", data="Download 50% ...") + + # Or update using the X-Sequence-ID header + requests.post("https://ntfy.sh/mytopic", + headers={"X-Sequence-ID": message_id}, data="Download complete") ``` === "PHP" @@ -1039,9 +1100,18 @@ message `id` to update it. The message ID of the first message will then serve a ])); $messageId = json_decode($response)->id; - // Update using the message ID + // Update via URL path file_get_contents("https://ntfy.sh/mytopic/$messageId", false, stream_context_create([ - 'http' => ['method' => 'POST', 'content' => 'Download complete!'] + 'http' => ['method' => 'POST', 'content' => 'Download 50% ...'] + ])); + + // Or update using the X-Sequence-ID header + file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "X-Sequence-ID: $messageId", + 'content' => 'Download complete' + ] ])); ``` @@ -1054,8 +1124,11 @@ less cumbersome, since you don't need to capture the message ID first. Just publ # Publish with a custom sequence ID curl -d "Downloading file..." ntfy.sh/mytopic/my-download-123 - # Update using the same sequence ID - curl -d "Download complete!" ntfy.sh/mytopic/my-download-123 + # Update using the same sequence ID (via URL path) + curl -d "Download 50% ..." ntfy.sh/mytopic/my-download-123 + + # Or update using the X-Sequence-ID header + curl -H "X-Sequence-ID: my-download-123" -d "Download complete" ntfy.sh/mytopic ``` === "ntfy CLI" @@ -1064,7 +1137,10 @@ less cumbersome, since you don't need to capture the message ID first. Just publ ntfy pub --sequence-id=my-download-123 mytopic "Downloading file..." # Update using the same sequence ID - ntfy pub --sequence-id=my-download-123 mytopic "Download complete!" + ntfy pub --sequence-id=my-download-123 mytopic "Download 50% ..." + + # Update again + ntfy pub -S my-download-123 mytopic "Download complete" ``` === "HTTP" @@ -1074,6 +1150,13 @@ less cumbersome, since you don't need to capture the message ID first. Just publ Downloading file... ``` + ``` http + POST /mytopic HTTP/1.1 + Host: ntfy.sh + X-Sequence-ID: my-download-123 + + Download complete + ``` === "JavaScript" ``` javascript @@ -1083,10 +1166,17 @@ less cumbersome, since you don't need to capture the message ID first. Just publ body: 'Downloading file...' }); - // Update with same sequence ID + // Update via URL path await fetch('https://ntfy.sh/mytopic/my-download-123', { method: 'POST', - body: 'Download complete!' + body: 'Download 50% ...' + }); + + // Or update using the X-Sequence-ID header + await fetch('https://ntfy.sh/mytopic', { + method: 'POST', + headers: { 'X-Sequence-ID': 'my-download-123' }, + body: 'Download complete' }); ``` @@ -1096,9 +1186,15 @@ less cumbersome, since you don't need to capture the message ID first. Just publ http.Post("https://ntfy.sh/mytopic/my-download-123", "text/plain", strings.NewReader("Downloading file...")) - // Update with same sequence ID + // Update via URL path http.Post("https://ntfy.sh/mytopic/my-download-123", "text/plain", - strings.NewReader("Download complete!")) + strings.NewReader("Download 50% ...")) + + // Or update using the X-Sequence-ID header + req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", + strings.NewReader("Download complete")) + req.Header.Set("X-Sequence-ID", "my-download-123") + http.DefaultClient.Do(req) ``` === "PowerShell" @@ -1106,8 +1202,12 @@ less cumbersome, since you don't need to capture the message ID first. Just publ # Publish with sequence ID Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Downloading file..." - # Update with same sequence ID - Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Download complete!" + # Update via URL path + Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic/my-download-123" -Body "Download 50% ..." + + # Or update using the X-Sequence-ID header + Invoke-RestMethod -Method POST -Uri "https://ntfy.sh/mytopic" ` + -Headers @{"X-Sequence-ID"="my-download-123"} -Body "Download complete" ``` === "Python" @@ -1117,8 +1217,12 @@ less cumbersome, since you don't need to capture the message ID first. Just publ # Publish with sequence ID requests.post("https://ntfy.sh/mytopic/my-download-123", data="Downloading file...") - # Update with same sequence ID - requests.post("https://ntfy.sh/mytopic/my-download-123", data="Download complete!") + # Update via URL path + requests.post("https://ntfy.sh/mytopic/my-download-123", data="Download 50% ...") + + # Or update using the X-Sequence-ID header + requests.post("https://ntfy.sh/mytopic", + headers={"X-Sequence-ID": "my-download-123"}, data="Download complete") ``` === "PHP" @@ -1128,14 +1232,23 @@ less cumbersome, since you don't need to capture the message ID first. Just publ 'http' => ['method' => 'POST', 'content' => 'Downloading file...'] ])); - // Update with same sequence ID + // Update via URL path file_get_contents('https://ntfy.sh/mytopic/my-download-123', false, stream_context_create([ - 'http' => ['method' => 'POST', 'content' => 'Download complete!'] + 'http' => ['method' => 'POST', 'content' => 'Download 50% ...'] + ])); + + // Or update using the X-Sequence-ID header + file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => 'X-Sequence-ID: my-download-123', + 'content' => 'Download complete' + ] ])); ``` -You can also set the sequence ID via the `X-Sequence-ID` header, the `sid` query parameter, or when -[publishing as JSON](#publish-as-json) using the `sequence_id` field. +You can also set the sequence ID via the `sid` query parameter, or when [publishing as JSON](#publish-as-json) +using the `sequence_id` field. ### Clearing notifications To clear a notification (mark it as read and dismiss it from the notification drawer), send a PUT request to From 94eb121f38f79d6d4a09c5e259d0c6eac6225f03 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 15 Jan 2026 13:07:07 -0500 Subject: [PATCH 372/378] Docs updates --- docs/publish.md | 104 +++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 0652fbd8..3363063a 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -946,19 +946,24 @@ _Supported on:_ :material-android: :material-firefox: You can **update, clear (mark as read and dismiss), or delete notifications** that have already been delivered. This is useful for scenarios like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant. -* Updating notifications will alter the content of an existing notification. -* Clearing notifications will mark them as read and dismiss them from the notification drawer. -* Deleting notifications will remove them from the notification drawer and remove them in the clients as well (if supported). +* [Updating notifications](#updating-notifications) will alter the content of an existing notification. +* [Clearing notifications](#clearing-notifications) will mark them as read and dismiss them from the notification drawer. +* [Deleting notifications](#deleting-notifications) will remove them from the notification drawer and remove them in the clients as well (if supported). -The way this works is that when you publish a message with a specific **sequence ID**, clients that receive the -notification will treat it as part of a sequence. When a new message with the same sequence ID is published, clients -will update the existing notification instead of creating a new one. You can also +Here's an example of a download progress notification being updated over time on Android:
    +To facilitate updating notifications and altering existing notifications, ntfy messages are linked together in a sequence, +using a **sequence ID**. When a notification is meant to be updated, cleared, or deleted, you publish a new message with the +same sequence ID and the clients will perform the appropriate action on the existing notification. + +Existing ntfy messages will not be updated on the server or in the message cache. Instead, a new message is created that indicates +the update, clear, or delete action. This append-only behavior ensures that message history remains intact. + ### Updating notifications To update an existing notification, publish a new message with the same sequence ID. Clients will replace the previous notification with the new one. You can either: @@ -968,7 +973,7 @@ notification with the new one. You can either: `X-Sequence-ID` header (or any of its aliases: `Sequence-ID` or`SID`) If you don't know the sequence ID ahead of time, you can publish a message first and then use the returned -message `id` to update it. The message ID of the first message will then serve as the sequence ID for subsequent updates: +message `id` to update it. Here's an example: === "Command line (curl)" ```bash @@ -1115,8 +1120,8 @@ message `id` to update it. The message ID of the first message will then serve a ])); ``` -You can also use a custom sequence ID (e.g., a download ID, job ID, etc.) when publishing the first message. This is -less cumbersome, since you don't need to capture the message ID first. Just publish directly to +You can also use a **custom sequence ID** (e.g., a download ID, job ID, etc.) when publishing the first message. +**This is less cumbersome**, since you don't need to capture the message ID first. Just publish directly to `//`: === "Command line (curl)" @@ -1145,12 +1150,13 @@ less cumbersome, since you don't need to capture the message ID first. Just publ === "HTTP" ``` http + # Publish a message with a custom sequence ID POST /mytopic/my-download-123 HTTP/1.1 Host: ntfy.sh Downloading file... - ``` - ``` http + + # Update again using the X-Sequence-ID header POST /mytopic HTTP/1.1 Host: ntfy.sh X-Sequence-ID: my-download-123 @@ -1247,12 +1253,24 @@ less cumbersome, since you don't need to capture the message ID first. Just publ ])); ``` -You can also set the sequence ID via the `sid` query parameter, or when [publishing as JSON](#publish-as-json) -using the `sequence_id` field. +You can also set the sequence ID via the `sequence-id` [query parameter](#list-of-all-parameters), or when +[publishing as JSON](#publish-as-json) using the `sequence_id` field. + +If the message ID (`id`) and the sequence ID (`sequence_id`) are different, the ntfy server will include the `sequence_id` +field the response. A sequence of updates may look like this (first example from above): + +```json +{"id":"xE73Iyuabi","time":1673542291,"event":"message","topic":"mytopic","message":"Downloading file..."} +{"id":"yF84Jzvbcj","time":1673542295,"event":"message","topic":"mytopic","sequence_id":"xE73Iyuabi","message":"Download 50% ..."} +{"id":"zG95Kawdde","time":1673542300,"event":"message","topic":"mytopic","sequence_id":"xE73Iyuabi","message":"Download complete"} +``` ### Clearing notifications -To clear a notification (mark it as read and dismiss it from the notification drawer), send a PUT request to -`///clear` (or `///read` as an alias): +Clearing a notification means **marking it as read and dismissing it from the notification drawer**. + +To do this, send a PUT request to the `///clear` endpoint (or `///read` as an alias). +This will then emit a `message_clear` event that is used by the clients (web app and Android app) to update the read status +and dismiss the notification. === "Command line (curl)" ```bash @@ -1295,13 +1313,17 @@ To clear a notification (mark it as read and dismiss it from the notification dr ])); ``` -This publishes a `message_clear` event, which tells clients to: +An example response from the server with the `message_clear` event may look like this: -- Mark the notification as read in the app -- Dismiss the browser/Android notification +```json +{"id":"jkl012","time":1673542305,"event":"message_clear","topic":"mytopic","sequence_id":"my-download-123"} +``` ### Deleting notifications -To delete a notification entirely, send a DELETE request to `//`: +Deleting a notification means **removing it from the notification drawer and from the client's database**. + +To do this, send a DELETE request to the `//` endpoint. This will emit a `message_delete` event +that is used by the clients (web app and Android app) to remove the notification entirely. === "Command line (curl)" ```bash @@ -1344,51 +1366,16 @@ To delete a notification entirely, send a DELETE request to `// Date: Thu, 15 Jan 2026 19:06:05 -0500 Subject: [PATCH 373/378] Refine, docs --- web/public/sw.js | 6 ++++++ web/src/app/Notifier.js | 6 +++--- web/src/components/hooks.js | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/web/public/sw.js b/web/public/sw.js index db87e7f8..7affd55f 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -163,6 +163,12 @@ const handlePushUnknown = async (data) => { const handlePush = async (data) => { const { message } = data; + // This logic is (partially) duplicated in + // - Android: SubscriberService::onNotificationReceived() + // - Android: FirebaseService::onMessageReceived() + // - Web app: hooks.js:handleNotification() + // - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ... + if (message.event === EVENT_MESSAGE) { await handlePushMessage(data); } else if (message.event === EVENT_MESSAGE_DELETE) { diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js index 723ec43d..f6e47a7c 100644 --- a/web/src/app/Notifier.js +++ b/web/src/app/Notifier.js @@ -26,7 +26,7 @@ class Notifier { subscriptionId: subscription.id, message: notification, defaultTitle, - topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString() + topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(), }) ); } @@ -40,7 +40,7 @@ class Notifier { console.log(`[Notifier] Cancelling notification with ${tag}`); const registration = await this.serviceWorkerRegistration(); const notifications = await registration.getNotifications({ tag }); - notifications.forEach(n => n.close()); + notifications.forEach((n) => n.close()); } catch (e) { console.log(`[Notifier] Error cancelling notification`, e); } @@ -72,7 +72,7 @@ class Notifier { if (hasWebPushTopics) { return pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlB64ToUint8Array(config.web_push_public_key) + applicationServerKey: urlB64ToUint8Array(config.web_push_public_key), }); } diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 1c9c2bff..9dadd551 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -51,8 +51,11 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop }; const handleNotification = async (subscriptionId, notification) => { - // Note: This logic is duplicated in the Android app in SubscriberService::onNotificationReceived() - // and FirebaseService::handleMessage(). + // This logic is (partially) duplicated in + // - Android: SubscriberService::onNotificationReceived() + // - Android: FirebaseService::onMessageReceived() + // - Web app: hooks.js:handleNotification() + // - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ... if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) { await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id); From 2cc4bf7d28ba07cf747c28ace78ef1b7a3e944fa Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 15 Jan 2026 21:55:20 -0500 Subject: [PATCH 374/378] Fix webpush stuff --- docs/releases.md | 22 ++++++++++++++-------- server/errors.go | 5 +++-- web/public/sw.js | 33 ++++++++++++++++++++------------- web/src/app/events.js | 4 +++- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 1d80a039..2f3f669e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1603,22 +1603,28 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications): You can now update, - clear (mark as read), or delete notifications using a sequence ID. This enables use cases like progress updates, - replacing outdated notifications, or dismissing notifications from all clients. +* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) + ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536), + [ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) + for the initial implementation) ### ntfy Android app v1.22.x (UNRELEASED) **Features:** -* Support for self-signed certs and client certs for mTLS (#215, #530, ntfy-android#149) +* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) + ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536), + [ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) + for the initial implementation) +* Support for self-signed certs and client certs for mTLS ([#215](https://github.com/binwiederhier/ntfy/issues/215), + [#530](https://github.com/binwiederhier/ntfy/issues/530), [ntfy-android#149](https://github.com/binwiederhier/ntfy-android/pull/149), + thanks to [@cyb3rko](https://github.com/cyb3rko) for reviewing) * Connection error dialog to help diagnose connection issues -* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications): Notifications with - the same sequence ID are updated in place, and `message_delete`/`message_clear` events dismiss notifications **Bug fixes + maintenance:** -* Use server-specific user for attachment downloads (#1529, thanks to @ManInDark for reporting) -* Fix crash in sharing dialog (thanks to @rogeliodh) +* Use server-specific user for attachment downloads ([#1529](https://github.com/binwiederhier/ntfy/issues/1529), + thanks to [@ManInDark](https://github.com/ManInDark) for reporting and testing) +* Fix crash in sharing dialog (thanks to [@rogeliodh](https://github.com/rogeliodh)) * Fix crash when exiting multi-delete in detail view * Fix potential crashes with icon downloader and backuper diff --git a/server/errors.go b/server/errors.go index e8f58d75..a29ff27d 100644 --- a/server/errors.go +++ b/server/errors.go @@ -3,8 +3,9 @@ package server import ( "encoding/json" "fmt" - "heckel.io/ntfy/v2/log" "net/http" + + "heckel.io/ntfy/v2/log" ) // errHTTP is a generic HTTP error for any non-200 HTTP error @@ -125,7 +126,7 @@ var ( errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} - errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#TODO", nil} + errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/web/public/sw.js b/web/public/sw.js index 7affd55f..6b298414 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -6,7 +6,13 @@ import { clientsClaim } from "workbox-core"; import { dbAsync } from "../src/app/db"; import { badge, icon, messageWithSequenceId, toNotificationParams } from "../src/app/notificationUtils"; import initI18n from "../src/app/i18n"; -import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE, EVENT_SUBSCRIPTION_EXPIRING } from "../src/app/events"; +import { + EVENT_MESSAGE, + EVENT_MESSAGE_CLEAR, + EVENT_MESSAGE_DELETE, + WEBPUSH_EVENT_MESSAGE, + WEBPUSH_EVENT_SUBSCRIPTION_EXPIRING, +} from "../src/app/events"; /** * General docs for service workers and PWAs: @@ -161,25 +167,26 @@ const handlePushUnknown = async (data) => { * @param {object} data see server/types.go, type webPushPayload */ const handlePush = async (data) => { - const { message } = data; - // This logic is (partially) duplicated in // - Android: SubscriberService::onNotificationReceived() // - Android: FirebaseService::onMessageReceived() // - Web app: hooks.js:handleNotification() // - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ... - if (message.event === EVENT_MESSAGE) { - await handlePushMessage(data); - } else if (message.event === EVENT_MESSAGE_DELETE) { - await handlePushMessageDelete(data); - } else if (message.event === EVENT_MESSAGE_CLEAR) { - await handlePushMessageClear(data); - } else if (message.event === EVENT_SUBSCRIPTION_EXPIRING) { - await handlePushSubscriptionExpiring(data); - } else { - await handlePushUnknown(data); + if (data.event === WEBPUSH_EVENT_MESSAGE) { + const { message } = data; + if (message.event === EVENT_MESSAGE) { + return await handlePushMessage(data); + } else if (message.event === EVENT_MESSAGE_DELETE) { + return await handlePushMessageDelete(data); + } else if (message.event === EVENT_MESSAGE_CLEAR) { + return await handlePushMessageClear(data); + } + } else if (data.event === WEBPUSH_EVENT_SUBSCRIPTION_EXPIRING) { + return await handlePushSubscriptionExpiring(data); } + + return await handlePushUnknown(data); }; /** diff --git a/web/src/app/events.js b/web/src/app/events.js index 94d7dc79..d5c5ab88 100644 --- a/web/src/app/events.js +++ b/web/src/app/events.js @@ -7,7 +7,9 @@ export const EVENT_MESSAGE = "message"; export const EVENT_MESSAGE_DELETE = "message_delete"; export const EVENT_MESSAGE_CLEAR = "message_clear"; export const EVENT_POLL_REQUEST = "poll_request"; -export const EVENT_SUBSCRIPTION_EXPIRING = "subscription_expiring"; + +export const WEBPUSH_EVENT_MESSAGE = "message"; +export const WEBPUSH_EVENT_SUBSCRIPTION_EXPIRING = "subscription_expiring"; // Check if an event is a notification event (message, delete, or read) export const isNotificationEvent = (event) => event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_CLEAR; From 0ad06e808ffd8e071a6cae252138f49e1d000c20 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 15 Jan 2026 22:14:56 -0500 Subject: [PATCH 375/378] Self-review --- web/src/registerSW.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/src/registerSW.js b/web/src/registerSW.js index 5ae85628..842cf80e 100644 --- a/web/src/registerSW.js +++ b/web/src/registerSW.js @@ -7,8 +7,6 @@ const intervalMS = 60 * 60 * 1000; // https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html const registerSW = () => { console.log("[ServiceWorker] Registering service worker"); - console.log("[ServiceWorker] serviceWorker in navigator:", "serviceWorker" in navigator); - if (!("serviceWorker" in navigator)) { console.warn("[ServiceWorker] Service workers not supported"); return; From fcf57a04e1651702fef521d452aad47431645c29 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 16 Jan 2026 09:36:27 -0500 Subject: [PATCH 376/378] Move event up --- server/message_cache.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index ec1a395e..c0c1ea78 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -31,6 +31,7 @@ const ( mid TEXT NOT NULL, sequence_id TEXT NOT NULL, time INT NOT NULL, + event TEXT NOT NULL, expires INT NOT NULL, topic TEXT NOT NULL, message TEXT NOT NULL, @@ -50,8 +51,7 @@ const ( user TEXT NOT NULL, content_type TEXT NOT NULL, encoding TEXT NOT NULL, - published INT NOT NULL, - event TEXT NOT NULL + published INT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); CREATE INDEX IF NOT EXISTS idx_sequence_id ON messages (sequence_id); @@ -69,50 +69,50 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, event) + INSERT INTO messages (mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesLatestQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND published = 1 ORDER BY time DESC, id DESC LIMIT 1 ` selectMessagesDueQuery = ` - SELECT mid, sequence_id, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, event + SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -410,6 +410,7 @@ func (c *messageCache) addMessages(ms []*message) error { m.ID, m.SequenceID, m.Time, + m.Event, m.Expires, m.Topic, m.Message, @@ -430,7 +431,6 @@ func (c *messageCache) addMessages(ms []*message) error { m.ContentType, m.Encoding, published, - m.Event, ) if err != nil { return err @@ -719,11 +719,12 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, sequenceID, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding, event string + var id, sequenceID, event, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string err := rows.Scan( &id, &sequenceID, ×tamp, + &event, &expires, &topic, &msg, @@ -742,7 +743,6 @@ func readMessage(rows *sql.Rows) (*message, error) { &user, &contentType, &encoding, - &event, ) if err != nil { return nil, err From c1ee163cabeec2bfa41e5c139c3b4429a7db1cdf Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 16 Jan 2026 10:07:09 -0500 Subject: [PATCH 377/378] Remove cache thing --- server/message_cache.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/message_cache.go b/server/message_cache.go index c0c1ea78..342f9687 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -771,10 +771,6 @@ func readMessage(rows *sql.Rows) (*message, error) { URL: attachmentURL, } } - // Clear SequenceID if it equals ID (we do not want the SequenceID in the message output) - if sequenceID == id { - sequenceID = "" - } return &message{ ID: id, SequenceID: sequenceID, From 01435d5fea6759b97aca7e3cbab061fd4f064d81 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 03:20:41 -0500 Subject: [PATCH 378/378] Bump --- go.mod | 14 +++++++------- go.sum | 16 ++++++++++++++++ web/package-lock.json | 18 +++++++++--------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 9ef06c2c..ad61782e 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.5 require ( - cloud.google.com/go/firestore v1.20.0 // indirect - cloud.google.com/go/storage v1.59.0 // indirect + cloud.google.com/go/firestore v1.21.0 // indirect + cloud.google.com/go/storage v1.59.1 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/emersion/go-smtp v0.18.0 @@ -21,7 +21,7 @@ require ( golang.org/x/sync v0.19.0 golang.org/x/term v0.39.0 golang.org/x/time v0.14.0 - google.golang.org/api v0.259.0 + google.golang.org/api v0.260.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -69,7 +69,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -95,9 +95,9 @@ require ( golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 // indirect + google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 241b5556..87ba62a2 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,13 @@ cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute v1.53.0 h1:dILGanjePNsYfZVYYv6K0d4+IPnKX1gn84Fk8jDPNvs= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo= cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo= +cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM= +cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= @@ -20,6 +23,8 @@ cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhO cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58= +cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= @@ -98,6 +103,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU= github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -265,14 +272,23 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ= google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4= +google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4= +google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 h1:wFALHMUiWKkK/x6rSxm79KpSnUyh7ks2E+mel670Dc4= google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:wE6SUYr3iNtF/D0GxVAjT+0CbDFktQNssYs9PVptCt4= +google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 h1:rUamZFBwsWVWg4Yb7iTbwYp81XVHUvOXNdrFCoYRRNE= +google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3/go.mod h1:wE6SUYr3iNtF/D0GxVAjT+0CbDFktQNssYs9PVptCt4= google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 h1:4DKBrmaqeptdEzp21EfrOEh8LE7PJ5ywH6wydSbOfGY= google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E= +google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss= +google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 h1:IY6/YYRrFUk0JPp0xOVctvFIVuRnjccihY5kxf5g0TE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/web/package-lock.json b/web/package-lock.json index 89894356..be2bc07a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3702,9 +3702,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8436,9 +8436,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9121,9 +9121,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": {