diff --git a/docs/community-organizer-spec.md b/docs/community-organizer-spec.md index 4cb14ad..da16075 100644 --- a/docs/community-organizer-spec.md +++ b/docs/community-organizer-spec.md @@ -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