mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Merge remote-tracking branch 'origin/main' into feat/onboarding-wizard
This commit is contained in:
commit
8ed46eef58
49 files changed
+3199
-307
No files matched your search
+39
-14
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.38.16
|
||||
version: 1.38.17
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
@@ -12021,7 +12021,9 @@ paths:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422":
|
||||
description: Body matched neither accepted shape, or `enabled` is true while the stored config has no server URL and
|
||||
topic.
|
||||
topic; or the server URL is on a private network the operator has not listed in
|
||||
`NOTIFICATION_PRIVATE_ORIGINS` (`meta.errorCode` = `private_origin_not_approved`), or it is a link-local,
|
||||
metadata or unspecified address that no grant can open (`private_origin_not_grantable`). Nothing was stored.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -12146,7 +12148,10 @@ paths:
|
||||
"413": *a19
|
||||
"415": *a20
|
||||
"422":
|
||||
description: Body matched neither accepted shape, or `enabled` is true while no webhook URL is stored.
|
||||
description: Body matched neither accepted shape, or `enabled` is true while no webhook URL is stored; or the URL is on
|
||||
a private network the operator has not listed in `NOTIFICATION_PRIVATE_ORIGINS` (`meta.errorCode` =
|
||||
`private_origin_not_approved`), or it is a link-local, metadata or unspecified address that no grant can
|
||||
open (`private_origin_not_grantable`). Nothing was stored.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -12370,7 +12375,9 @@ paths:
|
||||
summary: Send a test message to the saved ntfy topic
|
||||
description: Publishes one message to the account's configured ntfy server and topic. Takes no body. Rate-limited 5 per
|
||||
5 minutes per user. Refuses with 400 when no ntfy channel is saved or when the saved config is missing a server
|
||||
URL or topic; a publish failure is a 500.
|
||||
URL or topic; a publish failure is a 500. A server on a private network is refused with 422 and `meta.errorCode`
|
||||
= `private_origin_not_approved` unless the operator listed its exact origin in `NOTIFICATION_PRIVATE_ORIGINS`;
|
||||
the test and the scheduled delivery take the same decision.
|
||||
responses:
|
||||
"200":
|
||||
description: The provider accepted the message. Not proof it was delivered.
|
||||
@@ -12379,7 +12386,14 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NtfyTestEnvelope"
|
||||
"401": *a1
|
||||
"422": *a4
|
||||
"422":
|
||||
description: The saved server is on a private network the operator has not approved (`meta.errorCode` =
|
||||
`private_origin_not_approved`), or is a link-local, metadata or unspecified address that no grant can open
|
||||
(`private_origin_not_grantable`). The resolved address is never echoed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429": *a5
|
||||
/api/settings/telegram/test:
|
||||
post:
|
||||
@@ -12412,7 +12426,9 @@ paths:
|
||||
description: Posts one payload to the account's configured webhook URL. Takes no body. Rate-limited 5 per 5 minutes per
|
||||
user. Refuses with 400 when no webhook channel is saved or the saved config has no URL; a delivery failure is a
|
||||
500. The URL was checked against the SSRF floor when it was saved, so a test cannot be used to reach an internal
|
||||
host.
|
||||
host. A URL on a private network is refused with 422 and `meta.errorCode` = `private_origin_not_approved` unless
|
||||
the operator listed its exact origin in `NOTIFICATION_PRIVATE_ORIGINS`; the test and the scheduled delivery take
|
||||
the same decision.
|
||||
responses:
|
||||
"200":
|
||||
description: The provider accepted the message. Not proof it was delivered.
|
||||
@@ -12421,7 +12437,14 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/WebhookTestEnvelope"
|
||||
"401": *a1
|
||||
"422": *a4
|
||||
"422":
|
||||
description: The saved URL is on a private network the operator has not approved (`meta.errorCode` =
|
||||
`private_origin_not_approved`), or is a link-local, metadata or unspecified address that no grant can open
|
||||
(`private_origin_not_grantable`). The resolved address is never echoed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429": *a5
|
||||
/api/settings/data:
|
||||
delete:
|
||||
@@ -25769,9 +25792,10 @@ components:
|
||||
required:
|
||||
- enabled
|
||||
description: "ntfy channel config. `serverUrl` must pass the SSRF floor — a URL resolving into a private range is
|
||||
refused at input time. An EMPTY OR OMITTED `authToken` preserves the stored one rather than clearing it: the GET
|
||||
never returns the secret, so a client round-tripping the form would otherwise wipe it on every unrelated save.
|
||||
Sending a non-empty value replaces it. There is no way to clear a stored auth token through this endpoint."
|
||||
refused at input time unless the operator listed its exact origin in `NOTIFICATION_PRIVATE_ORIGINS`. An EMPTY OR
|
||||
OMITTED `authToken` preserves the stored one rather than clearing it: the GET never returns the secret, so a
|
||||
client round-tripping the form would otherwise wipe it on every unrelated save. Sending a non-empty value
|
||||
replaces it. There is no way to clear a stored auth token through this endpoint."
|
||||
TelegramSettingsPutRequest:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/NotificationChannelEnabledRequest"
|
||||
@@ -25829,8 +25853,9 @@ components:
|
||||
- enabled
|
||||
description: "Generic-webhook channel config — one channel covering Gotify, Discord, Slack, a Matrix bridge, Home
|
||||
Assistant, or any relay accepting an inbound JSON POST. `url` must pass the SSRF floor at input time and is
|
||||
re-checked at dispatch time. An EMPTY OR OMITTED `headerValue` preserves the stored one, for the same reason as
|
||||
ntfy's `authToken`: the GET never returns it. There is no way to clear a stored header value through this
|
||||
re-checked at dispatch time; a private origin passes both only when the operator listed it in
|
||||
`NOTIFICATION_PRIVATE_ORIGINS`. An EMPTY OR OMITTED `headerValue` preserves the stored one, for the same reason
|
||||
as ntfy's `authToken`: the GET never returns it. There is no way to clear a stored header value through this
|
||||
endpoint."
|
||||
DeleteAccountRequest:
|
||||
type: object
|
||||
@@ -29998,8 +30023,8 @@ components:
|
||||
dashboard_layout_conflict, empty_file, health_score_config_conflict, insights_layout_conflict,
|
||||
invalid_base_updated_at, json_carries_no_intake_time, json_not_an_array, medication_layout_conflict,
|
||||
missing_required_columns, modules_conflict, mood_tag_layout_conflict, not_configured,
|
||||
notification_prefs_conflict, oidc_only, private_origin_not_approved, rate_limited_self, too_many_rows,
|
||||
unreadable_json, upstream_invalid_json, vapid_not_configured"
|
||||
notification_prefs_conflict, oidc_only, private_origin_not_approved, private_origin_not_grantable,
|
||||
rate_limited_self, too_many_rows, unreadable_json, upstream_invalid_json, vapid_not_configured"
|
||||
type: string
|
||||
additionalProperties: {}
|
||||
required:
|
||||
|
||||
@@ -28,9 +28,16 @@ Each entry is an exact normalized `scheme://host:port` trust unit:
|
||||
rejected;
|
||||
- approving one hostname does not approve subdomains, sibling hosts, or a
|
||||
different port; and
|
||||
- the unspecified address, link-local and the cloud-metadata range cannot
|
||||
be listed, and a listed name that resolves there is refused at connect
|
||||
time; loopback (`http://localhost:1337`, `http://127.0.0.1:1337`) stays a
|
||||
valid grant for a host-networking deployment, exactly as before; and
|
||||
- any malformed non-empty entry fails evaluation instead of silently
|
||||
weakening the configured policy.
|
||||
|
||||
The grammar is shared with `NOTIFICATION_PRIVATE_ORIGINS`, the same grant
|
||||
for webhook and ntfy targets (`docs/self-hosting/notifications.md`).
|
||||
|
||||
Do not put a Nightscout API token, capability URL, health data, or a URL
|
||||
containing credentials in the allowlist. Restart the application after
|
||||
changing the environment.
|
||||
|
||||
@@ -180,10 +180,12 @@ HealthLog publishes to that topic.
|
||||
server if you self-host ntfy) and **Topic**, then save.
|
||||
|
||||
If your ntfy server requires auth, an access token can be attached; the
|
||||
server URL is validated as a public host and dialled through the egress
|
||||
guard (`src/lib/notifications/senders/ntfy.ts`). Urgent events are sent at
|
||||
ntfy's top priority (`5` / max) so they bypass batching and surface on the
|
||||
lock screen.
|
||||
server URL is validated at save time and dialled through the egress guard
|
||||
(`src/lib/notifications/senders/ntfy.ts`). A self-hosted ntfy on your own
|
||||
network needs the operator grant described under [Notification targets on
|
||||
a private network](#notification-targets-on-a-private-network). Urgent
|
||||
events are sent at ntfy's top priority (`5` / max) so they bypass batching
|
||||
and surface on the lock screen.
|
||||
|
||||
## Webhook
|
||||
|
||||
@@ -197,11 +199,78 @@ optionally, one custom header (e.g. an auth token).
|
||||
**header value**.
|
||||
2. Save. A Test button fires a one-off POST so you can confirm routing.
|
||||
|
||||
The URL is validated as a public host and dialled through
|
||||
`safeFetch({ requirePublicHost: true })` — the SSRF guard blocks private
|
||||
and loopback ranges because the target is user-supplied
|
||||
(`src/lib/notifications/senders/webhook.ts`). The webhook carries the
|
||||
event's urgency flag so you can route urgent events differently.
|
||||
The URL is validated at save time and dialled through `safeFetch` with
|
||||
the connect-time resolver pin — the SSRF guard refuses a target that
|
||||
resolves to a private or loopback address because the target is
|
||||
user-supplied (`src/lib/notifications/senders/webhook.ts`). A Gotify or
|
||||
Home Assistant on your own network needs the operator grant described in
|
||||
the next section. The webhook carries the event's urgency flag so you can
|
||||
route urgent events differently.
|
||||
|
||||
## Notification targets on a private network
|
||||
|
||||
Every webhook and ntfy target is resolved at send time and refused when
|
||||
the answer is a private, loopback, link-local or CGNAT address. That is
|
||||
the DNS-rebinding defence, and it applies to a public-looking name with a
|
||||
LAN A record (a Gotify behind Nginx Proxy Manager with a DNS-01
|
||||
certificate) exactly as it applies to `http://192.168.1.20`. The test
|
||||
button and the scheduled delivery take the same decision, so a test that
|
||||
fails with "Private network target" means nothing would have arrived
|
||||
either.
|
||||
|
||||
The operator opens a target by listing its exact origin in the server
|
||||
environment:
|
||||
|
||||
```env
|
||||
NOTIFICATION_PRIVATE_ORIGINS="https://gotify.example.com,http://ntfy.lan:8080"
|
||||
```
|
||||
|
||||
Each entry is one `scheme://host[:port]`, the same grammar as
|
||||
`NIGHTSCOUT_PRIVATE_ORIGINS`:
|
||||
|
||||
- only `http` and `https`; scheme, host and effective port must all match
|
||||
what the user saves (`https://gotify.example.com:8443` and
|
||||
`https://gotify.example.com` are two different grants);
|
||||
- no path, query, credentials, wildcard (`*.lan`) or CIDR (`10.0.0.0/8`) —
|
||||
a range grant would let a DNS rebinding to any address in the range pass
|
||||
the pin, so an operator with several relays lists several origins;
|
||||
- listing a host does not list its sub-hosts or sibling ports;
|
||||
- the unspecified address (`0.0.0.0`, `::`), link-local and the
|
||||
cloud-metadata range (`169.254.0.0/16`, `fe80::/10`) cannot be granted: a
|
||||
literal entry is logged once and ignored, and a listed name that resolves
|
||||
there is refused at dial time;
|
||||
- RFC1918, IPv6 unique-local, CGNAT (a Tailscale address or MagicDNS name)
|
||||
and, on a host-network deployment, loopback (`http://127.0.0.1:8080`,
|
||||
`http://localhost:8080`, `::1`) are what the list is for — an exact
|
||||
loopback origin with its port is as deliberate a decision as a LAN one;
|
||||
- a malformed entry is logged once at first use and grants nothing; the
|
||||
valid entries beside it keep working.
|
||||
|
||||
A listed origin is still dialled through the pinned resolver with
|
||||
redirects forbidden, so a relay that answers with a redirect to another
|
||||
host is refused. The variable has to be on the compose `environment:`
|
||||
whitelist to reach the container; the published `docker-compose.yml`
|
||||
lists it. Restart the app after changing it.
|
||||
|
||||
What is refused where, so you know which lever to pull:
|
||||
|
||||
| Target | Without the grant | With the exact origin listed |
|
||||
| ----------------------------------------------------------- | ----------------- | ---------------------------------------------- |
|
||||
| Public name resolving to a public address | works | works, public pin |
|
||||
| Public name with a private A record (LAN reverse proxy) | refused at send | works |
|
||||
| `gotify.lan`, `gotify.home.arpa`, a Docker service name | refused at send | works |
|
||||
| `gotify.local` (mDNS) | refused at save | allowed, if the container resolves mDNS |
|
||||
| Literal `192.168.x.x`, `10.x.x.x`, `100.64.x.x`, `fd00::/8` | refused at save | works |
|
||||
| `localhost`, `127.0.0.1`, `::1` (host networking) | refused at save | works |
|
||||
| `0.0.0.0`, `169.254.169.254`, `fe80::…` | refused at save | still refused (`private_origin_not_grantable`) |
|
||||
|
||||
A private target that is not listed is refused when the card is saved
|
||||
(`422`, `meta.errorCode` = `private_origin_not_approved`, the message
|
||||
names the variable; a link-local, metadata or unspecified target answers
|
||||
`private_origin_not_grantable` instead, because no grant can open it) or, for a DNS name the save cannot classify, when the
|
||||
test button or a reminder dials it. The resolved address is never shown
|
||||
to the user. Telegram, email and Web Push are not affected: their hosts
|
||||
are not user-supplied.
|
||||
|
||||
## Email (SMTP)
|
||||
|
||||
|
||||
Reference in new issue
Block a user