docs(spec): concretize identity model — signer abstraction + binding

Three connected updates triggered by aiolabs/lnbits#9 (in-progress
hardening of user Nostr keypair storage with NIP-46 bunker support):

§7.2 expanded — per-user signing is no longer abstract "future
work". Defines the 4-implementation signer abstraction (BotSigner /
LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner) matching
lnbits#9's design. MUST design plugins with this abstraction even
when v1 only ships BotSigner — Phase 2c plugs in user signers
without refactor.

§7.3 new — external-identity binding. Any system that can answer
"for chat handle X, what signer should I use?" works. LNbits is the
reference identity provider but not the only valid one. Binding MUST
be opt-in, verifiable, revocable.

§11 + §12 updated — open question on bunker UX folds into the new
sections; reference identity provider added to §12 with pointer to
lnbits#9.

Spec stays runtime-agnostic — LNbits is one valid provider, not
mandatory. Communities without an existing Nostr identity stack stay
on v1 BotSigner indefinitely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-24 17:21:49 +02:00
commit 3d29beecba

View file

@ -443,16 +443,70 @@ enables classifier tuning.
- Relay operators MAY auth-gate (require NIP-42) to prevent
community-scoped writes from non-members. Out of band coordination.
### 7.2 Future (bunker / remote signers)
### 7.2 Per-user signing (v2; design-for-it now)
Future spec versions will support per-user signing via NIP-46
(remote signing) so that events are attributable to individual users'
Nostr identities, not the bot. The chat-room → community mapping
stays the same; only the signer changes.
Once external-identity binding is in place (see §7.3), the bot SHOULD
sign events as the originating user, not as itself. The chat-room →
community mapping stays the same; only the signer changes.
Implementations SHOULD design their signing layer with this
forward-compatibility in mind (i.e. don't hardcode "the bot is
always the signer" deep in the data model).
Implementations MUST design their signing layer as an abstraction so
per-user signing plugs in without refactor. Recommended interface
(adopted from the LNbits reference implementation, see §12):
| Signer | Holds | Where it signs |
|---|---|---|
| `BotSigner` | One server-side keypair shared across all events | Server-side, in-process. The v1 fallback. |
| `LocalSigner` | A user's encrypted-at-rest keypair on the bot's host | Server-side, in-process, but per-user |
| `RemoteBunkerSigner` | A NIP-46 remote-signer connection (per-user) | RPC over relay (kind 24133) to the user's bunker |
| `ClientSideOnlySigner` | Nothing — sentinel meaning "user signs in their own client" | Not the bot; events for this user come in via subscription from elsewhere |
For each captured message the bot:
1. Looks up the originating chat handle (e.g. MXID) in its binding
table (§7.3).
2. If bound and the signer is server-callable (`LocalSigner` /
`RemoteBunkerSigner`), signs as the user. Drop the `author` tag
(the pubkey is the attribution).
3. If unbound — or bound to `ClientSideOnlySigner` for a verb the
user must sign themselves — falls back to `BotSigner` with the
`author` tag (the v1 §7.1 behavior).
This degrades gracefully: a community can run v1 indefinitely with
just `BotSigner`; users opt into sovereignty individually as they
bind their identity.
**NIP-46 is the recommended remote signer protocol.** It's what the
Nostr ecosystem is converging on for client-without-nsec flows, and
it works without browser extensions on iOS — important for any
Matrix client that runs on mobile.
### 7.3 External-identity binding (v2)
For per-user signing to mean anything, the bot needs a verified
mapping from `chat handle → external identity → signer`. The binding
mechanism is implementation-defined but MUST be:
- **Opt-in** per user. No silent association.
- **Verifiable** — the binding proves the user controls both identities
(e.g. magic-link round-trip, signature challenge, NIP-39 external
identity proof).
- **Revocable** — user can unbind. Bot drops back to fallback.
The spec does not mandate a specific identity provider. Any system
that can answer "for chat handle X, what signer should I use?" works:
- A **Lightning/Nostr account system** (LNbits, Alby Hub, etc.) — the
reference implementation, since these already hold user pubkeys
and have an auth surface that can mediate the binding flow.
- A **standalone web app** with its own auth and a `bind chat handle`
flow.
- An **on-Nostr profile claim** — user publishes a kind 0 / 30311 /
similar with a chat-handle attestation; bot reads relay and
cryptographically verifies the claim.
- A **NIP-39 external-identity proof** in the user's profile.
Adopters without any of the above can stay on v1 (`BotSigner` only)
— per-user signing is enhancement, not requirement.
---
@ -698,8 +752,13 @@ Not yet decided in this draft:
- **Migration / weekly review.** Bullet-journal-style migration of
stale open items into a new period. Needs a verb (`!migrate`?)
and a state transition spec.
- **External signer story.** NIP-46 bunker integration concrete
shape.
- **External signer story.** NIP-46 bunker integration is sketched
in §7.2 / §7.3; the LNbits reference identity provider is being
hardened in [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9).
Pending: ergonomics of the chat-side binding flow (DM the bot? web
callback? both?), and how to handle `ClientSideOnlySigner` users
whose events can't be bot-published at all (the bot subscribes and
mirrors instead).
Contributions welcome on any of these.
@ -732,6 +791,21 @@ scoping in §5. A ZeroClaw-based implementation would carry the
`["client", "maubot-tracker", "..."]`; renderers ignore the difference
since they filter by community `a`-tag.
### Reference identity provider
The aiolabs reference implementation uses [LNbits](https://lnbits.com/)
(specifically the [aiolabs fork](https://git.atitlan.io/aiolabs/lnbits))
as its identity provider — each user already has an LNbits account with
a Nostr `pubkey` field; the in-progress [issue #9 hardening](https://git.atitlan.io/aiolabs/lnbits/issues/9)
introduces a `NostrSigner` abstraction (`LocalSigner` / `RemoteBunkerSigner`
/ `ClientSideOnlySigner`) that matches the per-user-signing model in §7.2
exactly. The bot stores `(chat_handle → lnbits_user_id)` and resolves the
signer per-user at publish time.
Other communities can substitute their own identity provider (any system
that maps chat handles to Nostr signers per §7.3) — LNbits is one such
provider, not the only one.
---
## Changelog