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 - Relay operators MAY auth-gate (require NIP-42) to prevent
community-scoped writes from non-members. Out of band coordination. 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 Once external-identity binding is in place (see §7.3), the bot SHOULD
(remote signing) so that events are attributable to individual users' sign events as the originating user, not as itself. The chat-room →
Nostr identities, not the bot. The chat-room → community mapping community mapping stays the same; only the signer changes.
stays the same; only the signer changes.
Implementations SHOULD design their signing layer with this Implementations MUST design their signing layer as an abstraction so
forward-compatibility in mind (i.e. don't hardcode "the bot is per-user signing plugs in without refactor. Recommended interface
always the signer" deep in the data model). (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 - **Migration / weekly review.** Bullet-journal-style migration of
stale open items into a new period. Needs a verb (`!migrate`?) stale open items into a new period. Needs a verb (`!migrate`?)
and a state transition spec. and a state transition spec.
- **External signer story.** NIP-46 bunker integration concrete - **External signer story.** NIP-46 bunker integration is sketched
shape. 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. 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 `["client", "maubot-tracker", "..."]`; renderers ignore the difference
since they filter by community `a`-tag. 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 ## Changelog