Compare commits

..

6 Commits

Author SHA1 Message Date
binwiederhier
9e755a73f0 This works 2026-01-31 20:05:23 -05:00
binwiederhier
857f5742b9 Merge branch 'main' into user-header 2026-01-29 20:03:53 -05:00
binwiederhier
099cad02b8 Sw stuff 2026-01-24 15:54:57 -05:00
binwiederhier
9b1be517ea Remove auth_mode 2026-01-22 20:26:00 -05:00
binwiederhier
b67ffa4f5f Auth logout URL, auth proxy 2026-01-22 20:19:59 -05:00
binwiederhier
46cb9f2b41 User header 2026-01-21 20:14:45 -05:00
38 changed files with 687 additions and 1127 deletions

View File

@@ -37,6 +37,7 @@ ADD go.mod go.sum main.go ./
ADD ./client ./client ADD ./client ./client
ADD ./cmd ./cmd ADD ./cmd ./cmd
ADD ./log ./log ADD ./log ./log
ADD ./payments ./payments
ADD ./server ./server ADD ./server ./server
ADD ./user ./user ADD ./user ./user
ADD ./util ./util ADD ./util ./util

View File

@@ -34,12 +34,6 @@ You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/), available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347). as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
<p>
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img height="50" src="docs/static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="docs/static/img/badge-fdroid.svg"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img height="50" src="docs/static/img/badge-appstore.png"></a>
</p>
<p> <p>
<img src=".github/images/screenshot-curl.png" height="180"> <img src=".github/images/screenshot-curl.png" height="180">
<img src=".github/images/screenshot-web-detail.png" height="180"> <img src=".github/images/screenshot-web-detail.png" height="180">

View File

@@ -95,6 +95,8 @@ var flagsServe = append(
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.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-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-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: "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: "auth-user-header", Aliases: []string{"auth_user_header"}, EnvVars: []string{"NTFY_AUTH_USER_HEADER"}, Value: "", Usage: "if set (along with behind-proxy and auth-file), trust this header to contain the authenticated username (e.g. X-Forwarded-User, Remote-User)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-logout-url", Aliases: []string{"auth_logout_url"}, EnvVars: []string{"NTFY_AUTH_LOGOUT_URL"}, Value: "", Usage: "URL to redirect to when logging out in proxy auth mode (e.g. https://auth.example.com/logout)"}),
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-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: "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)"}), 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)"}),
@@ -206,6 +208,8 @@ func execServe(c *cli.Context) error {
behindProxy := c.Bool("behind-proxy") behindProxy := c.Bool("behind-proxy")
proxyForwardedHeader := c.String("proxy-forwarded-header") proxyForwardedHeader := c.String("proxy-forwarded-header")
proxyTrustedHosts := util.SplitNoEmpty(c.String("proxy-trusted-hosts"), ",") proxyTrustedHosts := util.SplitNoEmpty(c.String("proxy-trusted-hosts"), ",")
authUserHeader := c.String("auth-user-header")
authLogoutURL := c.String("auth-logout-url")
stripeSecretKey := c.String("stripe-secret-key") stripeSecretKey := c.String("stripe-secret-key")
stripeWebhookKey := c.String("stripe-webhook-key") stripeWebhookKey := c.String("stripe-webhook-key")
billingContact := c.String("billing-contact") billingContact := c.String("billing-contact")
@@ -313,7 +317,8 @@ func execServe(c *cli.Context) error {
} else if u.Path != "" { } else if u.Path != "" {
return fmt.Errorf("if set, base-url must not have a path (%s), as hosting ntfy on a sub-path is not supported, e.g. https://ntfy.mydomain.com", u.Path) return fmt.Errorf("if set, base-url must not have a path (%s), as hosting ntfy on a sub-path is not supported, e.g. https://ntfy.mydomain.com", u.Path)
} }
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") { }
if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
return errors.New("if set, upstream-base-url must start with http:// or https://") return errors.New("if set, upstream-base-url must start with http:// or https://")
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") { } else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
return errors.New("if set, upstream-base-url must not end with a slash (/)") return errors.New("if set, upstream-base-url must not end with a slash (/)")
@@ -338,12 +343,21 @@ func execServe(c *cli.Context) error {
if messageSizeLimit > 5*1024*1024 { if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M") return errors.New("message-size-limit cannot be higher than 5M")
} }
} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") { }
if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)") return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" { } else if behindProxy && proxyForwardedHeader == "" {
return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set") return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set")
} else if authUserHeader != "" && !behindProxy {
return errors.New("auth-user-header requires behind-proxy to be set")
} else if authUserHeader != "" && authFile == "" {
return errors.New("auth-user-header requires auth-file to be set")
} else if authUserHeader != "" && enableLogin {
return errors.New("auth-user-header cannot be used with enable-login")
} else if authUserHeader != "" && enableSignup {
return errors.New("auth-user-header cannot be used with enable-signup")
} else if visitorPrefixBitsIPv4 < 1 || visitorPrefixBitsIPv4 > 32 { } else if visitorPrefixBitsIPv4 < 1 || visitorPrefixBitsIPv4 > 32 {
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32") return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 { } else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
@@ -412,6 +426,15 @@ func execServe(c *cli.Context) error {
payments.Setup(stripeSecretKey) payments.Setup(stripeSecretKey)
} }
// Parse Twilio call format template
var twilioCallFormatTemplate *template.Template
if twilioCallFormat != "" {
twilioCallFormatTemplate, err = template.New("").Parse(twilioCallFormat)
if err != nil {
return fmt.Errorf("failed to parse twilio-call-format template: %w", err)
}
}
// Add default forbidden topics // Add default forbidden topics
disallowedTopics = append(disallowedTopics, server.DefaultDisallowedTopics...) disallowedTopics = append(disallowedTopics, server.DefaultDisallowedTopics...)
@@ -437,6 +460,8 @@ func execServe(c *cli.Context) error {
conf.AuthUsers = authUsers conf.AuthUsers = authUsers
conf.AuthAccess = authAccess conf.AuthAccess = authAccess
conf.AuthTokens = authTokens conf.AuthTokens = authTokens
conf.AuthUserHeader = authUserHeader
conf.AuthLogoutURL = authLogoutURL
conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
@@ -459,13 +484,7 @@ func execServe(c *cli.Context) error {
conf.TwilioAuthToken = twilioAuthToken conf.TwilioAuthToken = twilioAuthToken
conf.TwilioPhoneNumber = twilioPhoneNumber conf.TwilioPhoneNumber = twilioPhoneNumber
conf.TwilioVerifyService = twilioVerifyService conf.TwilioVerifyService = twilioVerifyService
if twilioCallFormat != "" { conf.TwilioCallFormat = twilioCallFormatTemplate
tmpl, err := template.New("twiml").Parse(twilioCallFormat)
if err != nil {
return fmt.Errorf("failed to parse twilio-call-format template: %w", err)
}
conf.TwilioCallFormat = tmpl
}
conf.MessageSizeLimit = int(messageSizeLimit) conf.MessageSizeLimit = int(messageSizeLimit)
conf.MessageDelayMax = messageDelayLimit conf.MessageDelayMax = messageDelayLimit
conf.TotalTopicLimit = totalTopicLimit conf.TotalTopicLimit = totalTopicLimit

View File

@@ -4,7 +4,7 @@ or POST requests. I use it to notify myself when scripts fail, or long-running c
## Step 1: Get the app ## Step 1: Get the app
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="static/img/badge-googleplay.png"></a> <a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="static/img/badge-fdroid.svg"></a> <a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="static/img/badge-appstore.png"></a> <a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="static/img/badge-appstore.png"></a>
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store or F-Droid. To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store or F-Droid.

View File

@@ -71,7 +71,7 @@ deb/rpm packages.
The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely 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. 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 B6B7 CFDB 962D 4F1E C4AF`): Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 6B7C CFDB 962D 4F1E C4AF`):
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
@@ -567,18 +567,18 @@ kubectl apply -k /ntfy
cpu: 150m cpu: 150m
memory: 150Mi memory: 150Mi
volumeMounts: volumeMounts:
- mountPath: /etc/ntfy/server.yml - mountPath: /etc/ntfy
subPath: server.yml subPath: server.yml
name: config-volume # generated via configMapGenerator from kustomization file name: config-volume # generated vie configMapGenerator from kustomization file
- mountPath: /var/cache/ntfy - mountPath: /var/cache/ntfy
name: cache-volume # cache volume mounted to persistent volume name: cache-volume #cache volume mounted to persistent volume
volumes: volumes:
- name: config-volume - name: config-volume
configMap: # uses configmap generator to parse server.yml to configmap configMap: # uses configmap generator to parse server.yml to configmap
name: server-config name: server-config
- name: cache-volume - name: cache-volume
persistentVolumeClaim: # stores /cache/ntfy in defined pv persistentVolumeClaim: # stores /cache/ntfy in defined pv
claimName: ntfy-pvc claimName: ntfy-pvc
``` ```
=== "ntfy-pvc.yaml" === "ntfy-pvc.yaml"

View File

@@ -182,8 +182,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to 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) - [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) - [BRun](https://github.com/cbrake/brun) - Native Linux automation platform connecting triggers to actions without containers (Go)
- [Uptime Monitor](https://uptime-monitor.org) - Self-hosted, enterprise-grade uptime monitoring and alerting system (TS)
- [send_to_ntfy_extension](https://github.com/TheDuffman85/send_to_ntfy_extension/) ⭐ - A browser extension to send the notifications to ntfy (JS)
## Blog + forum posts ## Blog + forum posts

View File

@@ -1619,7 +1619,7 @@ And the same example using [JSON publishing](#publish-as-json):
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
topic: "myhome", topic: "myhome",
message: "Somebody retweeted your tweet.", message": "Somebody retweeted your tweet.",
actions: [ actions: [
{ {
action: "view", action: "view",
@@ -1879,7 +1879,7 @@ And the same example using [JSON publishing](#publish-as-json):
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
topic: "wifey", topic: "wifey",
message: "Your wife requested you send a picture of yourself.", message": "Your wife requested you send a picture of yourself.",
actions: [ actions: [
{ {
"action": "broadcast", "action": "broadcast",
@@ -2154,7 +2154,7 @@ And the same example using [JSON publishing](#publish-as-json):
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
topic: "myhome", topic: "myhome",
message: "Garage door has been open for 15 minutes. Close it?", "message": "Garage door has been open for 15 minutes. Close it?",
actions: [ actions: [
{ {
"action": "http", "action": "http",
@@ -2643,7 +2643,7 @@ You can enable templating by setting the `X-Template` header (or its aliases `Te
will use a custom template file from the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`). 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. 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`) * **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`, `title`, and/or `priority` will be parsed as a Go template. 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. See [inline templating](#inline-templating) for more details.
To learn the basics of Go's templating language, please see [template syntax](#template-syntax). To learn the basics of Go's templating language, please see [template syntax](#template-syntax).
@@ -2686,7 +2686,7 @@ and set the `X-Template` header or query parameter to the name of the template f
For example, if you have a template file `/etc/ntfy/templates/myapp.yml`, you can set the header `X-Template: myapp` or 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. 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`, `message`, and `priority` keys, 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. which are interpreted as Go templates.
Here's an **example custom template**: Here's an **example custom template**:
@@ -2704,11 +2704,6 @@ Here's an **example custom template**:
Status: {{ .status }} Status: {{ .status }}
Type: {{ .type | upper }} ({{ .percent }}%) Type: {{ .type | upper }} ({{ .percent }}%)
Server: {{ .server }} Server: {{ .server }}
priority: |
{{ if gt .percent 90.0 }}5
{{ else if gt .percent 75.0 }}4
{{ else }}3
{{ end }}
``` ```
Once you have the template file in place, you can send the payload to your topic using the `X-Template` Once you have the template file in place, you can send the payload to your topic using the `X-Template`
@@ -2790,7 +2785,7 @@ Which will result in a notification that looks like this:
### Inline templating ### Inline templating
When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message`, `title`, and `priority` fields of your 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. webhook payload.
Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh). Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh).
@@ -2846,12 +2841,12 @@ Here's an **easier example with a shorter JSON payload**:
curl \ curl \
--globoff \ --globoff \
-d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \ -d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \
'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}' 'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}'
``` ```
=== "HTTP" === "HTTP"
``` http ``` http
POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}} HTTP/1.1 POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}} HTTP/1.1
Host: ntfy.sh Host: ntfy.sh
{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} {"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
@@ -2859,7 +2854,7 @@ Here's an **easier example with a shorter JSON payload**:
=== "JavaScript" === "JavaScript"
``` javascript ``` javascript
fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', { fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}', {
method: 'POST', method: 'POST',
body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
}) })
@@ -2868,7 +2863,7 @@ Here's an **easier example with a shorter JSON payload**:
=== "Go" === "Go"
``` go ``` go
body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}` body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}`
uri := `https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if eq .error.level "severe"}}5{{else}}3{{end}}` uri := "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}"
req, _ := http.NewRequest("POST", uri, strings.NewReader(body)) req, _ := http.NewRequest("POST", uri, strings.NewReader(body))
http.DefaultClient.Do(req) http.DefaultClient.Do(req)
``` ```
@@ -2878,7 +2873,7 @@ Here's an **easier example with a shorter JSON payload**:
``` powershell ``` powershell
$Request = @{ $Request = @{
Method = "POST" Method = "POST"
URI = 'https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}' URI = "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}"
Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
ContentType = "application/json" ContentType = "application/json"
} }
@@ -2888,14 +2883,14 @@ Here's an **easier example with a shorter JSON payload**:
=== "Python" === "Python"
``` python ``` python
requests.post( requests.post(
'https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}",
data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
) )
``` ```
=== "PHP" === "PHP"
``` php-inline ``` php-inline
file_get_contents('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', false, stream_context_create([ file_get_contents("https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", false, stream_context_create([
'http' => [ 'http' => [
'method' => 'POST', 'method' => 'POST',
'header' => "Content-Type: application/json", 'header' => "Content-Type: application/json",
@@ -2904,9 +2899,9 @@ Here's an **easier example with a shorter JSON payload**:
])); ]));
``` ```
This example uses the `message`/`m`, `title`/`t`, and `priority`/`p` query parameters, but obviously this also works with the This example uses the `message`/`m` and `title`/`t` query parameters, but obviously this also works with the corresponding
corresponding headers. It will send a notification with a title `phil-pc: A severe error has occurred`, a message `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`, and priority `5` (max) if the level is "severe", or `3` (default) otherwise. `Error message: Disk has run out of space`.
### Template syntax ### Template syntax
ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful, ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful,
@@ -2925,7 +2920,7 @@ your templates there first ([example for Grafana alert](https://repeatit.io/#/sh
ntfy supports a subset of the **[Sprig template functions](publish/template-functions.md)** (originally copied from [Sprig](https://github.com/Masterminds/sprig), 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. 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, and priority templates. Below are the functions that are available to use inside your message/title templates.
* [String Functions](publish/template-functions.md#string-functions): `trim`, `trunc`, `substr`, `plural`, etc. * [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. * [String List Functions](publish/template-functions.md#string-list-functions): `splitList`, `sortAlpha`, etc.
@@ -3508,6 +3503,9 @@ Here's an example with a custom message, tags and a priority:
## Updating + deleting notifications ## Updating + deleting notifications
_Supported on:_ :material-android: :material-firefox: _Supported on:_ :material-android: :material-firefox:
!!! info
This feature is not fully released yet. The ntfy Android 1.22.x is being released right now. This may take a week or so.
You can **update, clear (mark as read and dismiss), 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. like download progress updates, replacing outdated information, or dismissing notifications that are no longer relevant.

View File

@@ -12,10 +12,10 @@ 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. Please check out the release notes for [upcoming releases](#not-released-yet) below.
## ntfy Android app v1.22.2 ### ntfy Android app v1.22.2
Released January 20, 2026 Released January 20, 2026
This release adds support for [updating and deleting notifications](publish.md#updating-deleting-notifications) (requires server v2.16.0), This release adds support for [updating and deleting notifications](publish.md#updating--deleting-notifications) (requires server v2.16.0),
as well as [certificate management for self-signed certs and mTLS client certificates](subscribe/phone.md#manage-certificates), as well as [certificate management for self-signed certs and mTLS client certificates](subscribe/phone.md#manage-certificates),
and a new connection error dialog to help [troubleshoot connection issues](subscribe/phone.md#troubleshooting). and a new connection error dialog to help [troubleshoot connection issues](subscribe/phone.md#troubleshooting).
@@ -1665,40 +1665,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet ## Not released yet
### ntfy Android v1.23.x (UNRELEASED) _Nothing here_
**Features:**
* Search within a topic ([#141](https://github.com/binwiederhier/ntfy/issues/141), [ntfy-android#153](https://github.com/binwiederhier/ntfy-android/pull/153), thanks to [@Copephobia](https://github.com/Copephobia) and [@StoyanYonkov](https://github.com/StoyanYonkov) for reporting and sponsoring)
* Add "reconnecting to N topics ..." to foreground notification ([#1101](https://github.com/binwiederhier/ntfy/issues/1101), thanks to [@milosivanovic](https://github.com/milosivanovic) for reporting)
* Improved default server dialog with full-screen UI and stricter URL validation ([#1582](https://github.com/binwiederhier/ntfy/issues/1582))
* Show last notification time for UnifiedPush subscriptions ([#1230](https://github.com/binwiederhier/ntfy/issues/1230), [#1454](https://github.com/binwiederhier/ntfy/issues/1454), thanks to [@Tealk](https://github.com/Tealk) and [@user4andre](https://github.com/user4andre) for reporting)
**Bug fixes + maintenance:**
* Fix `clear=true` on action buttons not marking notification as read ([#1029](https://github.com/binwiederhier/ntfy/issues/1029), thanks to [@ElFishi](https://github.com/ElFishi) for reporting)
* Fix crash when default server URL is missing scheme by auto-prepending `https://` ([#1582](https://github.com/binwiederhier/ntfy/issues/1582), thanks to [@hard-zero1](https://github.com/hard-zero1))
* Fix notification timestamp to use original send time instead of receive time ([#1112](https://github.com/binwiederhier/ntfy/issues/1112), thanks to [@voruti](https://github.com/voruti) for reporting)
* Fix notifications being missed after service restart by using persisted lastNotificationId ([#1591](https://github.com/binwiederhier/ntfy/issues/1591), thanks to @Epifeny for reporting)
### ntfy server v2.17.x (UNRELEASED)
**Features:**
* Web: Show red notification dot on favicon when there are unread messages ([#1017](https://github.com/binwiederhier/ntfy/issues/1017), thanks to [@ad-si](https://github.com/ad-si) for reporting)
* Support templating in the priority field ([#1426](https://github.com/binwiederhier/ntfy/issues/1426), thanks to [@seantomburke](https://github.com/seantomburke) for reporting)
**Bug fixes + maintenance:**
* Web: Fix `clear=true` on action buttons not clearing the notification ([#1029](https://github.com/binwiederhier/ntfy/issues/1029), thanks to [@ElFishi](https://github.com/ElFishi) for reporting)
* Fix crash when commit string is shorter than 7 characters in non-GitHub-Action builds ([#1493](https://github.com/binwiederhier/ntfy/issues/1493), thanks to [@cyrinux](https://github.com/cyrinux) for reporting)
* Fix log spam from `http: response.WriteHeader on hijacked connection` for WebSocket errors ([#1362](https://github.com/binwiederhier/ntfy/issues/1362), thanks to [@bonfiresh](https://github.com/bonfiresh) for reporting)
* Web: Fix Markdown message line height to match plain text (1.5 instead of 1.2) ([#1139](https://github.com/binwiederhier/ntfy/issues/1139), thanks to [@etfz](https://github.com/etfz) for reporting)
* Web: Fix long lines (e.g. JSON) being truncated by adding horizontal scroll ([#1363](https://github.com/binwiederhier/ntfy/issues/1363), thanks to [@v3DJG6GL](https://github.com/v3DJG6GL) for reporting)
* Web: Fix Windows notification icon being cut off ([#884](https://github.com/binwiederhier/ntfy/issues/884), thanks to [@ZhangTianrong](https://github.com/ZhangTianrong) for reporting)
* Web: Use full URL in curl example on empty topic pages ([#1435](https://github.com/binwiederhier/ntfy/issues/1435), [#1535](https://github.com/binwiederhier/ntfy/pull/1535), thanks to [@elmatadoor](https://github.com/elmatadoor) for reporting and [@jjasghar](https://github.com/jjasghar) for the PR)
* Web: Add validation feedback for service URL when adding user ([#1566](https://github.com/binwiederhier/ntfy/issues/1566), thanks to [@jermanuts](https://github.com/jermanuts))
* Refactor: Use `slices.Contains` from stdlib to simplify code ([#1406](https://github.com/binwiederhier/ntfy/pull/1406), thanks to [@tanhuaan](https://github.com/tanhuaan))
* Docs: Remove obsolete `version` field from docker-compose examples ([#1333](https://github.com/binwiederhier/ntfy/issues/1333), thanks to [@seals187](https://github.com/seals187) for reporting and [@cyb3rko](https://github.com/cyb3rko) for fixing)
* Docs: Fix Kustomize config in installation docs ([#1367](https://github.com/binwiederhier/ntfy/issues/1367), thanks to [@toby-griffiths](https://github.com/toby-griffiths))
* Docs: Use SVG F-Droid badge and add app store badges to README ([#1170](https://github.com/binwiederhier/ntfy/issues/1170), thanks to [@PanderMusubi](https://github.com/PanderMusubi) for reporting)

View File

@@ -1,240 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="43 43 560 164"
version="1.1"
id="svg78"
sodipodi:docname="get-it-on-en.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview80"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs8">
<radialGradient
xlink:href="#a"
id="b"
cx="113"
cy="-12.89"
r="59.662"
fx="113"
fy="-12.89"
gradientTransform="matrix(0 1.96105 -1.97781 0 254.507 78.763)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="a">
<stop
offset="0"
style="stop-color:#fff;stop-opacity:.09803922"
id="stop3" />
<stop
offset="1"
style="stop-color:#fff;stop-opacity:0"
id="stop5" />
</linearGradient>
</defs>
<g
transform="translate(-289,-312.362)"
id="g76">
<path
id="rect10"
style="display:inline;overflow:visible;stroke:#a6a6a6;stroke-width:4;marker:none"
d="m 352,355.362 h 520 c 11.08,0 20,8.92 20,20 v 124 c 0,11.08 -8.92,20 -20,20 H 352 c -11.08,0 -20,-8.92 -20,-20 v -124 c 0,-11.08 8.92,-20 20,-20 z" />
<g
aria-label="GET IT ON"
id="text14"
style="font-size:12.3952px;line-height:100%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0;word-spacing:0;display:inline;overflow:visible;fill:#ffffff;stroke-width:1px;marker:none">
<path
d="m 529.2627,398.81787 v -6.6817 h -5.49866 v -2.76599 h 8.83117 v 10.68072 q -1.94952,1.383 -4.29895,2.09949 -2.34942,0.69983 -5.01544,0.69983 -5.83191,0 -9.1311,-3.39917 -3.28253,-3.41583 -3.28253,-9.49768 0,-6.09851 3.28253,-9.49768 3.29919,-3.41583 9.1311,-3.41583 2.43274,0 4.61554,0.59985 2.19947,0.59985 4.04901,1.76623 v 3.58246 q -1.86621,-1.58294 -3.96569,-2.38275 -2.09949,-0.7998 -4.41559,-0.7998 -4.56555,0 -6.86499,2.54937 -2.28278,2.54938 -2.28278,7.59815 0,5.0321 2.28278,7.58148 2.29944,2.54938 6.86499,2.54938 1.78289,0 3.18255,-0.29993 1.39966,-0.31659 2.51606,-0.96643 z"
style="font-size:34.125px"
id="path83" />
<path
d="m 538.74371,377.48975 h 15.7295 v 2.83264 h -12.36365 v 7.36487 h 11.84711 v 2.83264 h -11.84711 v 9.01446 h 12.66357 v 2.83264 h -16.02942 z"
style="font-size:34.125px"
id="path85" />
<path
d="m 556.85596,377.48975 h 21.04486 v 2.83264 h -8.83118 V 402.367 h -3.38251 v -22.04461 h -8.83117 z"
style="font-size:34.125px"
id="path87" />
<path
d="m 591.99738,377.48975 h 3.36584 V 402.367 h -3.36584 z"
style="font-size:34.125px"
id="path89" />
<path
d="m 598.61243,377.48975 h 21.04486 v 2.83264 h -8.83118 V 402.367 h -3.38251 v -22.04461 h -8.83117 z"
style="font-size:34.125px"
id="path91" />
<path
d="m 643.85138,379.77252 q -3.66577,0 -5.83191,2.73267 -2.14947,2.73266 -2.14947,7.44818 0,4.69885 2.14947,7.43152 2.16614,2.73266 5.83191,2.73266 3.66577,0 5.79858,-2.73266 2.14948,-2.73267 2.14948,-7.43152 0,-4.71552 -2.14948,-7.44818 -2.13281,-2.73267 -5.79858,-2.73267 z m 0,-2.73266 q 5.23206,0 8.36462,3.5158 3.13257,3.49915 3.13257,9.39771 0,5.8819 -3.13257,9.3977 -3.13256,3.49915 -8.36462,3.49915 -5.24872,0 -8.39795,-3.49915 -3.13257,-3.49914 -3.13257,-9.3977 0,-5.89856 3.13257,-9.39771 3.14923,-3.5158 8.39795,-3.5158 z"
style="font-size:34.125px"
id="path93" />
<path
d="m 660.61395,377.48975 h 4.53223 l 11.03064,20.81158 v -20.81158 h 3.26587 V 402.367 h -4.53223 L 663.87982,381.55542 V 402.367 h -3.26587 z"
style="font-size:34.125px"
id="path95" />
</g>
<g
aria-label="F-Droid"
id="text18"
style="font-weight:700;font-size:29.7088px;line-height:100%;font-family:Rokkitt;-inkscape-font-specification:'Rokkitt Bold';letter-spacing:0;word-spacing:0;display:inline;overflow:visible;fill:#ffffff;stroke-width:1px;marker:none">
<path
d="m 510.81067,481.24332 v 8.11767 h 27.97119 v -8.11767 l -7.23633,-1.3916 v -18.55469 h 23.65723 v -10.43701 h -23.65723 v -18.60108 h 22.03369 l 0.60303,8.07129 h 10.39063 v -18.5083 h -53.76221 v 8.16406 l 7.18994,1.3916 v 48.47413 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path98" />
<path
d="m 599.13098,465.70377 v -10.43702 h -26.16211 v 10.43702 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path100" />
<path
d="m 637.67834,421.82193 h -30.3833 v 8.16406 l 7.18995,1.3916 v 48.47413 l -7.18995,1.3916 v 8.11767 h 30.3833 c 16.51368,0 28.43506,-11.59668 28.43506,-28.15674 v -11.1792 c 0,-16.51367 -11.92138,-28.20312 -28.43506,-28.20312 z m -9.64843,10.43701 h 8.95263 c 9.69483,0 15.53955,7.23633 15.53955,17.67334 v 11.27197 c 0,10.57618 -5.84472,17.76612 -15.53955,17.76612 h -8.95263 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path102" />
<path
d="m 674.09192,481.24332 v 8.11767 h 26.5332 v -8.11767 l -6.49414,-1.3916 v -24.58497 c 1.48438,-2.82959 3.89649,-4.31396 7.88574,-4.12841 l 6.67969,0.3247 1.43799,-12.47802 c -1.29883,-0.46387 -3.43262,-0.74219 -5.33447,-0.74219 -4.87061,0 -8.62793,3.06152 -10.99366,8.25683 l -0.0928,-1.11328 -0.51025,-6.21582 h -19.80713 v 8.16407 l 7.18994,1.3916 v 31.12549 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path104" />
<path
d="m 713.24231,463.80191 v 0.97412 c 0,15.07569 8.85986,25.55908 23.75,25.55908 14.70459,0 23.61084,-10.48339 23.61084,-25.55908 v -0.97412 c 0,-15.0293 -8.85986,-25.55908 -23.70361,-25.55908 -14.79737,0 -23.65723,10.57617 -23.65723,25.55908 z m 13.54492,0.97412 v -0.97412 c 0,-8.90625 3.06152,-15.12207 10.11231,-15.12207 7.05078,0 10.20507,6.21582 10.20507,15.12207 v 0.97412 c 0,9.0918 -3.10791,15.16846 -10.1123,15.16846 -7.18994,0 -10.20508,-6.03027 -10.20508,-15.16846 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path106" />
<path
d="M 786.16223,428.548 V 416.99771 H 772.15344 V 428.548 Z m -20.08545,52.69532 v 8.11767 h 26.57959 v -8.11767 l -6.49414,-1.3916 v -40.68116 h -20.78125 v 8.16407 l 7.23633,1.3916 v 31.12549 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path108" />
<path
d="m 829.76575,483.23795 1.0205,6.12304 h 18.22999 v -8.11767 l -6.49415,-1.3916 v -62.85401 h -20.78125 v 8.16406 l 7.23633,1.39161 v 17.99804 c -3.01513,-4.03564 -7.05078,-6.30859 -12.06054,-6.30859 -12.43164,0 -19.62159,10.62256 -19.62159,26.44043 v 0.97412 c 0,14.84375 7.14356,24.67773 19.52881,24.67773 5.52002,0 9.7876,-2.45849 12.9419,-7.09716 z m -18.92578,-17.58057 v -0.97412 c 0,-9.46289 2.87597,-15.91065 9.50927,-15.91065 3.89649,0 6.77246,1.85547 8.62793,5.05616 v 21.2915 c -1.85547,3.01514 -4.77783,4.68506 -8.7207,4.68506 -6.67969,0 -9.4165,-5.38086 -9.4165,-14.14795 z"
style="font-size:95px;line-height:100%;font-family:'Roboto Slab';-inkscape-font-specification:'Roboto Slab Bold'"
id="path110" />
</g>
<path
d="m 2.589,1006.862 4.25,5.5"
style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
id="path20" />
<path
d="m 2.611,1005.61 c -0.453,0.011 -0.761,0.188 -0.98,0.448 2.027,2.409 2.368,2.792 5.135,6.221 1.02,1.32 2.082,0.638 1.062,-0.681 l -4.25,-5.5 a 1.24,1.24 0 0 0 -0.967,-0.489"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.298039;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
id="path22" />
<path
d="m 1.622,1006.07 a 1.25,1.25 0 0 0 -0.022,1.557 l 4.25,5.5 c 1.02,1.319 1.15,-0.613 1.15,-0.613 0,0 -3.735,-4.51 -5.378,-6.443"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
id="path24" />
<path
d="m 2.338,1005.844 c -0.438,0 -0.96,0.142 -0.824,0.799 0.103,0.501 4.66,6.074 4.66,6.074 1.02,1.32 2.494,0.677 1.474,-0.642 l -4.234,-5.473 c -0.26,-0.29 -0.608,-0.744 -1.076,-0.758"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="matrix(-2.63159,0,0,2.63157,483.158,-2270.475)"
id="path26" />
<path
d="m 2.589,1006.862 4.25,5.5"
style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
id="path28" />
<path
d="m 2.611,1005.61 c -0.453,0.011 -0.761,0.188 -0.98,0.448 2.027,2.409 2.368,2.792 5.135,6.221 1.02,1.32 2.082,0.638 1.062,-0.681 l -4.25,-5.5 a 1.24,1.24 0 0 0 -0.967,-0.489"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.298039;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
id="path30" />
<path
d="m 1.622,1006.07 a 1.25,1.25 0 0 0 -0.022,1.557 l 4.25,5.5 c 1.02,1.319 1.15,-0.613 1.15,-0.613 0,0 -3.735,-4.51 -5.378,-6.443"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
id="path32" />
<path
d="m 2.338,1005.844 c -0.438,0 -0.96,0.142 -0.824,0.799 0.103,0.501 4.66,6.074 4.66,6.074 1.02,1.32 2.494,0.677 1.474,-0.642 l -4.234,-5.473 c -0.26,-0.29 -0.608,-0.744 -1.076,-0.758"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.475)"
id="path34" />
<g
transform="matrix(2.63159,0,0,2.63157,467.369,-2270.475)"
id="g44">
<path
id="rect36"
style="opacity:1;fill:#aeea00;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m -34,1010.36 h 32 c 1.662,0 3,1.338 3,3 v 6.92 c 0,1.662 -1.338,3 -3,3 h -32 c -1.662,0 -3,-1.338 -3,-3 v -6.92 c 0,-1.662 1.338,-3 3,-3 z" />
<path
id="rect38"
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m -34,1013.279 h 32 c 1.662,0 3,1.338 3,3 v 4 c 0,1.662 -1.338,3 -3,3 h -32 c -1.662,0 -3,-1.338 -3,-3 v -4 c 0,-1.662 1.338,-3 3,-3 z" />
<path
id="rect40"
style="opacity:1;fill:#ffffff;fill-opacity:0.298039;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m -34,1010.362 h 32 c 1.662,0 3,1.338 3,3 v 4 c 0,1.662 -1.338,3 -3,3 h -32 c -1.662,0 -3,-1.338 -3,-3 v -4 c 0,-1.662 1.338,-3 3,-3 z" />
<path
id="rect42"
style="opacity:1;fill:#aeea00;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m -34,1011.5 h 32 c 1.662,0 3,1.0954 3,2.456 v 5.729 c 0,1.3606 -1.338,2.456 -3,2.456 h -32 c -1.662,0 -3,-1.0954 -3,-2.456 v -5.729 c 0,-1.3606 1.338,-2.456 3,-2.456 z" />
</g>
<g
transform="matrix(2.63159,0,0,2.63157,356.842,-2270.745)"
id="g54">
<path
id="rect46"
style="opacity:1;fill:#1976d2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m 8,1024.522 h 32 c 1.662,0 3,1.338 3,3 v 19.84 c 0,1.662 -1.338,3 -3,3 H 8 c -1.662,0 -3,-1.338 -3,-3 v -19.84 c 0,-1.662 1.338,-3 3,-3 z" />
<path
id="rect48"
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m 8,1037.3621 h 32 c 1.662,0 3,1.338 3,3 v 7 c 0,1.662 -1.338,3 -3,3 H 8 c -1.662,0 -3,-1.338 -3,-3 v -7 c 0,-1.662 1.338,-3 3,-3 z" />
<path
id="rect50"
style="opacity:1;fill:#ffffff;fill-opacity:0.2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m 8,1024.442 h 32 c 1.662,0 3,1.338 3,3 v 7 c 0,1.662 -1.338,3 -3,3 H 8 c -1.662,0 -3,-1.338 -3,-3 v -7 c 0,-1.662 1.338,-3 3,-3 z" />
<path
id="rect52"
style="opacity:1;fill:#1976d2;stroke-width:3;stroke-linecap:round;stroke-miterlimit:3"
d="m 8,1025.662 h 32 c 1.662,0 3,1.2122 3,2.718 v 18.124 c 0,1.5058 -1.338,2.718 -3,2.718 H 8 c -1.662,0 -3,-1.2122 -3,-2.718 v -18.124 c 0,-1.5058 1.338,-2.718 3,-2.718 z" />
</g>
<g
transform="matrix(2.63159,0,0,2.63157,356.842,396.264)"
id="g60">
<path
d="m 24,17.75 c -2.88,0 -5.32,1.985 -6.033,4.65 H 21.18 A 3.22,3.22 0 0 1 24,20.75 3.23,3.23 0 0 1 27.25,24 3.23,3.23 0 0 1 24,27.25 3.22,3.22 0 0 1 21.07,25.4 h -3.154 c 0.642,2.766 3.132,4.85 6.084,4.85 3.434,0 6.25,-2.816 6.25,-6.25 0,-3.434 -2.816,-6.25 -6.25,-6.25"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0d47a1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
id="path56" />
<path
id="circle58"
style="opacity:1;fill:none;fill-opacity:0.403922;stroke:#0d47a1;stroke-width:1.9;stroke-linecap:round"
d="M 33.55,24 A 9.5500002,9.5500002 0 0 1 24,33.55 9.5500002,9.5500002 0 0 1 14.45,24 9.5500002,9.5500002 0 0 1 24,14.45 9.5500002,9.5500002 0 0 1 33.55,24 Z" />
</g>
<g
transform="matrix(2.63159,0,0,2.63157,356.842,-2269.159)"
id="g66">
<path
id="ellipse62"
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
d="m 17.75,1016.487 a 3.375,3.875 0 0 1 -3.375,3.875 3.375,3.875 0 0 1 -3.375,-3.875 3.375,3.875 0 0 1 3.375,-3.875 3.375,3.875 0 0 1 3.375,3.875 z" />
<path
id="circle64"
style="opacity:1;fill:#ffffff;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
d="m 17.75,1016.987 a 3.375,3.375 0 0 1 -3.375,3.375 3.375,3.375 0 0 1 -3.375,-3.375 3.375,3.375 0 0 1 3.375,-3.375 3.375,3.375 0 0 1 3.375,3.375 z" />
</g>
<g
transform="matrix(2.63159,0,0,2.63157,408.158,-2269.159)"
id="g72">
<path
id="ellipse68"
style="opacity:1;fill:#263238;fill-opacity:0.2;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
d="m 17.75,1016.487 a 3.375,3.875 0 0 1 -3.375,3.875 3.375,3.875 0 0 1 -3.375,-3.875 3.375,3.875 0 0 1 3.375,-3.875 3.375,3.875 0 0 1 3.375,3.875 z" />
<path
id="circle70"
style="opacity:1;fill:#ffffff;stroke-width:1.9;stroke-linecap:round;stroke-opacity:0.697211"
d="m 17.75,1016.987 a 3.375,3.375 0 0 1 -3.375,3.375 3.375,3.375 0 0 1 -3.375,-3.375 3.375,3.375 0 0 1 3.375,-3.375 3.375,3.375 0 0 1 3.375,3.375 z" />
</g>
<path
d="m 282.715,299.835 a 3.29,3.29 0 0 0 -2.662,5.336 l 9.474,12.261 A 7.9,7.9 0 0 0 289,320.257 v 18.21 a 7.877,7.877 0 0 0 7.895,7.895 h 84.21 A 7.877,7.877 0 0 0 389,338.468 v -18.211 c 0,-0.999 -0.19,-1.949 -0.525,-2.826 l 9.472,-12.26 a 3.29,3.29 0 0 0 -2.433,-5.334 3.29,3.29 0 0 0 -2.772,1.31 l -9.013,11.666 a 7.9,7.9 0 0 0 -2.624,-0.45 h -84.21 c -0.922,0 -1.8,0.163 -2.622,0.45 l -9.015,-11.666 a 3.29,3.29 0 0 0 -2.543,-1.312 m 14.18,49.527 A 7.877,7.877 0 0 0 289,357.257 v 52.21 a 7.877,7.877 0 0 0 7.895,7.895 h 84.21 A 7.877,7.877 0 0 0 389,409.468 v -52.211 a 7.877,7.877 0 0 0 -7.895,-7.895 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#b);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:6.57895;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto"
transform="translate(81,76)"
id="path74" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -5,7 +5,7 @@ on GitHub ([Android](https://github.com/binwiederhier/ntfy-android), [iOS](https
contribute, or [build your own](../develop.md). contribute, or [build your own](../develop.md).
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="../../static/img/badge-googleplay.png"></a> <a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.svg"></a> <a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a> <a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a>
You can get the Android app from [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy), You can get the Android app from [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy),
@@ -82,8 +82,9 @@ you'll see as a permanent notification that looks like this:
<figcaption>Instant delivery foreground notification</figcaption> <figcaption>Instant delivery foreground notification</figcaption>
</figure> </figure>
To turn off this notification, long-press on the foreground notification (screenshot above) and navigate to the Android does not allow you to dismiss this notification, unless you turn off the notification channel in the settings.
settings. Then toggle the "Subscription Service" off: To do so, long-press on the foreground notification (screenshot above) and navigate to the settings. Then toggle the
"Subscription Service" off:
<figure markdown> <figure markdown>
![foreground service](../static/img/notification-settings.png){ width=500 } ![foreground service](../static/img/notification-settings.png){ width=500 }
@@ -101,11 +102,6 @@ notifications. Firebase is overall pretty bad at delivering messages in time, bu
The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in the Google Play flavor of the app. 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 F-Droid flavor. It won't use Firebase for any self-hosted servers, and not at all in the F-Droid flavor.
!!! info "F-Droid: Always instant delivery"
Since the F-Droid build does not include Firebase, **all subscriptions use instant delivery by default**, and
there is no option to disable it. The F-Droid app hides all mentions of "instant delivery" in the UI, since
showing options that can't be changed would only be confusing.
## Publishing messages ## Publishing messages
_Supported on:_ :material-android: _Supported on:_ :material-android:

38
go.mod
View File

@@ -1,14 +1,16 @@
module heckel.io/ntfy/v2 module heckel.io/ntfy/v2
go 1.24.6 go 1.24.0
toolchain go1.24.5
require ( require (
cloud.google.com/go/firestore v1.21.0 // indirect cloud.google.com/go/firestore v1.21.0 // indirect
cloud.google.com/go/storage v1.59.2 // indirect cloud.google.com/go/storage v1.59.1 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/emersion/go-smtp v0.18.0 github.com/emersion/go-smtp v0.18.0
github.com/gabriel-vasile/mimetype v1.4.13 github.com/gabriel-vasile/mimetype v1.4.12
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/mattn/go-sqlite3 v1.14.33 github.com/mattn/go-sqlite3 v1.14.33
github.com/olebedev/when v1.1.0 github.com/olebedev/when v1.1.0
@@ -19,7 +21,7 @@ require (
golang.org/x/sync v0.19.0 golang.org/x/sync v0.19.0
golang.org/x/term v0.39.0 golang.org/x/term v0.39.0
golang.org/x/time v0.14.0 golang.org/x/time v0.14.0
google.golang.org/api v0.265.0 google.golang.org/api v0.262.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@@ -54,7 +56,7 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
@@ -64,12 +66,12 @@ require (
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // 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/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
@@ -82,20 +84,20 @@ require (
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.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.65.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.49.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/grpc v1.78.0 // indirect google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

68
go.sum
View File

@@ -18,8 +18,8 @@ cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= 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 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw= cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58=
cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= 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 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8= firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
@@ -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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94 h1:kkHPnzHm5Ln7WA0XYjrr2ITA0l9Vs6H++Ni//P+SZso=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94/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 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -68,8 +68,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= 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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -81,8 +81,8 @@ 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 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
@@ -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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= 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/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= 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 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -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= 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4= 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.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= 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.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= 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.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= 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 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
@@ -263,16 +263,16 @@ 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= 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/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20260203192932-546029d2fa20 h1:/CU1zrxTpGylJJbe3Ru94yy6sZRbzALq2/oxl3pGB3U= google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d h1:hUplc9kLwH374NIY3PreRUK3Unc0xLm/W7MDsm0gCNo=
google.golang.org/genproto v0.0.0-20260203192932-546029d2fa20/go.mod h1:Tt+08/KdKEt3l8x3Pby3HLQxMB3uk/MzaQ4ZIv0ORTs= google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:SpjiK7gGN2j/djoQMxLl3QOe/J/XxNzC5M+YLecVVWU=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= 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/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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

View File

@@ -166,6 +166,8 @@ type Config struct {
BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported) 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) 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 ProxyTrustedPrefixes []netip.Prefix // List of trusted proxy networks (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true
AuthUserHeader string // Header to read the authenticated user from, if BehindProxy is true (e.g. X-Forwarded-User, Remote-User)
AuthLogoutURL string // URL to redirect to when logging out in proxy auth mode (e.g. https://auth.example.com/logout)
StripeSecretKey string StripeSecretKey string
StripeWebhookKey string StripeWebhookKey string
StripePriceCacheDuration time.Duration StripePriceCacheDuration time.Duration
@@ -263,6 +265,8 @@ func NewConfig() *Config {
VisitorPrefixBitsIPv6: DefaultVisitorPrefixBitsIPv6, // Default: use /64 for IPv6 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 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 ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs
AuthUserHeader: "", // Header to read the authenticated user from (requires behind-proxy and auth-file)
AuthLogoutURL: "", // URL to redirect to when logging out in proxy auth mode
StripeSecretKey: "", StripeSecretKey: "",
StripeWebhookKey: "", StripeWebhookKey: "",
StripePriceCacheDuration: DefaultStripePriceCacheDuration, StripePriceCacheDuration: DefaultStripePriceCacheDuration,

View File

@@ -78,21 +78,6 @@ func (e errHTTP) clone() errHTTP {
} }
} }
// errWebSocketPostUpgrade is a wrapper error indicating an error occurred after the WebSocket
// upgrade completed (i.e., the connection was hijacked). This is used to avoid calling
// WriteHeader on hijacked connections, which causes log spam.
type errWebSocketPostUpgrade struct {
err error
}
func (e *errWebSocketPostUpgrade) Error() string {
return e.err.Error()
}
func (e *errWebSocketPostUpgrade) Unwrap() error {
return e.err
}
var ( var (
errHTTPBadRequest = &errHTTP{40000, http.StatusBadRequest, "invalid request", "", nil} errHTTPBadRequest = &errHTTP{40000, http.StatusBadRequest, "invalid request", "", nil}
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications", nil} errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications", nil}

View File

@@ -1,16 +1,14 @@
package server package server
import ( import (
"errors"
"fmt" "fmt"
"net/http"
"strings"
"unicode/utf8"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"net/http"
"strings"
"unicode/utf8"
) )
// Log tags // Log tags
@@ -85,8 +83,7 @@ func httpContext(r *http.Request) log.Context {
} }
func websocketErrorContext(err error) log.Context { func websocketErrorContext(err error) log.Context {
var c *websocket.CloseError if c, ok := err.(*websocket.CloseError); ok {
if errors.As(err, &c) {
return log.Context{ return log.Context{
"error": c.Error(), "error": c.Error(),
"error_code": c.Code, "error_code": c.Code,

View File

@@ -434,14 +434,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor,
} else { } else {
ev.Info("WebSocket error: %s", err.Error()) ev.Info("WebSocket error: %s", err.Error())
} }
// Write error response only if the connection was not hijacked yet. Bytes written to hijacked w.WriteHeader(httpErr.HTTPCode)
// connections are WebSocket frames, not HTTP, and will cause "http: response.WriteHeader on hijacked return // Do not attempt to write any body to upgraded connection
// connection" log spam.
var postUpgradeErr *errWebSocketPostUpgrade
if !errors.As(err, &postUpgradeErr) {
w.WriteHeader(httpErr.HTTPCode)
}
return
} }
if isNormalError { if isNormalError {
ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code) ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code)
@@ -626,6 +620,10 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
} }
func (s *Server) configResponse() *apiConfigResponse { func (s *Server) configResponse() *apiConfigResponse {
authMode := ""
if s.config.AuthUserHeader != "" {
authMode = "proxy"
}
return &apiConfigResponse{ return &apiConfigResponse{
BaseURL: "", // Will translate to window.location.origin BaseURL: "", // Will translate to window.location.origin
AppRoot: s.config.WebRoot, AppRoot: s.config.WebRoot,
@@ -641,6 +639,8 @@ func (s *Server) configResponse() *apiConfigResponse {
WebPushPublicKey: s.config.WebPushPublicKey, WebPushPublicKey: s.config.WebPushPublicKey,
DisallowedTopics: s.config.DisallowedTopics, DisallowedTopics: s.config.DisallowedTopics,
ConfigHash: s.config.Hash(), ConfigHash: s.config.Hash(),
AuthMode: authMode,
AuthLogoutURL: s.config.AuthLogoutURL,
} }
} }
@@ -673,6 +673,11 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visito
// handleStatic returns all static resources (excluding the docs), including the web app // handleStatic returns all static resources (excluding the docs), including the web app
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error { func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
r.URL.Path = webSiteDir + r.URL.Path r.URL.Path = webSiteDir + r.URL.Path
// Prevent caching of HTML files to ensure auth proxies can intercept unauthenticated requests.
// Static hashed assets (JS, CSS, images) can still be cached normally.
if strings.HasSuffix(r.URL.Path, ".html") {
w.Header().Set("Cache-Control", "no-store")
}
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
return nil return nil
} }
@@ -793,7 +798,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
return nil, err return nil, err
} }
m := newDefaultMessage(t.ID, "") m := newDefaultMessage(t.ID, "")
cache, firebase, email, call, template, unifiedpush, priorityStr, e := s.parsePublishParams(r, m) cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m)
if e != nil { if e != nil {
return nil, e.With(t) return nil, e.With(t)
} }
@@ -824,7 +829,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if cache { if cache {
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
} }
if err := s.handlePublishBody(r, v, m, body, template, unifiedpush, priorityStr); err != nil { if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
return nil, err return nil, err
} }
if m.Message == "" { if m.Message == "" {
@@ -1055,11 +1060,11 @@ 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, priorityStr string, err *errHTTP) { 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) { if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) {
pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path) pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path)
if err != nil { if err != nil {
return false, false, "", "", "", false, "", err return false, false, "", "", "", false, err
} }
m.SequenceID = pathSequenceID m.SequenceID = pathSequenceID
} else { } else {
@@ -1068,7 +1073,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
if sequenceIDRegex.MatchString(sequenceID) { if sequenceIDRegex.MatchString(sequenceID) {
m.SequenceID = sequenceID m.SequenceID = sequenceID
} else { } else {
return false, false, "", "", "", false, "", errHTTPBadRequestSequenceIDInvalid return false, false, "", "", "", false, errHTTPBadRequestSequenceIDInvalid
} }
} else { } else {
m.SequenceID = m.ID m.SequenceID = m.ID
@@ -1089,7 +1094,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
} }
if attach != "" { if attach != "" {
if !urlRegex.MatchString(attach) { if !urlRegex.MatchString(attach) {
return false, false, "", "", "", false, "", errHTTPBadRequestAttachmentURLInvalid return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid
} }
m.Attachment.URL = attach m.Attachment.URL = attach
if m.Attachment.Name == "" { if m.Attachment.Name == "" {
@@ -1107,19 +1112,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
} }
if icon != "" { if icon != "" {
if !urlRegex.MatchString(icon) { if !urlRegex.MatchString(icon) {
return false, false, "", "", "", false, "", errHTTPBadRequestIconURLInvalid return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid
} }
m.Icon = icon m.Icon = icon
} }
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
if s.smtpSender == nil && email != "" { if s.smtpSender == nil && email != "" {
return false, false, "", "", "", false, "", errHTTPBadRequestEmailDisabled return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled
} }
call = readParam(r, "x-call", "call") call = readParam(r, "x-call", "call")
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
return false, false, "", "", "", false, "", errHTTPBadRequestPhoneCallsDisabled return false, false, "", "", "", false, errHTTPBadRequestPhoneCallsDisabled
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
return false, false, "", "", "", false, "", errHTTPBadRequestPhoneNumberInvalid return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid
} }
template = templateMode(readParam(r, "x-template", "template", "tpl")) template = templateMode(readParam(r, "x-template", "template", "tpl"))
messageStr := readParam(r, "x-message", "message", "m") messageStr := readParam(r, "x-message", "message", "m")
@@ -1131,33 +1136,29 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
m.Message = messageStr m.Message = messageStr
} }
var e error var e error
priorityStr = readParam(r, "x-priority", "priority", "prio", "p") m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
if !template.Enabled() { if e != nil {
m.Priority, e = util.ParsePriority(priorityStr) return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid
if e != nil {
return false, false, "", "", "", false, "", errHTTPBadRequestPriorityInvalid
}
priorityStr = "" // Clear since it's already parsed
} }
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
if delayStr != "" { if delayStr != "" {
if !cache { if !cache {
return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoCache return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache
} }
if email != "" { if email != "" {
return 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 != "" { if call != "" {
return 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()) delay, err := util.ParseFutureTime(delayStr, time.Now())
if err != nil { if err != nil {
return false, false, "", "", "", false, "", errHTTPBadRequestDelayCannotParse return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
return false, false, "", "", "", false, "", errHTTPBadRequestDelayTooSmall return false, false, "", "", "", false, errHTTPBadRequestDelayTooSmall
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
return false, false, "", "", "", false, "", errHTTPBadRequestDelayTooLarge return false, false, "", "", "", false, errHTTPBadRequestDelayTooLarge
} }
m.Time = delay.Unix() m.Time = delay.Unix()
} }
@@ -1165,7 +1166,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
if actionsStr != "" { if actionsStr != "" {
m.Actions, e = parseActions(actionsStr) m.Actions, e = parseActions(actionsStr)
if e != nil { if e != nil {
return 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") contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
@@ -1184,7 +1185,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
cache = false cache = false
email = "" email = ""
} }
return cache, firebase, email, call, template, unifiedpush, priorityStr, nil return cache, firebase, email, call, template, unifiedpush, nil
} }
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
@@ -1203,7 +1204,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 // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 7. curl -T file.txt ntfy.sh/mytopic // 7. curl -T file.txt ntfy.sh/mytopic
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment // 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 templateMode, unifiedpush bool, priorityStr string) 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 if m.Event == pollRequestEvent { // Case 1
return s.handleBodyDiscard(body) return s.handleBodyDiscard(body)
} else if unifiedpush { } else if unifiedpush {
@@ -1213,7 +1214,7 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
} else if m.Attachment != nil && m.Attachment.Name != "" { } else if m.Attachment != nil && m.Attachment.Name != "" {
return s.handleBodyAsAttachment(r, v, m, body) // Case 4 return s.handleBodyAsAttachment(r, v, m, body) // Case 4
} else if template.Enabled() { } else if template.Enabled() {
return s.handleBodyAsTemplatedTextMessage(m, template, body, priorityStr) // Case 5 return s.handleBodyAsTemplatedTextMessage(m, template, body) // Case 5
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
return s.handleBodyAsTextMessage(m, body) // Case 6 return s.handleBodyAsTextMessage(m, body) // Case 6
} }
@@ -1249,7 +1250,7 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
return nil return nil
} }
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser, priorityStr string) error { func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser) error {
body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit)) body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit))
if err != nil { if err != nil {
return err return err
@@ -1262,7 +1263,7 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM
return err return err
} }
} else { } else {
if err := s.renderTemplateFromParams(m, peekedBody, priorityStr); err != nil { if err := s.renderTemplateFromParams(m, peekedBody); err != nil {
return err return err
} }
} }
@@ -1293,51 +1294,33 @@ func (s *Server) renderTemplateFromFile(m *message, templateName, peekedBody str
} }
var err error var err error
if tpl.Message != nil { if tpl.Message != nil {
if m.Message, err = s.renderTemplate(templateName+" (message)", *tpl.Message, peekedBody); err != nil { if m.Message, err = s.renderTemplate(*tpl.Message, peekedBody); err != nil {
return err return err
} }
} }
if tpl.Title != nil { if tpl.Title != nil {
if m.Title, err = s.renderTemplate(templateName+" (title)", *tpl.Title, peekedBody); err != nil { if m.Title, err = s.renderTemplate(*tpl.Title, peekedBody); err != nil {
return err return err
} }
} }
if tpl.Priority != nil {
renderedPriority, err := s.renderTemplate(templateName+" (priority)", *tpl.Priority, peekedBody)
if err != nil {
return err
}
if m.Priority, err = util.ParsePriority(renderedPriority); err != nil {
return errHTTPBadRequestPriorityInvalid
}
}
return nil return nil
} }
// renderTemplateFromParams transforms the JSON message body according to the inline template in the // renderTemplateFromParams transforms the JSON message body according to the inline template in the
// message, title, and priority parameters. // message and title parameters.
func (s *Server) renderTemplateFromParams(m *message, peekedBody string, priorityStr string) error { func (s *Server) renderTemplateFromParams(m *message, peekedBody string) error {
var err error var err error
if m.Message, err = s.renderTemplate("priority query parameter", m.Message, peekedBody); err != nil { if m.Message, err = s.renderTemplate(m.Message, peekedBody); err != nil {
return err return err
} }
if m.Title, err = s.renderTemplate("title query parameter", m.Title, peekedBody); err != nil { if m.Title, err = s.renderTemplate(m.Title, peekedBody); err != nil {
return err return err
} }
if priorityStr != "" {
renderedPriority, err := s.renderTemplate("priority query parameter", priorityStr, peekedBody)
if err != nil {
return err
}
if m.Priority, err = util.ParsePriority(renderedPriority); err != nil {
return errHTTPBadRequestPriorityInvalid
}
}
return nil return nil
} }
// renderTemplate renders a template with the given JSON source data. // renderTemplate renders a template with the given JSON source data.
func (s *Server) renderTemplate(name, tpl, source string) (string, error) { func (s *Server) renderTemplate(tpl string, source string) (string, error) {
if templateDisallowedRegex.MatchString(tpl) { if templateDisallowedRegex.MatchString(tpl) {
return "", errHTTPBadRequestTemplateDisallowedFunctionCalls return "", errHTTPBadRequestTemplateDisallowedFunctionCalls
} }
@@ -1352,7 +1335,7 @@ func (s *Server) renderTemplate(name, tpl, source string) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes)) limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes))
if err := t.Execute(limitWriter, data); err != nil { if err := t.Execute(limitWriter, data); err != nil {
return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("template %s: %s", name, err.Error()) return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error())
} }
return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines
} }
@@ -1458,15 +1441,12 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
return err return err
} }
var wlock sync.Mutex var wlock sync.Mutex
var closed bool
defer func() { defer func() {
// This blocks until any in-flight sub() call finishes writing/flushing the response writer, // Hack: This is the fix for a horrible data race that I have not been able to figure out in quite some time.
// then marks the connection as closed so future sub() calls are no-ops. This prevents a panic // It appears to be happening when the Go HTTP code reads from the socket when closing the request (i.e. AFTER
// from writing to a response writer that has been cleaned up after the handler returns. // this function returns), and causes a data race with the ResponseWriter. Locking wlock here silences the
// See https://github.com/binwiederhier/ntfy/issues/338#issuecomment-1163425889. // data race detector. See https://github.com/binwiederhier/ntfy/issues/338#issuecomment-1163425889.
wlock.Lock() wlock.TryLock()
closed = true
wlock.Unlock()
}() }()
sub := func(v *visitor, msg *message) error { sub := func(v *visitor, msg *message) error {
if !filters.Pass(msg) { if !filters.Pass(msg) {
@@ -1478,9 +1458,6 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
} }
wlock.Lock() wlock.Lock()
defer wlock.Unlock() defer wlock.Unlock()
if closed {
return nil
}
if _, err := w.Write([]byte(m)); err != nil { if _, err := w.Write([]byte(m)); err != nil {
return err return err
} }
@@ -1671,10 +1648,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Trace("WebSocket connection closed") logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Trace("WebSocket connection closed")
return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot
} }
if err != nil { return err
return &errWebSocketPostUpgrade{err}
}
return nil
} }
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) { func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
@@ -2177,6 +2151,24 @@ func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
if s.userManager == nil { if s.userManager == nil {
return vip, nil return vip, nil
} }
// Check for proxy-forwarded user header (requires behind-proxy and auth-user-header to be set)
if s.config.BehindProxy && s.config.AuthUserHeader != "" {
if username := strings.TrimSpace(r.Header.Get(s.config.AuthUserHeader)); username != "" {
u, err := s.userManager.User(username)
if err != nil {
logr(r).Err(err).Debug("User from auth-user-header not found")
return vip, errHTTPUnauthorized
}
if u.Deleted {
logr(r).Debug("User from auth-user-header is deleted")
return vip, errHTTPUnauthorized
}
logr(r).Debug("User from header found")
return s.visitor(ip, u), nil
}
// If auth-user-header is set, but no user was provided, return unauthorized
return vip, errHTTPUnauthorized
}
header, err := readAuthHeader(r) header, err := readAuthHeader(r)
if err != nil { if err != nil {
return vip, err return vip, err

View File

@@ -124,6 +124,27 @@
# proxy-forwarded-header: "X-Forwarded-For" # proxy-forwarded-header: "X-Forwarded-For"
# proxy-trusted-hosts: # proxy-trusted-hosts:
# If set (along with behind-proxy and auth-file), trust this header to contain the authenticated
# username. This is useful when running ntfy behind an authentication proxy like Authelia,
# Authentik, or Caddy Security that handles authentication and forwards the user identity.
#
# Common header names:
# - X-Forwarded-User (Authelia default)
# - Remote-User (common convention)
# - X-Remote-User
#
# IMPORTANT: Only enable this if you trust the proxy to authenticate users. The header value
# is trusted unconditionally when behind-proxy is also set. Users must be pre-provisioned in
# the ntfy database (via auth-file); they are not auto-created.
#
# auth-user-header:
# If auth-user-header is set, this is the URL to redirect users to when they click logout.
# This is typically the logout URL of your authentication proxy (e.g. Authelia, Authentik).
# If not set, the logout button will be hidden in the web UI when using proxy auth.
#
# auth-logout-url:
# If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments # If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments
# are "attachment-cache-dir" and "base-url". # are "attachment-cache-dir" and "base-url".
# #
@@ -160,7 +181,7 @@
# If enabled, allow outgoing e-mail notifications via the 'X-Email' header. If this header is set, # 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. # messages will additionally be sent out as e-mail using an external SMTP server.
# #
# As of today, only SMTP servers with plain text auth (or no auth at all), and STARTTLS are supported. # As of today, only SMTP servers with plain text auth (or no auth at all), and STARTLS are supported.
# Please also refer to the rate limiting settings below (visitor-email-limit-burst & visitor-email-limit-burst). # Please also refer to the rate limiting settings below (visitor-email-limit-burst & visitor-email-limit-burst).
# #
# - smtp-sender-addr is the hostname:port of the SMTP server # - smtp-sender-addr is the hostname:port of the SMTP server
@@ -198,8 +219,8 @@
# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 # - 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-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-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-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-expiry-duration defines the duration after which unused subscriptions will expire (default is 60d)
# #
# web-push-public-key: # web-push-public-key:
@@ -280,7 +301,7 @@
# #
# - upstream-base-url is the base URL of the upstream server. Should be "https://ntfy.sh". # - upstream-base-url is the base URL of the upstream server. Should be "https://ntfy.sh".
# - upstream-access-token is the token used to authenticate with the upstream server. This is only required # - upstream-access-token is the token used to authenticate with the upstream server. This is only required
# if you exceed the upstream rate limits, or the upstream server requires authentication. # if you exceed the upstream rate limits, or the uptream server requires authentication.
# #
# upstream-base-url: # upstream-base-url:
# upstream-access-token: # upstream-access-token:

View File

@@ -7,7 +7,6 @@ import (
_ "embed" _ "embed"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -2399,6 +2398,102 @@ func TestServer_Visitor_Custom_Forwarded_Header_IPv6(t *testing.T) {
require.Equal(t, "2001:db8:3333::1", v.ip.String()) require.Equal(t, "2001:db8:3333::1", v.ip.String())
} }
func TestServer_AuthUserHeader_Success(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "X-Forwarded-User"
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
r.Header.Set("X-Forwarded-User", "phil")
v, err := s.maybeAuthenticate(r)
require.Nil(t, err)
require.NotNil(t, v.User())
require.Equal(t, "phil", v.User().Name)
}
func TestServer_AuthUserHeader_UserNotFound(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "X-Forwarded-User"
s := newTestServer(t, c)
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
r.Header.Set("X-Forwarded-User", "unknown-user")
_, err := s.maybeAuthenticate(r)
require.Equal(t, errHTTPUnauthorized, err)
}
func TestServer_AuthUserHeader_NoHeader_ReturnsUnauthorized(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "X-Forwarded-User"
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
// No X-Forwarded-User header, even with Authorization header -> unauthorized
// When auth-user-header is configured, the header MUST be present
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
r.Header.Set("Authorization", util.BasicAuth("phil", "phil"))
_, err := s.maybeAuthenticate(r)
require.Equal(t, errHTTPUnauthorized, err)
}
func TestServer_AuthUserHeader_NoHeader_NoAuthReturnsUnauthorized(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "X-Forwarded-User"
s := newTestServer(t, c)
// No X-Forwarded-User header and no Authorization header -> unauthorized
// When auth-user-header is configured, the header MUST be present
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
_, err := s.maybeAuthenticate(r)
require.Equal(t, errHTTPUnauthorized, err)
}
func TestServer_AuthUserHeader_NotBehindProxy(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = false // Auth user header should be ignored if not behind proxy
c.AuthUserHeader = "X-Forwarded-User"
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
// Header is present but should be ignored since behind-proxy is false
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
r.Header.Set("X-Forwarded-User", "phil")
v, err := s.maybeAuthenticate(r)
require.Nil(t, err)
require.Nil(t, v.User()) // Should be anonymous since header is ignored
}
func TestServer_AuthUserHeader_RemoteUser(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "Remote-User" // Common alternative header name
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false))
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
r.Header.Set("Remote-User", "admin")
v, err := s.maybeAuthenticate(r)
require.Nil(t, err)
require.NotNil(t, v.User())
require.Equal(t, "admin", v.User().Name)
require.True(t, v.User().IsAdmin())
}
func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
t.Parallel() t.Parallel()
count := 50000 count := 50000
@@ -3290,117 +3385,6 @@ func TestServer_MessageTemplate_Until100_000(t *testing.T) {
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations") require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
} }
func TestServer_MessageTemplate_Priority(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic", `{"priority":"5"}`, map[string]string{
"X-Message": "Test message",
"X-Priority": "{{.priority}}",
"X-Template": "1",
})
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "Test message", m.Message)
require.Equal(t, 5, m.Priority)
}
func TestServer_MessageTemplate_Priority_Conditional(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
// Test with error status -> priority 5
response := request(t, s, "PUT", "/mytopic", `{"status":"Error","message":"Something went wrong"}`, map[string]string{
"X-Message": "Status: {{.status}} - {{.message}}",
"X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`,
"X-Template": "1",
})
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "Status: Error - Something went wrong", m.Message)
require.Equal(t, 5, m.Priority)
// Test with success status -> priority 3
response = request(t, s, "PUT", "/mytopic", `{"status":"Success","message":"All good"}`, map[string]string{
"X-Message": "Status: {{.status}} - {{.message}}",
"X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`,
"X-Template": "1",
})
require.Equal(t, 200, response.Code)
m = toMessage(t, response.Body.String())
require.Equal(t, "Status: Success - All good", m.Message)
require.Equal(t, 3, m.Priority)
}
func TestServer_MessageTemplate_Priority_NamedValue(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic", `{"severity":"high"}`, map[string]string{
"X-Message": "Alert",
"X-Priority": "{{.severity}}",
"X-Template": "1",
})
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, 4, m.Priority) // "high" = 4
}
func TestServer_MessageTemplate_Priority_Invalid(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic", `{"priority":"invalid"}`, map[string]string{
"X-Message": "Test message",
"X-Priority": "{{.priority}}",
"X-Template": "1",
})
require.Equal(t, 400, response.Code)
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
}
func TestServer_MessageTemplate_Priority_QueryParam(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic?template=1&priority={{.priority}}", `{"priority":"max"}`, nil)
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, 5, m.Priority) // "max" = 5
}
func TestServer_MessageTemplate_Priority_FromTemplateFile(t *testing.T) {
t.Parallel()
c := newTestConfig(t)
c.TemplateDir = t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "priority-test.yml"), []byte(`
title: "{{.title}}"
message: "{{.message}}"
priority: '{{if eq .level "critical"}}5{{else if eq .level "warning"}}4{{else}}3{{end}}'
`), 0644))
s := newTestServer(t, c)
// Test with critical level
response := request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"System down","level":"critical"}`, nil)
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "Alert", m.Title)
require.Equal(t, "System down", m.Message)
require.Equal(t, 5, m.Priority)
// Test with warning level
response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"High load","level":"warning"}`, nil)
require.Equal(t, 200, response.Code)
m = toMessage(t, response.Body.String())
require.Equal(t, 4, m.Priority)
// Test with info level
response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"All good","level":"info"}`, nil)
require.Equal(t, 200, response.Code)
m = toMessage(t, response.Body.String())
require.Equal(t, 3, m.Priority)
}
func TestServer_DeleteMessage(t *testing.T) { func TestServer_DeleteMessage(t *testing.T) {
t.Parallel() t.Parallel()
s := newTestServer(t, newTestConfig(t)) s := newTestServer(t, newTestConfig(t))
@@ -3872,61 +3856,3 @@ func waitForWithMaxWait(t *testing.T, maxWait time.Duration, f func() bool) {
} }
t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack())) t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack()))
} }
// mockResponseWriter is a mock ResponseWriter for testing
type mockResponseWriter struct {
header http.Header
statusCode int
body []byte
writeHeaderHit bool
}
func newMockResponseWriter() *mockResponseWriter {
return &mockResponseWriter{
header: make(http.Header),
}
}
func (m *mockResponseWriter) Header() http.Header {
return m.header
}
func (m *mockResponseWriter) Write(b []byte) (int, error) {
m.body = append(m.body, b...)
return len(b), nil
}
func (m *mockResponseWriter) WriteHeader(statusCode int) {
m.statusCode = statusCode
m.writeHeaderHit = true
}
func TestServer_HandleError_SkipsWriteHeaderOnHijackedConnection(t *testing.T) {
// Test that handleError does not call WriteHeader for WebSocket errors wrapped
// with errWebSocketPostUpgrade (indicating the connection was hijacked)
s := newTestServer(t, newTestConfig(t))
// Create a WebSocket upgrade request
r, _ := http.NewRequest("GET", "/mytopic/ws", nil)
r.Header.Set("Upgrade", "websocket")
r.Header.Set("Connection", "Upgrade")
v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("1.2.3.4"), nil)
// Test post-upgrade errors wrapped with errWebSocketPostUpgrade (should NOT call WriteHeader)
postUpgradeErr := &errWebSocketPostUpgrade{errors.New("websocket: close 1000 (normal)")}
mock := newMockResponseWriter()
s.handleError(mock, r, v, postUpgradeErr)
require.False(t, mock.writeHeaderHit, "WriteHeader should not be called for post-upgrade errors")
// Test pre-upgrade errors (should call WriteHeader)
preUpgradeErrors := []error{
errHTTPBadRequestWebSocketsUpgradeHeaderMissing,
errHTTPTooManyRequestsLimitSubscriptions,
errHTTPInternalError,
}
for _, err := range preUpgradeErrors {
mock := newMockResponseWriter()
s.handleError(mock, r, v, err)
require.True(t, mock.writeHeaderHit, "WriteHeader should be called for error: %s", err.Error())
}
}

View File

@@ -299,7 +299,7 @@ func (t templateMode) FileName() string {
return "" return ""
} }
// templateFile represents a template file with title, message, and priority // templateFile represents a template file with title and message
// It is used for file-based templates, e.g. grafana, influxdb, etc. // It is used for file-based templates, e.g. grafana, influxdb, etc.
// //
// Example YAML: // Example YAML:
@@ -308,11 +308,9 @@ func (t templateMode) FileName() string {
// message: | // message: |
// This is a {{ .Type }} alert. // This is a {{ .Type }} alert.
// It can be multiline. // It can be multiline.
// priority: '{{ if eq .status "Error" }}5{{ else }}3{{ end }}'
type templateFile struct { type templateFile struct {
Title *string `yaml:"title"` Title *string `yaml:"title"`
Message *string `yaml:"message"` Message *string `yaml:"message"`
Priority *string `yaml:"priority"`
} }
type apiHealthResponse struct { type apiHealthResponse struct {
@@ -485,6 +483,8 @@ type apiConfigResponse struct {
WebPushPublicKey string `json:"web_push_public_key"` WebPushPublicKey string `json:"web_push_public_key"`
DisallowedTopics []string `json:"disallowed_topics"` DisallowedTopics []string `json:"disallowed_topics"`
ConfigHash string `json:"config_hash"` ConfigHash string `json:"config_hash"`
AuthMode string `json:"auth_mode,omitempty"` // "proxy" if auth-user-header is set, empty otherwise
AuthLogoutURL string `json:"auth_logout_url,omitempty"` // URL to redirect to on logout (only for proxy auth)
} }
type apiAccountBillingPrices struct { type apiAccountBillingPrices struct {

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"reflect" "reflect"
"slices"
"strings" "strings"
) )
@@ -96,7 +95,12 @@ func coalesce(v ...any) any {
// Returns: // Returns:
// - bool: True if all values are non-empty, false otherwise // - bool: True if all values are non-empty, false otherwise
func all(v ...any) bool { func all(v ...any) bool {
return !slices.ContainsFunc(v, empty) for _, val := range v {
if empty(val) {
return false
}
}
return true
} }
// anyNonEmpty checks if at least one value in a list is non-empty. // anyNonEmpty checks if at least one value in a list is non-empty.

View File

@@ -12,7 +12,6 @@ import (
"net/netip" "net/netip"
"os" "os"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -50,7 +49,12 @@ func FileExists(filename string) bool {
// Contains returns true if needle is contained in haystack // Contains returns true if needle is contained in haystack
func Contains[T comparable](haystack []T, needle T) bool { func Contains[T comparable](haystack []T, needle T) bool {
return slices.Contains(haystack, needle) for _, s := range haystack {
if s == needle {
return true
}
}
return false
} }
// ContainsIP returns true if any one of the of prefixes contains the ip. // ContainsIP returns true if any one of the of prefixes contains the ip.

417
web/package-lock.json generated
View File

@@ -46,9 +46,9 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.28.5", "@babel/helper-validator-identifier": "^7.28.5",
@@ -60,9 +60,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -70,21 +70,21 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.28.6",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6", "@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0", "@babel/parser": "^7.28.6",
"@babel/template": "^7.28.6", "@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0", "@babel/traverse": "^7.28.6",
"@babel/types": "^7.29.0", "@babel/types": "^7.28.6",
"@jridgewell/remapping": "^2.3.5", "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
@@ -108,13 +108,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.0", "@babel/parser": "^7.28.6",
"@babel/types": "^7.29.0", "@babel/types": "^7.28.6",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@@ -395,12 +395,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.29.0" "@babel/types": "^7.28.6"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -572,15 +572,15 @@
} }
}, },
"node_modules/@babel/plugin-transform-async-generator-functions": { "node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz",
"integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1",
"@babel/traverse": "^7.29.0" "@babel/traverse": "^7.28.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -762,9 +762,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
"integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -977,16 +977,16 @@
} }
}, },
"node_modules/@babel/plugin-transform-modules-systemjs": { "node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.29.0", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
"integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-transforms": "^7.28.6", "@babel/helper-module-transforms": "^7.28.3",
"@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5", "@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.29.0" "@babel/traverse": "^7.28.5"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1013,14 +1013,14 @@
} }
}, },
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
"version": "7.29.0", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
"integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-create-regexp-features-plugin": "^7.27.1",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.27.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1247,9 +1247,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-regenerator": { "node_modules/@babel/plugin-transform-regenerator": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz",
"integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1444,13 +1444,13 @@
} }
}, },
"node_modules/@babel/preset-env": { "node_modules/@babel/preset-env": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz",
"integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.29.0", "@babel/compat-data": "^7.28.6",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.27.1",
@@ -1464,7 +1464,7 @@
"@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-import-attributes": "^7.28.6",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
"@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-async-generator-functions": "^7.29.0", "@babel/plugin-transform-async-generator-functions": "^7.28.6",
"@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-async-to-generator": "^7.28.6",
"@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.28.6", "@babel/plugin-transform-block-scoping": "^7.28.6",
@@ -1475,7 +1475,7 @@
"@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-destructuring": "^7.28.5",
"@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-dotall-regex": "^7.28.6",
"@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6",
"@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1",
"@babel/plugin-transform-explicit-resource-management": "^7.28.6", "@babel/plugin-transform-explicit-resource-management": "^7.28.6",
"@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-exponentiation-operator": "^7.28.6",
@@ -1488,9 +1488,9 @@
"@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-systemjs": "^7.28.5",
"@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
"@babel/plugin-transform-numeric-separator": "^7.28.6", "@babel/plugin-transform-numeric-separator": "^7.28.6",
@@ -1502,7 +1502,7 @@
"@babel/plugin-transform-private-methods": "^7.28.6", "@babel/plugin-transform-private-methods": "^7.28.6",
"@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-private-property-in-object": "^7.28.6",
"@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.29.0", "@babel/plugin-transform-regenerator": "^7.28.6",
"@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-regexp-modifiers": "^7.28.6",
"@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1",
"@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1",
@@ -1515,10 +1515,10 @@
"@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1",
"@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
"@babel/preset-modules": "0.1.6-no-external-plugins", "@babel/preset-modules": "0.1.6-no-external-plugins",
"babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs2": "^0.4.14",
"babel-plugin-polyfill-corejs3": "^0.14.0", "babel-plugin-polyfill-corejs3": "^0.13.0",
"babel-plugin-polyfill-regenerator": "^0.6.6", "babel-plugin-polyfill-regenerator": "^0.6.5",
"core-js-compat": "^3.48.0", "core-js-compat": "^3.43.0",
"semver": "^6.3.1" "semver": "^6.3.1"
}, },
"engines": { "engines": {
@@ -1567,17 +1567,17 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.28.6",
"@babel/helper-globals": "^7.28.0", "@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0", "@babel/parser": "^7.28.6",
"@babel/template": "^7.28.6", "@babel/template": "^7.28.6",
"@babel/types": "^7.29.0", "@babel/types": "^7.28.6",
"debug": "^4.3.1" "debug": "^4.3.1"
}, },
"engines": { "engines": {
@@ -1585,9 +1585,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.0", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
@@ -2309,9 +2309,9 @@
} }
}, },
"node_modules/@isaacs/brace-expansion": { "node_modules/@isaacs/brace-expansion": {
"version": "5.0.1", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2798,9 +2798,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2812,9 +2812,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2826,9 +2826,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2840,9 +2840,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2854,9 +2854,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2868,9 +2868,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2882,9 +2882,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2896,9 +2896,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2910,9 +2910,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2924,9 +2924,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2938,9 +2938,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -2952,9 +2952,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -2966,9 +2966,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2980,9 +2980,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2994,9 +2994,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -3008,9 +3008,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -3022,9 +3022,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -3036,9 +3036,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz",
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3050,9 +3050,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3064,9 +3064,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3078,9 +3078,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3092,9 +3092,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3106,9 +3106,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -3120,9 +3120,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3134,9 +3134,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3248,9 +3248,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.11", "version": "19.2.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.11.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
"integrity": "sha512-tORuanb01iEzWvMGVGv2ZDhYZVeRMrw453DCSAIn/5yvcSVnMoUMTyf33nQJLahYEnv9xqrTNbgz4qY5EfSh0g==", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3658,14 +3658,14 @@
} }
}, },
"node_modules/babel-plugin-polyfill-corejs3": { "node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.14.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
"integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.6", "@babel/helper-define-polyfill-provider": "^0.6.5",
"core-js-compat": "^3.48.0" "core-js-compat": "^3.43.0"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -3702,9 +3702,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.19", "version": "2.9.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -3823,9 +3823,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001767", "version": "1.0.30001766",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
"integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4267,9 +4267,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.286", "version": "1.5.278",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -5328,7 +5328,7 @@
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -7091,9 +7091,9 @@
} }
}, },
"node_modules/path-scurry/node_modules/lru-cache": { "node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.2.5", "version": "11.2.4",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
@@ -7278,24 +7278,24 @@
} }
}, },
"node_modules/react": { "node_modules/react": {
"version": "19.2.4", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.4", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.2.4" "react": "^19.2.3"
} }
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
@@ -7333,9 +7333,9 @@
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.2.4", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-refresh": { "node_modules/react-refresh": {
@@ -7628,9 +7628,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.57.1", "version": "4.56.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7644,31 +7644,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm-eabi": "4.56.0",
"@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-android-arm64": "4.56.0",
"@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.56.0",
"@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-darwin-x64": "4.56.0",
"@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.56.0",
"@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.56.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.56.0",
"@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.56.0",
"@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.56.0",
"@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.56.0",
"@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.56.0",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.56.0",
"@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.56.0",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.56.0",
"@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.56.0",
"@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.56.0",
"@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.56.0",
"@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.56.0",
"@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openbsd-x64": "4.56.0",
"@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.56.0",
"@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.56.0",
"@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.56.0",
"@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.56.0",
"@rollup/rollup-win32-x64-msvc": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.56.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -9331,7 +9331,6 @@
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
@@ -9360,13 +9359,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/workbox-build/node_modules/minimatch": { "node_modules/workbox-build/node_modules/minimatch": {
"version": "10.1.2", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/brace-expansion": "^5.0.1" "@isaacs/brace-expansion": "^5.0.0"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "20 || >=22"

View File

@@ -20,4 +20,6 @@ var config = {
web_push_public_key: "", 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"],
config_hash: "dev", // Placeholder for development; actual value is generated server-side config_hash: "dev", // Placeholder for development; actual value is generated server-side
auth_mode: "", // "proxy" if auth-user-header is set, empty otherwise
auth_logout_url: "", // URL to redirect to on logout (only for proxy auth)
}; };

View File

@@ -75,7 +75,7 @@
"publish_dialog_attachment_limits_quota_reached": "надвишава квотата, остават {{remainingBytes}}", "publish_dialog_attachment_limits_quota_reached": "надвишава квотата, остават {{remainingBytes}}",
"publish_dialog_priority_high": "Висок приоритет", "publish_dialog_priority_high": "Висок приоритет",
"publish_dialog_priority_default": "Подразбиран приоритет", "publish_dialog_priority_default": "Подразбиран приоритет",
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за дисково пространство", "publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
"publish_dialog_tags_label": "Етикети", "publish_dialog_tags_label": "Етикети",
"publish_dialog_email_label": "Адрес на електронна поща", "publish_dialog_email_label": "Адрес на електронна поща",
"publish_dialog_priority_max": "Най-висок приоритет", "publish_dialog_priority_max": "Най-висок приоритет",

View File

@@ -73,7 +73,7 @@
"publish_dialog_tags_placeholder": "Komma-getrennte Liste von Tags, z.B. Warnung, srv1-Backup", "publish_dialog_tags_placeholder": "Komma-getrennte Liste von Tags, z.B. Warnung, srv1-Backup",
"publish_dialog_priority_label": "Priorität", "publish_dialog_priority_label": "Priorität",
"publish_dialog_filename_label": "Dateiname", "publish_dialog_filename_label": "Dateiname",
"publish_dialog_title_placeholder": "Benachrichtigungstitel, z. B. Speicherplatzwarnung", "publish_dialog_title_placeholder": "Benachrichtigungs-Titel, z.B. CPU-Last-Warnung",
"publish_dialog_tags_label": "Tags", "publish_dialog_tags_label": "Tags",
"publish_dialog_click_label": "Klick-URL", "publish_dialog_click_label": "Klick-URL",
"publish_dialog_click_placeholder": "URL die geöffnet werden soll, wenn die Benachrichtigung angeklickt wird", "publish_dialog_click_placeholder": "URL die geöffnet werden soll, wenn die Benachrichtigung angeklickt wird",

View File

@@ -357,8 +357,6 @@
"prefs_users_dialog_title_add": "Add user", "prefs_users_dialog_title_add": "Add user",
"prefs_users_dialog_title_edit": "Edit user", "prefs_users_dialog_title_edit": "Edit user",
"prefs_users_dialog_base_url_label": "Service URL, e.g. https://ntfy.sh", "prefs_users_dialog_base_url_label": "Service URL, e.g. https://ntfy.sh",
"prefs_users_dialog_base_url_invalid": "Invalid URL format. Must start with http:// or https://",
"prefs_users_dialog_base_url_exists": "A user for this service URL already exists",
"prefs_users_dialog_username_label": "Username, e.g. phil", "prefs_users_dialog_username_label": "Username, e.g. phil",
"prefs_users_dialog_password_label": "Password", "prefs_users_dialog_password_label": "Password",
"prefs_appearance_title": "Appearance", "prefs_appearance_title": "Appearance",

View File

@@ -1,52 +0,0 @@
{
"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_show_menu": "הצגת תפריט",
"action_bar_logo_alt": "הלוגו של ntfy",
"action_bar_settings": "הגדרות",
"action_bar_account": "חשבון",
"action_bar_change_display_name": "החלפת שם תצוגה",
"action_bar_reservation_add": "שימור נושא",
"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": "ביטול מינוי",
"notifications_list_item": "התראה",
"notifications_mark_read": "סימון כנקראה",
"notifications_delete": "מחיקה",
"notifications_copied_to_clipboard": "הועתקה ללוח הגזירים",
"notifications_tags": "תגיות",
"notifications_priority_x": "עדיפות {{priority}}",
"notifications_new_indicator": "התראה חדשה",
"notifications_attachment_copy_url_button": "העתקת כתובת",
"notifications_attachment_open_title": "מעבר אל {{url}}",
"notifications_attachment_open_button": "פתיחת צרופה",
"notifications_attachment_link_expires": "תוקף הקישור פג ב־{{date}}",
"notifications_attachment_link_expired": "תוקף קישור ההורדה פג",
"notifications_actions_failed_notification": "פעולה לא מוצלחת",
"notifications_none_for_topic_title": "לא קיבלת התראות בנושא הזה עדיין.",
"notifications_none_for_topic_description": "כדי לשלוח התראות לנושא הזה, צריך לשלוח PUT או POST לכתובת הנושא הזה.",
"notifications_none_for_any_title": "לא קיבלת התראות כלל.",
"notifications_no_subscriptions_title": "נראה שלא נרשמת למינויים עדיין."
}

View File

@@ -30,11 +30,11 @@
"publish_dialog_topic_label": "Название темы", "publish_dialog_topic_label": "Название темы",
"publish_dialog_topic_placeholder": "Название темы, например phil_alerts", "publish_dialog_topic_placeholder": "Название темы, например phil_alerts",
"publish_dialog_title_label": "Заголовок", "publish_dialog_title_label": "Заголовок",
"publish_dialog_title_placeholder": "Заголовок уведомления, например, Предупреждение о занятости диска", "publish_dialog_title_placeholder": "Заголовок уведомления, например Disk space alert",
"publish_dialog_message_label": "Сообщение", "publish_dialog_message_label": "Сообщение",
"publish_dialog_message_placeholder": "Введите сообщение здесь", "publish_dialog_message_placeholder": "Введите сообщение здесь",
"publish_dialog_tags_label": "Тэги", "publish_dialog_tags_label": "Тэги",
"publish_dialog_tags_placeholder": "Ярлыки, разделенные запятыми, например: warning, srv1-backup", "publish_dialog_tags_placeholder": "Список тэгов, разделённый запятой, например: warning, srv1-backup",
"publish_dialog_priority_label": "Приоритет", "publish_dialog_priority_label": "Приоритет",
"publish_dialog_click_label": "Ссылка при открытии", "publish_dialog_click_label": "Ссылка при открытии",
"publish_dialog_click_placeholder": "URL-адрес, который откроется при нажатии на уведомление", "publish_dialog_click_placeholder": "URL-адрес, который откроется при нажатии на уведомление",
@@ -242,8 +242,8 @@
"action_bar_reservation_delete": "Удалить резервирование", "action_bar_reservation_delete": "Удалить резервирование",
"action_bar_profile_title": "Профиль", "action_bar_profile_title": "Профиль",
"action_bar_profile_settings": "Настройки", "action_bar_profile_settings": "Настройки",
"action_bar_profile_logout": "Выйти", "action_bar_profile_logout": "Выход",
"action_bar_sign_in": "Войти", "action_bar_sign_in": "Вход",
"action_bar_sign_up": "Регистрация", "action_bar_sign_up": "Регистрация",
"action_bar_change_display_name": "Изменить псевдоним", "action_bar_change_display_name": "Изменить псевдоним",
"message_bar_publish": "Опубликовать сообщение", "message_bar_publish": "Опубликовать сообщение",
@@ -395,7 +395,7 @@
"prefs_notifications_web_push_title": "Фоновые уведомления", "prefs_notifications_web_push_title": "Фоновые уведомления",
"prefs_notifications_web_push_enabled_description": "Уведомления приходят даже когда веб-приложение не запущено (через Web Push)", "prefs_notifications_web_push_enabled_description": "Уведомления приходят даже когда веб-приложение не запущено (через Web Push)",
"prefs_notifications_web_push_disabled_description": "Уведомления приходят, когда веб-приложение запущено (через WebSocket)", "prefs_notifications_web_push_disabled_description": "Уведомления приходят, когда веб-приложение запущено (через WebSocket)",
"prefs_appearance_theme_title": "Тема оформления", "prefs_appearance_theme_title": "Тема",
"prefs_notifications_web_push_enabled": "Включено для {{server}}", "prefs_notifications_web_push_enabled": "Включено для {{server}}",
"prefs_notifications_web_push_disabled": "Выключено", "prefs_notifications_web_push_disabled": "Выключено",
"notifications_actions_failed_notification": "Неудачное действие", "notifications_actions_failed_notification": "Неудачное действие",
@@ -403,7 +403,5 @@
"subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто", "subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто",
"prefs_appearance_theme_system": "Как в системе (по умолчанию)", "prefs_appearance_theme_system": "Как в системе (по умолчанию)",
"prefs_appearance_theme_dark": "Тёмная", "prefs_appearance_theme_dark": "Тёмная",
"prefs_appearance_theme_light": "Светлая", "prefs_appearance_theme_light": "Светлая"
"account_basics_cannot_edit_or_delete_provisioned_user": "Пользователя, созданного автоматически, нельзя изменить или удалить",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Автоматически созданный токен нельзя изменить или удалить"
} }

View File

@@ -1,7 +1,9 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from "workbox-precaching"; import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
import { NavigationRoute, registerRoute } from "workbox-routing"; import { registerRoute } from "workbox-routing";
import { NetworkFirst } from "workbox-strategies"; import { NetworkFirst, StaleWhileRevalidate } from "workbox-strategies";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { ExpirationPlugin } from "workbox-expiration";
import { clientsClaim } from "workbox-core"; import { clientsClaim } from "workbox-core";
import { dbAsync } from "../src/app/db"; import { dbAsync } from "../src/app/db";
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils"; import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
@@ -237,24 +239,8 @@ const handleClick = async (event) => {
if (event.action) { if (event.action) {
const action = event.notification.data.message.actions.find(({ label }) => event.action === label); const action = event.notification.data.message.actions.find(({ label }) => event.action === label);
// Helper to clear notification and mark as read
const clearNotification = async () => {
event.notification.close();
const { subscriptionId, message: msg } = event.notification.data;
const seqId = msg.sequence_id || msg.id;
if (subscriptionId && seqId) {
const db = await dbAsync();
await db.notifications.where({ subscriptionId, sequenceId: seqId }).modify({ new: 0 });
const badgeCount = await db.notifications.where({ new: 1 }).count();
self.navigator.setAppBadge?.(badgeCount);
}
};
if (action.action === "view") { if (action.action === "view") {
self.clients.openWindow(action.url); self.clients.openWindow(action.url);
if (action.clear) {
await clearNotification();
}
} else if (action.action === "http") { } else if (action.action === "http") {
try { try {
const response = await fetch(action.url, { const response = await fetch(action.url, {
@@ -266,11 +252,6 @@ const handleClick = async (event) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`); throw new Error(`HTTP ${response.status} ${response.statusText}`);
} }
// Only clear on success
if (action.clear) {
await clearNotification();
}
} catch (e) { } catch (e) {
console.error("[ServiceWorker] Error performing http action", e); console.error("[ServiceWorker] Error performing http action", e);
self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, { self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, {
@@ -280,6 +261,10 @@ const handleClick = async (event) => {
}); });
} }
} }
if (action.clear) {
event.notification.close();
}
} else if (message.click) { } else if (message.click) {
self.clients.openWindow(message.click); self.clients.openWindow(message.click);
@@ -354,27 +339,42 @@ clientsClaim();
cleanupOutdatedCaches(); cleanupOutdatedCaches();
if (!import.meta.env.DEV) { if (!import.meta.env.DEV) {
// we need the app_root setting, so we import the config.js file from the go server // Use NetworkFirst for navigation requests. This ensures that auth proxies (like Authelia)
// this does NOT include the same base_url as the web app running in a window, // can intercept unauthenticated requests, while still providing offline fallback.
// since we don't have access to `window` like in `src/app/config.js` // The 3-second timeout means if the network is slow/unavailable, cached HTML is served.
self.importScripts("/config.js");
// this is the fallback single-page-app route, matching vite.config.js PWA config,
// and is served by the go web server. It is needed for the single-page-app to work.
// https://developer.chrome.com/docs/workbox/modules/workbox-routing/#how-to-register-a-navigation-route
registerRoute( registerRoute(
new NavigationRoute(createHandlerBoundToURL("/app.html"), { ({ request }) => request.mode === "navigate",
allowlist: [ new NetworkFirst({
// the app root itself, could be /, or not cacheName: "html-cache",
new RegExp(`^${config.app_root}$`), networkTimeoutSeconds: 3,
plugins: [new CacheableResponsePlugin({ statuses: [200] }), new ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 60 })],
})
);
// Cache static assets (JS, CSS, images, fonts) with StaleWhileRevalidate for better performance.
// Serves cached version immediately while fetching fresh version in the background.
registerRoute(
({ request }) =>
request.destination === "script" ||
request.destination === "style" ||
request.destination === "image" ||
request.destination === "font",
new StaleWhileRevalidate({
cacheName: "assets-cache",
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 }),
], ],
}) })
); );
// the manifest excludes config.js (see vite.config.js) since the dist-file differs from the // Handle config.js with NetworkFirst. The manifest excludes it (see vite.config.js) since
// actual config served by the go server. this adds it back with `NetworkFirst`, so that the // the dist-file differs from the actual config served by the go server.
// most recent config from the go server is cached, but the app still works if the network registerRoute(
// is unavailable. this is important since there's no "refresh" button in the installed pwa ({ url }) => url.pathname === "/config.js",
// to force a reload. new NetworkFirst({
registerRoute(({ url }) => url.pathname === "/config.js", new NetworkFirst()); cacheName: "config-cache",
plugins: [new CacheableResponsePlugin({ statuses: [200] })],
})
);
} }

View File

@@ -16,6 +16,7 @@ import {
withBasicAuth, withBasicAuth,
withBearerAuth, withBearerAuth,
} from "./utils"; } from "./utils";
import config from "./config";
import session from "./Session"; import session from "./Session";
import subscriptionManager from "./SubscriptionManager"; import subscriptionManager from "./SubscriptionManager";
import prefs from "./Prefs"; import prefs from "./Prefs";
@@ -341,7 +342,18 @@ class AccountApi {
async sync() { async sync() {
try { try {
if (!session.token()) { // For proxy auth, detect user from /v1/account if no session exists
if (config.auth_mode === AuthMode.PROXY && !session.exists()) {
console.log(`[AccountApi] Proxy auth mode, detecting user from /v1/account`);
const account = await this.get();
// Never store "*" (anonymous) as username
if (account.username && account.username !== "*") {
console.log(`[AccountApi] Proxy auth: storing session for ${account.username}`);
await session.store(account.username, ""); // Empty token for proxy auth
}
return account;
}
if (!session.exists()) {
return null; return null;
} }
console.log(`[AccountApi] Syncing account`); console.log(`[AccountApi] Syncing account`);
@@ -367,6 +379,11 @@ class AccountApi {
} catch (e) { } catch (e) {
console.log(`[AccountApi] Error fetching account`, e); console.log(`[AccountApi] Error fetching account`, e);
if (e instanceof UnauthorizedError) { if (e instanceof UnauthorizedError) {
// For proxy auth, hard refresh to get fresh auth from proxy
if (config.auth_mode === AuthMode.PROXY) {
window.location.reload();
return undefined;
}
await session.resetAndRedirect(routes.login); await session.resetAndRedirect(routes.login);
} }
return undefined; return undefined;
@@ -431,5 +448,10 @@ export const Permission = {
DENY_ALL: "deny-all", DENY_ALL: "deny-all",
}; };
// Maps to apiConfigResponse.AuthMode in server/types.go
export const AuthMode = {
PROXY: "proxy",
};
const accountApi = new AccountApi(); const accountApi = new AccountApi();
export default accountApi; export default accountApi;

View File

@@ -3,6 +3,10 @@ import Dexie from "dexie";
/** /**
* Manages the logged-in user's session and access token. * Manages the logged-in user's session and access token.
* The session replica is stored in IndexedDB so that the service worker can access it. * The session replica is stored in IndexedDB so that the service worker can access it.
*
* For proxy authentication (when config.auth_mode === "proxy"), the token will be empty
* since authentication is handled by the proxy. In this case, store(username, "") is called
* with an empty token, and exists() returns true based on the username alone.
*/ */
class Session { class Session {
constructor() { constructor() {
@@ -53,7 +57,7 @@ class Session {
} }
exists() { exists() {
return this.username() && this.token(); return !!this.username();
} }
username() { username() {

View File

@@ -60,7 +60,6 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr
const image = isImage(message.attachment) ? message.attachment.url : undefined; const image = isImage(message.attachment) ? message.attachment.url : undefined;
const sequenceId = message.sequence_id || message.id; const sequenceId = message.sequence_id || message.id;
const tag = notificationTag(baseUrl, topic, sequenceId); const tag = notificationTag(baseUrl, topic, sequenceId);
const subscriptionId = `${baseUrl}/${topic}`;
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
return [ return [
@@ -76,7 +75,6 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr
silent: false, silent: false,
// This is used by the notification onclick event // This is used by the notification onclick event
data: { data: {
subscriptionId,
message, message,
topicRoute, topicRoute,
}, },

View File

@@ -8,7 +8,6 @@ import pop from "../sounds/pop.mp3";
import popSwoosh from "../sounds/pop-swoosh.mp3"; import popSwoosh from "../sounds/pop-swoosh.mp3";
import config from "./config"; import config from "./config";
import emojisMapped from "./emojisMapped"; import emojisMapped from "./emojisMapped";
import { THEME } from "./Prefs";
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`; export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
@@ -275,84 +274,6 @@ export const urlB64ToUint8Array = (base64String) => {
return outputArray; return outputArray;
}; };
export const darkModeEnabled = (prefersDarkMode, themePreference) => {
switch (themePreference) {
case THEME.DARK:
return true;
case THEME.LIGHT:
return false;
case THEME.SYSTEM:
default:
return prefersDarkMode;
}
};
// Canvas-based favicon with a red notification dot when there are unread messages
let faviconCanvas;
let faviconOriginalIcon;
const loadFaviconIcon = () =>
new Promise((resolve) => {
if (faviconOriginalIcon) {
resolve(faviconOriginalIcon);
return;
}
const img = new Image();
img.onload = () => {
faviconOriginalIcon = img;
resolve(img);
};
img.onerror = () => resolve(null);
// Use PNG instead of ICO — .ico files can't be reliably drawn to canvas in all browsers
img.src = "/static/images/ntfy.png";
});
export const updateFavicon = async (count) => {
const size = 32;
const img = await loadFaviconIcon();
if (!img) {
return;
}
if (!faviconCanvas) {
faviconCanvas = document.createElement("canvas");
faviconCanvas.width = size;
faviconCanvas.height = size;
}
const ctx = faviconCanvas.getContext("2d");
ctx.clearRect(0, 0, size, size);
ctx.drawImage(img, 0, 0, size, size);
if (count > 0) {
const dotRadius = 5;
const borderWidth = 2;
const dotX = size - dotRadius - borderWidth + 1;
const dotY = size - dotRadius - borderWidth + 1;
// Transparent border: erase a ring around the dot so the icon doesn't bleed into it
ctx.save();
ctx.globalCompositeOperation = "destination-out";
ctx.beginPath();
ctx.arc(dotX, dotY, dotRadius + borderWidth, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
// Red dot
ctx.beginPath();
ctx.arc(dotX, dotY, dotRadius, 0, 2 * Math.PI);
ctx.fillStyle = "#dc3545";
ctx.fill();
}
const link = document.querySelector("link[rel='icon']");
if (link) {
link.href = faviconCanvas.toDataURL("image/png");
}
};
export const copyToClipboard = (text) => { export const copyToClipboard = (text) => {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text); return navigator.clipboard.writeText(text);

View File

@@ -16,7 +16,7 @@ import routes from "./routes";
import db from "../app/db"; import db from "../app/db";
import { topicDisplayName } from "../app/utils"; import { topicDisplayName } from "../app/utils";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import accountApi from "../app/AccountApi"; import accountApi, { AuthMode } from "../app/AccountApi";
import PopupMenu from "./PopupMenu"; import PopupMenu from "./PopupMenu";
import { SubscriptionPopup } from "./SubscriptionPopup"; import { SubscriptionPopup } from "./SubscriptionPopup";
import { useIsLaunchedPWA } from "./hooks"; import { useIsLaunchedPWA } from "./hooks";
@@ -139,6 +139,17 @@ const ProfileIcon = () => {
}; };
const handleLogout = async () => { const handleLogout = async () => {
// For proxy auth, redirect to the logout URL if configured
if (config.auth_mode === AuthMode.PROXY) {
if (config.auth_logout_url) {
await db().delete();
localStorage.removeItem("user");
localStorage.removeItem("token");
window.location.href = config.auth_logout_url;
}
return;
}
// Standard logout
try { try {
await accountApi.logout(); await accountApi.logout();
await db().delete(); await db().delete();
@@ -147,6 +158,9 @@ const ProfileIcon = () => {
} }
}; };
// Determine if logout button should be shown (hide if proxy auth without logout URL)
const showLogout = config.auth_mode !== AuthMode.PROXY || config.auth_logout_url;
return ( return (
<> <>
{session.exists() && ( {session.exists() && (
@@ -178,12 +192,14 @@ const ProfileIcon = () => {
</ListItemIcon> </ListItemIcon>
{t("action_bar_profile_settings")} {t("action_bar_profile_settings")}
</MenuItem> </MenuItem>
<MenuItem onClick={handleLogout}> {showLogout && (
<ListItemIcon> <MenuItem onClick={handleLogout}>
<Logout fontSize="small" /> <ListItemIcon>
</ListItemIcon> <Logout fontSize="small" />
{t("action_bar_profile_logout")} </ListItemIcon>
</MenuItem> {t("action_bar_profile_logout")}
</MenuItem>
)}
</PopupMenu> </PopupMenu>
</> </>
); );

View File

@@ -11,7 +11,7 @@ import ActionBar from "./ActionBar";
import Preferences from "./Preferences"; import Preferences from "./Preferences";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import { expandUrl, getKebabCaseLangStr, darkModeEnabled, updateFavicon } from "../app/utils"; import { expandUrl, getKebabCaseLangStr } from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks"; import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks";
@@ -21,7 +21,7 @@ import Login from "./Login";
import Signup from "./Signup"; import Signup from "./Signup";
import Account from "./Account"; import Account from "./Account";
import initI18n from "../app/i18n"; // Translations! import initI18n from "../app/i18n"; // Translations!
import prefs from "../app/Prefs"; import prefs, { THEME } from "../app/Prefs";
import RTLCacheProvider from "./RTLCacheProvider"; import RTLCacheProvider from "./RTLCacheProvider";
import session from "../app/Session"; import session from "../app/Session";
@@ -29,6 +29,20 @@ initI18n();
export const AccountContext = createContext(null); export const AccountContext = createContext(null);
const darkModeEnabled = (prefersDarkMode, themePreference) => {
switch (themePreference) {
case THEME.DARK:
return true;
case THEME.LIGHT:
return false;
case THEME.SYSTEM:
default:
return prefersDarkMode;
}
};
const App = () => { const App = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const languageDir = i18n.dir(); const languageDir = i18n.dir();
@@ -83,7 +97,6 @@ const App = () => {
const updateTitle = (newNotificationsCount) => { const updateTitle = (newNotificationsCount) => {
document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy"; document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy";
window.navigator.setAppBadge?.(newNotificationsCount); window.navigator.setAppBadge?.(newNotificationsCount);
updateFavicon(newNotificationsCount);
}; };
const Layout = () => { const Layout = () => {

View File

@@ -1,11 +1,11 @@
import * as React from "react"; import * as React from "react";
import { useState } from "react"; import { useState, useEffect } from "react";
import { Typography, TextField, Button, Box, IconButton, InputAdornment } from "@mui/material"; import { Typography, TextField, Button, Box, IconButton, InputAdornment } from "@mui/material";
import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Visibility, VisibilityOff } from "@mui/icons-material"; import { Visibility, VisibilityOff } from "@mui/icons-material";
import accountApi from "../app/AccountApi"; import accountApi, { AuthMode } from "../app/AccountApi";
import AvatarBox from "./AvatarBox"; import AvatarBox from "./AvatarBox";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
@@ -18,6 +18,13 @@ const Login = () => {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
// Redirect to app if using proxy authentication
useEffect(() => {
if (config.auth_mode === AuthMode.PROXY) {
window.location.href = routes.app;
}
}, []);
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
const user = { username, password }; const user = { username, password };

View File

@@ -33,13 +33,12 @@ import {
maybeActionErrors, maybeActionErrors,
openUrl, openUrl,
shortUrl, shortUrl,
topicUrl, topicShortUrl,
unmatchedTags, unmatchedTags,
} from "../app/utils"; } from "../app/utils";
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils"; import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import notifier from "../app/Notifier";
import priority1 from "../img/priority-1.svg"; import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg"; import priority2 from "../img/priority-2.svg";
import priority4 from "../img/priority-4.svg"; import priority4 from "../img/priority-4.svg";
@@ -189,7 +188,7 @@ const MarkdownContainer = styled("div")`
} }
p { p {
line-height: 1.5; line-height: 1.2;
} }
blockquote, blockquote,
@@ -304,7 +303,7 @@ const NotificationItem = (props) => {
{formatTitle(notification)} {formatTitle(notification)}
</Typography> </Typography>
)} )}
<Typography variant="body1" sx={{ whiteSpace: "pre-line", overflowX: "auto" }}> <Typography variant="body1" sx={{ whiteSpace: "pre-line" }}>
<NotificationBody notification={notification} /> <NotificationBody notification={notification} />
{maybeActionErrors(notification)} {maybeActionErrors(notification)}
</Typography> </Typography>
@@ -509,15 +508,6 @@ const updateActionStatus = (notification, action, progress, error) => {
}); });
}; };
const clearNotification = async (notification) => {
console.log(`[Notifications] Clearing notification ${notification.id}`);
const subscription = await subscriptionManager.get(notification.subscriptionId);
if (subscription) {
await notifier.cancel(subscription, notification);
}
await subscriptionManager.markNotificationRead(notification.id);
};
const performHttpAction = async (notification, action) => { const performHttpAction = async (notification, action) => {
console.log(`[Notifications] Performing HTTP user action`, action); console.log(`[Notifications] Performing HTTP user action`, action);
try { try {
@@ -533,9 +523,6 @@ const performHttpAction = async (notification, action) => {
const success = response.status >= 200 && response.status <= 299; const success = response.status >= 200 && response.status <= 299;
if (success) { if (success) {
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
if (action.clear) {
await clearNotification(notification);
}
} else { } else {
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`); updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
} }
@@ -561,16 +548,10 @@ const UserAction = (props) => {
); );
} }
if (action.action === "view") { if (action.action === "view") {
const handleClick = () => {
openUrl(action.url);
if (action.clear) {
clearNotification(notification);
}
};
return ( return (
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}> <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
<Button <Button
onClick={handleClick} onClick={() => openUrl(action.url)}
aria-label={t("notifications_actions_open_url_title", { aria-label={t("notifications_actions_open_url_title", {
url: action.url, url: action.url,
})} })}
@@ -607,7 +588,7 @@ const UserAction = (props) => {
const NoNotifications = (props) => { const NoNotifications = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const topicUrlResolved = topicUrl(props.subscription.baseUrl, props.subscription.topic); const topicShortUrlResolved = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
return ( return (
<VerticallyCenteredContainer maxWidth="xs"> <VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
@@ -620,7 +601,7 @@ const NoNotifications = (props) => {
{t("notifications_example")}:<br /> {t("notifications_example")}:<br />
<tt> <tt>
{'$ curl -d "Hi" '} {'$ curl -d "Hi" '}
{topicUrlResolved} {topicShortUrlResolved}
</tt> </tt>
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
@@ -633,7 +614,7 @@ const NoNotifications = (props) => {
const NoNotificationsWithoutSubscription = (props) => { const NoNotificationsWithoutSubscription = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const subscription = props.subscriptions[0]; const subscription = props.subscriptions[0];
const topicUrlResolved = topicUrl(subscription.baseUrl, subscription.topic); const topicShortUrlResolved = topicShortUrl(subscription.baseUrl, subscription.topic);
return ( return (
<VerticallyCenteredContainer maxWidth="xs"> <VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}> <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
@@ -646,7 +627,7 @@ const NoNotificationsWithoutSubscription = (props) => {
{t("notifications_example")}:<br /> {t("notifications_example")}:<br />
<tt> <tt>
{'$ curl -d "Hi" '} {'$ curl -d "Hi" '}
{topicUrlResolved} {topicShortUrlResolved}
</tt> </tt>
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>

View File

@@ -429,23 +429,13 @@ const UserDialog = (props) => {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
const editMode = props.user !== null; const editMode = props.user !== null;
const baseUrlValid = baseUrl.length === 0 || validUrl(baseUrl);
const baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl);
const baseUrlError = baseUrl.length > 0 && (!baseUrlValid || baseUrlExists);
const addButtonEnabled = (() => { const addButtonEnabled = (() => {
if (editMode) { if (editMode) {
return username.length > 0 && password.length > 0; return username.length > 0 && password.length > 0;
} }
return validUrl(baseUrl) && !baseUrlExists && username.length > 0 && password.length > 0; const baseUrlValid = validUrl(baseUrl);
})(); const baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl);
const baseUrlHelperText = (() => { return baseUrlValid && !baseUrlExists && username.length > 0 && password.length > 0;
if (baseUrl.length > 0 && !baseUrlValid) {
return t("prefs_users_dialog_base_url_invalid");
}
if (baseUrlExists) {
return t("prefs_users_dialog_base_url_exists");
}
return "";
})(); })();
const handleSubmit = async () => { const handleSubmit = async () => {
props.onSubmit({ props.onSubmit({
@@ -477,8 +467,6 @@ const UserDialog = (props) => {
type="url" type="url"
fullWidth fullWidth
variant="standard" variant="standard"
error={baseUrlError}
helperText={baseUrlHelperText}
/> />
)} )}
<TextField <TextField