Skip to content

feat: approval through ntfy - #147

Merged
groupsky merged 20 commits into
mainfrom
feat/24-ntfy
Oct 31, 2026
Merged

groupsky merged 20 commits into
mainfrom
feat/24-ntfy

Conversation

@groupsky

@groupsky groupsky commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Refs #24

An ntfy approver, as a second approval channel next to Telegram.

Decisions on the open questions

  1. One channel per broker. The config has [telegram] or [ntfy], not both. Leaner: one approver, one Run loop, no merge of two answer streams. Both at once can come later.
  2. Anyone may write to the answer topic. The 16-byte random value of each request is the only proof. Only the broker reads the answer topic. Refused answers get a budget, as refused taps do on Telegram (T6). Leaner: no write token in the buttons, and a write token would be seen by the same readers anyway.
  3. A long message is shortened, as for Telegram, to stay under 4,096 bytes, so ntfy never turns it into an attachment.
  4. The guide does not offer ntfy.sh. It recommends only a self-hosted server with auth-default-access: deny-all, with upstream-base-url for iOS. The Firebase question then does not need an answer for v1.

Progress

  • internal/ntfy: the approver (Ask, End, Notify, Run) with a fake ntfy server
  • Tests: wrong value, value of another request, late answer, second answer, replayed answer, flood of bad answers, lost subscription
  • [ntfy] config section; the core takes answers without a user ID with [ntfy] (audit gets a channel field)
  • Broker start and stop order with ntfy
  • DESIGN.md sections 3, 4, 7 (ntfy details), 9 (audit channel), 14, 15, 17 (7516e0b)
  • THREAT-MODEL.md section 3 and a new threat (T40) for the server and the topic readers (25ccb59)
  • docs/setup.md (950735b) and docs/manual-ntfy-test.md (c8b7400)
  • Manual test on Android and iPhone (needs Geno)
  • Security review of the whole PR: nothing medium or higher; three low findings fixed in 26090ec, fix round reviewed

Tests pass on Go 1.27.1 and 1.26.0 with CI=true (vet, -race); new tests stressed with -count=20 -cpu 1,2,4.

Next: merge on green CI. The manual phone test stays open on #24 (needs Geno).

Open review findings (internal/ntfy, all low)

  1. One value opens every button of a request, and it stays after an answer that did not count. Fix: one value per button (per action). Fixed in d690b2c.
  2. The "line too long" warning in run.go has no budget. Fix: send it through refuse(). Fixed in 4a08396.
  3. The 4,096-byte cap counts raw bytes, but ntfy caps the JSON body at 8,192; escaped claims can go over. Fix: measure the encoded body (SetEscapeHTML(false)). Fixed in ad02280.
  4. The reconnect wait resets on the open event, so a server that closes at once gets ~1 connect per second. Fix: reset only after a message or keepalive. Fixed in 4a08396.
  5. An early tap (before Ask has the message ID) is dropped, and clear: true hides the buttons. Fails closed; usability only.
  6. When Ask fails after the server took the message, the phone keeps dead buttons. Not a safety problem.
  7. The broker's update notice went out on ntfy as a high-priority alert. Fixed in 45e80b4: notices use a low priority (Inform); alerts and the security-release notice stay high.
  8. CI: a data race in the fake ntfy server of the broker test (the token). Fixed in 24e0acc.
  9. The ntfy channel has no alarm, warning or summary for standing grants. Fixed in fc6fd16: a config with [ntfy] and [standing] does not load (fail closed). Standing grants on ntfy can come later. The example unit now loads the ntfy-token credential.
  10. ntfy result lines said "by you", but ntfy cannot tell who tapped. Fixed: they say only the time. The unit's exit code 77 comment names ntfy.
  11. Last review, low, not fixed: the count of skipped lost-subscription warnings is not logged. Lean choice: the warning itself already says the stream is lost; the count adds nothing a person can act on.

🤖 Generated with Claude Code

groupsky and others added 20 commits October 31, 2026 14:26
Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audit decision entry gets an optional channel field, ntfy:TOPIC.

Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reader of the answer topic could turn a seen Deny into an Approve,
because all buttons of a request had the same value. Each button now
has its own random value, and an answer must carry the value of its
own action.

Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warning for a line that is too long (and other bad lines) now uses
the same budget as refused answers. The reconnect wait resets only
after a message or keepalive event, not after the open event that ntfy
sends on every connection.

Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fitBody cut only the text at 4,096 bytes, but ntfy refuses a JSON
publish body over 8,192 bytes, and json.Marshal made <, > and & six
bytes each. The body is now encoded without HTML escaping, and the text
is shortened until the whole body, with the buttons, fits.

Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The channel (Telegram or ntfy) is picked once at start and takes the
place of Telegram in the start and stop order. A refused ntfy token
ends with exit code 77, as a refused Telegram token does.

Refs #24

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ntfy cannot tell who tapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y or fragment in the URL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@groupsky
groupsky marked this pull request as ready for review October 31, 2026 16:12
@groupsky
groupsky merged commit de53e2d into main Oct 31, 2026
8 checks passed
@groupsky
groupsky deleted the feat/24-ntfy branch October 31, 2026 16:12
@groupsky groupsky mentioned this pull request Oct 31, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant