docs(spec): sync identity-model section with shipped nsecbunkerd state

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 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-28 00:42:47 +02:00
commit b1c37161dd

View file

@ -463,12 +463,15 @@ per-user signing plugs in without refactor. Recommended interface
**The steady-state architecture is the operator-IdP-with-sidecar-bunker **The steady-state architecture is the operator-IdP-with-sidecar-bunker
pattern.** The bot (or any other client of the identity provider) holds 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. 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 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 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 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 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: `LocalSigner` is included in the abstraction for two narrow reasons:
(a) it's how identity providers migrate existing plaintext-nsec rows (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 user's pubkey (for display, addressing, ECDH)
- The bunker URL (transport: which relay channel the bunker listens on) - The bunker URL (transport: which relay channel the bunker listens on)
- The connection token (the credential to authenticate to the bunker) - The connection token (the credential to authenticate to the bunker)
- The granted permission scope (e.g. `sign_event:31922,31923,31925` - The granted permission scope: a `(method, kind)` whitelist
for a community-organizer bot — no kind-1 notes, no DMs, no profile (e.g. `sign_event:31922,31923,31925` for a community-organizer bot —
edits) 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 Per-device-scoped tokens are the security property that makes
multi-client identity safe. If the bot is compromised, the attacker 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 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 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 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 #### Identity-provider options
@ -804,12 +812,17 @@ Not yet decided in this draft:
and a state transition spec. and a state transition spec.
- **External signer story.** Architecture is concrete in §7.2 / §7.3 - **External signer story.** Architecture is concrete in §7.2 / §7.3
/ §12 (operator-IdP with sidecar `nsecbunkerd`, per-device scoped / §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) connection tokens). The signer abstraction
has shipped the signer abstraction; phase 2 ([#18](https://git.atitlan.io/aiolabs/lnbits/issues/18)) ([aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9))
is the actual bunker integration. Pending design: ergonomics of the and the bunker integration
chat-side binding flow (DM the bot? web callback? both?), and how ([aiolabs/lnbits#18](https://git.atitlan.io/aiolabs/lnbits/issues/18))
to handle `ClientSideOnlySigner` users whose events can't be have both shipped end-to-end; `nsecbunkerd` is deployed and signs
bot-published at all (the bot subscribes and mirrors instead). 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. 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) This is the architecture from [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9)
(the IdP framing and signer abstraction) and (the IdP framing and signer abstraction) and
[aiolabs/lnbits#18](https://git.atitlan.io/aiolabs/lnbits/issues/18) [aiolabs/lnbits#18](https://git.atitlan.io/aiolabs/lnbits/issues/18)
(the concrete bunker integration using (the concrete bunker integration). Both have shipped end-to-end: the
[`nsecbunkerd`](https://github.com/kind-0/nsecbunkerd)). PR #17 has signer abstraction lives in `lnbits/core/signers/`, and a forked
shipped phase 1 (the abstraction + transitional `LocalSigner` + the [`nsecbunkerd`](https://git.atitlan.io/aiolabs/nsecbunkerd) (deploys
classify migration); phase 2 (#18) is the actual bunker integration. 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" #### 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 The pattern is reusable beyond LNbits — any operator who wants to
provide identity-as-a-service to their users can run this same shape provide identity-as-a-service to their users can run this same shape
with any NIP-46-compliant bunker (`nsecbunkerd`, Pablo's reference with any NIP-46-compliant bunker (`nsecbunkerd`, `nak bunker`, or any
implementation, or any future alternative). The Nostriga 2024 NIP-46 future alternative). The Nostriga 2024 NIP-46 panel describes the
panel describes the same architecture. 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 #### Adopters without an IdP
@ -914,4 +948,14 @@ the bot signing as itself and human attribution carried in the
## Changelog ## 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. - **0.1** (2026-05-24) — initial draft.