Name Stash on what it publishes (NIP-89 client tag), with an opt-out #10

Merged
laoc merged 4 commits from feat/client-tag into main 2026-08-06 11:32:00 +00:00
Owner

Closes the "Add client tag" issue. NIP-89's client tag, plus the opt-out the spec attaches to it.

What lands

["client", "Stash"] on every event the app publishes, applied in NostrRepository.sign — every one of the app's publish sites funnels through there, so it is the only place the tag has to go, and the only place the opt-out has to be honoured.

Name-only, not the full tuple. The spec's form is ["client", <name>, "31990:<pubkey>:<d>", "<relay hint>"], and no kind:31990 for Stash exists yet — re-checked today against damus, nos.lol, nostr.band, primal and haven with a kind-0 positive control on each. An address pointing at nothing is worse than no address: a reader who follows it learns only that the app lied. docs/NIP89_HANDLER.md carries the ready-to-sign handler event and the nak line; adding the last two entries afterwards is additive and changes nothing for readers who only look at the name.

The opt-out is a requirement, not a courtesy — "this has privacy implications for users, so clients SHOULD allow users to opt-out of using this tag." Settings → Publishing → Name Stash on what you publish, on by default. Opting out means the tag is absent, not blank: a ["client",""] would still say an app that sets client tags published this.

One design call

The signer reads the preference on every sign rather than holding a copy of it. A cached flag has to be re-set from preferences at startup and again on every toggle, and either wiring failing is invisible from the outside — the app would go on naming itself on the events of someone who turned it off. One source of truth, so a relaunch and a toggle reach the signer by the same path.

Verification

  • 393/393 JVM tests (389 on the base + 4 new).

  • 88/88 on device, expected=88 started=88 passed=88 failed=0 — not a truncated run.

  • 8 of 8 mutations of the tag rule caught by the JVM suite: opt-out ignored, opt-out inverted, duplicate guard removed, tag prepended, caller's tags dropped, app named wrong, tag key wrong, guard reading the wrong element.

  • 3 of 3 wiring mutations caught on device — the ones no JVM test can fail on:

    mutation result
    signer ignores the setting (applyTo(tags, true)) CAUGHT, 2 failed
    signer never tags (applyTo(tags, false)) CAUGHT, 3 failed
    preference defaults to off CAUGHT, 3 failed

    ClientTagPublishE2ETest is what those land on: preference written before the app starts, real private-key login, real save, tags read back off the relay. The rule test and the settings test both pass while that link is missing.

Notes for merging

  • Rebased onto main after #6–#9 merged; sits directly on ff39aee.
  • versionCode = 21, deliberately not 20: fix/bunker-relogin landed on 19 and two different debug APKs carrying 19 are already out for testing. Two builds sharing a versionCode is how a sideloaded upgrade silently refuses; 20 stays free for whatever is built next off main.
  • Still needed for the full 4-tuple, and not guessable: the pubkey that signs the kind:31990, and a relay hint.

The 88/88 and 393/393 above were re-run after the rebase onto main — the earlier numbers were measured on the pre-merge base and do not apply to this tip.

Closes the "Add client tag" issue. NIP-89's `client` tag, plus the opt-out the spec attaches to it. ## What lands `["client", "Stash"]` on every event the app publishes, applied in `NostrRepository.sign` — every one of the app's publish sites funnels through there, so it is the only place the tag has to go, and the only place the opt-out has to be honoured. **Name-only, not the full tuple.** The spec's form is `["client", <name>, "31990:<pubkey>:<d>", "<relay hint>"]`, and no `kind:31990` for Stash exists yet — re-checked today against damus, nos.lol, nostr.band, primal and haven with a kind-0 positive control on each. An address pointing at nothing is worse than no address: a reader who follows it learns only that the app lied. `docs/NIP89_HANDLER.md` carries the ready-to-sign handler event and the `nak` line; adding the last two entries afterwards is additive and changes nothing for readers who only look at the name. **The opt-out is a requirement, not a courtesy** — "this has privacy implications for users, so clients SHOULD allow users to opt-out of using this tag." Settings → Publishing → *Name Stash on what you publish*, on by default. Opting out means the tag is absent, not blank: a `["client",""]` would still say an app that sets client tags published this. ## One design call The signer reads the **preference** on every sign rather than holding a copy of it. A cached flag has to be re-set from preferences at startup *and* again on every toggle, and either wiring failing is invisible from the outside — the app would go on naming itself on the events of someone who turned it off. One source of truth, so a relaunch and a toggle reach the signer by the same path. ## Verification - **393/393 JVM tests** (389 on the base + 4 new). - **88/88 on device**, `expected=88 started=88 passed=88 failed=0` — not a truncated run. - **8 of 8 mutations of the tag rule** caught by the JVM suite: opt-out ignored, opt-out inverted, duplicate guard removed, tag prepended, caller's tags dropped, app named wrong, tag key wrong, guard reading the wrong element. - **3 of 3 wiring mutations caught on device** — the ones no JVM test can fail on: | mutation | result | |---|---| | signer ignores the setting (`applyTo(tags, true)`) | CAUGHT, 2 failed | | signer never tags (`applyTo(tags, false)`) | CAUGHT, 3 failed | | preference defaults to off | CAUGHT, 3 failed | `ClientTagPublishE2ETest` is what those land on: preference written before the app starts, real private-key login, real save, tags read back off the relay. The rule test and the settings test both pass while that link is missing. ## Notes for merging - Rebased onto `main` after #6–#9 merged; sits directly on `ff39aee`. - `versionCode = 21`, deliberately not 20: `fix/bunker-relogin` landed on 19 and two *different* debug APKs carrying 19 are already out for testing. Two builds sharing a versionCode is how a sideloaded upgrade silently refuses; 20 stays free for whatever is built next off main. - Still needed for the full 4-tuple, and not guessable: the pubkey that signs the `kind:31990`, and a relay hint. The 88/88 and 393/393 above were re-run after the rebase onto `main` — the earlier numbers were measured on the pre-merge base and do not apply to this tip.
NIP-89's `client` tag says which app made an event. Every event Stash
publishes is built in one place, `NostrRepository.sign`, so that is where
the tag goes and the only place it has to go.

The name alone, not the full `name`/`31990` address/relay-hint tuple: no
kind:31990 handler event for Stash exists yet, and an address pointing at
nothing is worse than no address — a reader who follows it learns only
that the app lied. The other three entries are additive later. Name-only
is also what Amethyst, Alexandria and Imwald put on their own events.

The spec attaches a condition: "this has privacy implications for users,
so clients SHOULD allow users to opt-out of using this tag." Hence a flag
rather than a constant. The setting that drives it comes next.
NIP-89 asks for it: "this has privacy implications for users, so clients
SHOULD allow users to opt-out of using this tag." A switch under
Settings > Publishing, on by default — the tag is the useful behaviour
and the spec asks for an opt-out, not an opt-in.

The signer reads the preference on every sign rather than holding a copy.
A cached flag would have to be set from preferences at startup *and*
again on every change, and either wiring failing is invisible from the
outside: the app would go on naming itself on the events of a user who
turned it off. One source of truth, one path, and a relaunch and a toggle
reach the signer the same way.

ClientTagPublishE2ETest is what covers that path end to end — preference
written before the app starts, real login, real save, tags read back off
the relay. The rule test and the settings test can both pass while that
link is missing.
21, not 19: `fix/bunker-relogin` — the other open branch on this same
stack — bumps to 19 too, and two different debug APKs carrying 19 are
already out for testing. Two builds sharing a versionCode is how a
sideloaded upgrade silently refuses. 20 is left free for that branch.
laoc force-pushed feat/client-tag from a3f2daa489
All checks were successful
build / build (push) Successful in 7m1s
to 2e7ed9ff35 2026-08-06 10:41:21 +00:00
Compare
laoc changed target branch from feat/nsec-login to main 2026-08-06 10:41:27 +00:00
laoc merged commit 3956d436ca into main 2026-08-06 11:32:00 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
laoc/stash-bookmark!10
No description provided.