From b1c37161ddb09ac138a07a4e3812833af58ec017 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 28 Jun 2026 00:42:47 +0200 Subject: [PATCH] docs(spec): sync identity-model section with shipped nsecbunkerd state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aiolabs/lnbits #9 + #18 are no longer in-flight; aiolabs/nsecbunkerd deploys live from dev with sign-time TTL+revoke (#27), windowed usage caps (#34), hard-reject on lapsed binding (#38), and a full NDK -> nostr-tools transport port (#43/#45/#47). §7.2 now mandates sign-time lifecycle enforcement; §7.3 lists expiresAt + usage caps as part of the binding artifact; §12 swaps the upstream link for the aiolabs fork and adds the never-full-wipe nsecbunker.db operational hazard. Co-Authored-By: Claude Opus 4.7 --- docs/community-organizer-spec.md | 82 ++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/docs/community-organizer-spec.md b/docs/community-organizer-spec.md index 4174e8d..cd40f8b 100644 --- a/docs/community-organizer-spec.md +++ b/docs/community-organizer-spec.md @@ -463,12 +463,15 @@ per-user signing plugs in without refactor. Recommended interface **The steady-state architecture is the operator-IdP-with-sidecar-bunker pattern.** The bot (or any other client of the identity provider) holds zero nsec material; a separate bunker process on the same host (e.g. -`nak bunker`) holds every user's target key and signs on RPC. The bot +`nsecbunkerd`) holds every user's target key and signs on RPC. The bot authenticates to the bunker with a per-user NIP-46 connection token issued at binding time (§7.3), scoped to only the kinds the bot needs to sign. Compromise of the bot leaks the scoped tokens; revoking them at the bunker side is one RPC and doesn't disturb the target key or -any other client device the user has authorized. +any other client device the user has authorized. The bunker MUST +enforce the token's TTL and revocation **live at sign time**, not +only at connect time — a token that lapses mid-session stops signing +on the next request. `LocalSigner` is included in the abstraction for two narrow reasons: (a) it's how identity providers migrate existing plaintext-nsec rows @@ -524,9 +527,11 @@ sign on behalf of the user. The bot stores: - The user's pubkey (for display, addressing, ECDH) - The bunker URL (transport: which relay channel the bunker listens on) - The connection token (the credential to authenticate to the bunker) -- The granted permission scope (e.g. `sign_event:31922,31923,31925` - for a community-organizer bot — no kind-1 notes, no DMs, no profile - edits) +- The granted permission scope: a `(method, kind)` whitelist + (e.g. `sign_event:31922,31923,31925` for a community-organizer bot — + no kind-1 notes, no DMs, no profile edits), an `expiresAt` TTL, and + optionally per-rule windowed usage caps (e.g. "20 sign_events per + hour AND 200 per day") Per-device-scoped tokens are the security property that makes multi-client identity safe. If the bot is compromised, the attacker @@ -534,7 +539,10 @@ can publish NIP-52 events as the user — annoying, but they can't publish kind-1 notes, change the user's profile, send DMs, or do anything else the user's other devices can. Revocation of the bot's token at the bunker side is one RPC and doesn't disturb the target -key or any other authorized device. +key or any other authorized device. Any in-scope cap (TTL, usage +window) is enforced live at sign time, not only at connect, so a +compromise window is bounded by both the operator's explicit revoke +and whichever cap fires first. #### Identity-provider options @@ -804,12 +812,17 @@ Not yet decided in this draft: and a state transition spec. - **External signer story.** Architecture is concrete in §7.2 / §7.3 / §12 (operator-IdP with sidecar `nsecbunkerd`, per-device scoped - connection tokens). Phase 1 of [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9) - has shipped the signer abstraction; phase 2 ([#18](https://git.atitlan.io/aiolabs/lnbits/issues/18)) - is the actual bunker integration. Pending design: 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). + connection tokens). The signer abstraction + ([aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9)) + and the bunker integration + ([aiolabs/lnbits#18](https://git.atitlan.io/aiolabs/lnbits/issues/18)) + have both shipped end-to-end; `nsecbunkerd` is deployed and signs + live for the operator/agent identities on the reference instance. + Pending design: ergonomics of the chat-side **per-user** binding + flow (DM the bot? web callback? both?) — the tracker plugin's + Phase 2c — 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. @@ -881,10 +894,22 @@ on the same host: This is the architecture from [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9) (the IdP framing and signer abstraction) and [aiolabs/lnbits#18](https://git.atitlan.io/aiolabs/lnbits/issues/18) -(the concrete bunker integration using -[`nsecbunkerd`](https://github.com/kind-0/nsecbunkerd)). PR #17 has -shipped phase 1 (the abstraction + transitional `LocalSigner` + the -classify migration); phase 2 (#18) is the actual bunker integration. +(the concrete bunker integration). Both have shipped end-to-end: the +signer abstraction lives in `lnbits/core/signers/`, and a forked +[`nsecbunkerd`](https://git.atitlan.io/aiolabs/nsecbunkerd) (deploys +from the `dev` branch — `master` is parked at upstream) runs as the +sidecar. The fork enforces token TTL + revoke live at sign time +([#27](https://git.atitlan.io/aiolabs/nsecbunkerd/pulls/27)), +per-rule windowed usage caps +([#34](https://git.atitlan.io/aiolabs/nsecbunkerd/pulls/34)), +hard-rejects (rather than re-prompts) a lapsed binding +([#38](https://git.atitlan.io/aiolabs/nsecbunkerd/pulls/38)), and is +fully off NDK after the `nostr-tools` relay-pool port +([#43](https://git.atitlan.io/aiolabs/nsecbunkerd/pulls/43) / +[#45](https://git.atitlan.io/aiolabs/nsecbunkerd/pulls/45) / +[#47](https://git.atitlan.io/aiolabs/nsecbunkerd/pulls/47)) — relay +subscriptions survive reconnect, and cross-relay event dedup keeps +usage caps from over-counting. #### Why a sidecar bunker, not "encrypt at rest in the IdP" @@ -898,9 +923,18 @@ gets its own audit log, independent of the IdP's. The pattern is reusable beyond LNbits — any operator who wants to provide identity-as-a-service to their users can run this same shape -with any NIP-46-compliant bunker (`nsecbunkerd`, Pablo's reference -implementation, or any future alternative). The Nostriga 2024 NIP-46 -panel describes the same architecture. +with any NIP-46-compliant bunker (`nsecbunkerd`, `nak bunker`, or any +future alternative). The Nostriga 2024 NIP-46 panel describes the +same architecture. + +> **Operational note for adopters using `nsecbunkerd`.** Pairing +> state is split between the bunker's `nsecbunker.db` (per-account +> `KeyUser` binding + redeemed `Token`) and the IdP's signer config +> (`{token, client_nsec, policy_id}`). **Never full-wipe +> `nsecbunker.db`** to strip stale ACL rows — full wipe orphans the +> IdP's per-user signer config and the only auto-repair re-mints +> identities. Strip with a targeted `DELETE FROM SigningCondition`. +> See the bunker's `docs/runbook-migrations.md`. #### Adopters without an IdP @@ -914,4 +948,14 @@ the bot signing as itself and human attribution carried in the ## Changelog +- **0.2** (2026-06-28) — reflect shipped state of the reference IdP + + sidecar bunker. `aiolabs/lnbits#9` and `#18` are no longer + in-flight; `aiolabs/nsecbunkerd` (deploys from `dev`) is live with + sign-time enforcement of token TTL + revoke (#27), per-rule + windowed usage caps (#34), hard-reject on lapsed binding (#38), + and a full NDK→`nostr-tools` transport port (#43/#45/#47). §7.2 + now mandates sign-time lifecycle enforcement; §7.3 lists `expiresAt` + + usage caps as part of the binding artifact; §12 adds the + full-wipe operational hazard. Open question on "external signer + story" trimmed to per-user binding ergonomics (tracker Phase 2c). - **0.1** (2026-05-24) — initial draft.