diff --git a/docs/README.md b/docs/README.md index 4d0e21b..117f9b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,9 @@ for compatible applications. - [`spec.md`](./spec.md) — canonical BitLogin protocol specification - [`second-factor.md`](./second-factor.md) — second-factor design notes +- [`login-methods.md`](./login-methods.md) — proposal: the widget as the single + login surface, adding NIP-07 and NIP-46 sign-in behind the existing + provider facade (nsec-paste login rejected on the record) ## Portable account layer diff --git a/docs/login-methods.md b/docs/login-methods.md new file mode 100644 index 0000000..43a17cb --- /dev/null +++ b/docs/login-methods.md @@ -0,0 +1,234 @@ +# BitLogin as the Single Login Surface — NIP-07 and NIP-46 Methods + +**Document version:** 0.1 (design proposal) +**Status:** Proposal only — nothing here is implemented. §LM6 records a settled +rejection (nsec paste as a login method), kept on the record. +**Depends on:** `docs/spec.md` (§ references point there unless marked §LM or +§SF), `docs/second-factor.md` §SF10 (imported identities, implemented) + +--- + +# LM1. Purpose and scope + +Today `` offers exactly one way in: a BitLogin login name and +password. A site that wants to serve existing Nostr users — extension holders, +bunker users — has to build its own login chooser and treat BitLogin as one +button in that menu. + +This proposal inverts that. The widget becomes the login menu: one embed, one +host-facing surface, and the user picks their method behind it: + +1. **BitLogin login name + password** — the existing derived-account flow, + unchanged, and still the default presented to new users. +2. **NIP-07** — delegate to a signer extension already installed in the + browser (§LM4). +3. **NIP-46** — connect to a remote signer ("bunker") over relays (§LM5). + +§26.4 already names NIP-07 and NIP-46 as anticipated future signer support; +this document is the design for that line. + +Out of scope, permanently: accepting a pasted `nsec` as a login method +(§LM6). Users holding a raw key are routed to the implemented §SF10 import +flow instead, which wraps the key into a full BitLogin account once rather +than handling it at every sign-in. + +# LM2. Why aggregate rather than be aggregated + +The bottleneck for BitLogin is developer adoption, and a developer evaluating +a login widget hears one objection immediately: *my existing users have +extensions and bunkers; a password-only widget doesn't cover them.* Supporting +all three methods behind one embed removes that objection, and every embed +then distributes BitLogin's native method as the easy path for everyone who +does not already have Nostr keys. + +Prior art validates the shape: `nostr-login` (nostrband) is an embeddable +widget aggregating extension, NIP-46, and other methods, and it occupies the +"one login surface" niche today. BitLogin's differentiator is not the +aggregator layer — that part is commodity — but the derived-account custody +underneath it. The aggregator is worth building because it is the wedge that +gets the custody in front of users, not because it is novel. + +The positioning consequence is worth stating plainly: with this change, a site +never needs a login UI other than BitLogin, and BitLogin's security story — +*no method ever puts a root key on the page* — holds across every method it +offers (§LM6). + +# LM3. Architecture: one facade, pluggable signers + +The widget already exposes a NIP-07-shaped provider — installed as +`window.nostr` (element.ts installs and releases it, taking over from an +extension where configurable) and mirrored as an element-scoped API. Hosts +program against that facade, not against the derivation machinery. That +boundary is exactly where the abstraction goes. + +```text +host page ── window.nostr / API (unchanged surface) + │ + provider facade + │ + active Signer + ┌──────┼──────────────┐ + DerivedSigner Nip07Signer Nip46Signer + (existing (delegates (bunker client + worker path) to the over relays) + extension's + provider) +``` + +A `Signer` implements the calls the facade already promises — `getPublicKey`, +`signEvent`, NIP-44 and NIP-04 encrypt/decrypt — plus metadata: + +- `method`: `"bitlogin" | "nip07" | "nip46"`, surfaced to the host in the + sign-in event so applications can adapt (e.g. hide password-rotation UI for + non-BitLogin sessions). +- `capabilities`: which optional calls this signer actually supports. + Extensions vary — some lack `nip44` — and the facade must report honestly + rather than throw surprises (§LM4). + +Rules that hold for every signer: + +- **No silent hangs.** Every facade call has a timeout and a typed failure. + This restates the Tier 1 rule from the 2026-07-30 audit for a world where a + signature may wait on a phone that is asleep (§LM5). +- **The facade is the only surface.** No signer leaks its backend (extension + provider object, bunker connection, worker handle) to the host page. +- **One active signer per element.** Method choice happens at sign-in; + switching methods is a sign-out and sign-in, composing with (not + complicating) the account-switching design. + +# LM4. NIP-07: delegate to an installed extension + +The cheapest method: no key custody, no new stored secrets, no relay traffic. +The widget detects an injected `window.nostr` that is not its own, offers +"Sign in with extension," and the `Nip07Signer` passes facade calls through +to the extension's provider. + +Design points: + +1. **Arbitration inverts, not disappears.** The current element takes over + `window.nostr` at sign-in and warns when an extension's property is + non-configurable. With a NIP-07 session the widget must *not* fight the + extension — the extension's provider is the backend. The existing + install/release logic stays for BitLogin-method sessions; NIP-07 sessions + leave `window.nostr` alone (or install a passthrough only where the + extension injected late). The detection must also snapshot the extension's + provider *before* the widget's own install, so the two never confuse each + other. +2. **Capability probing.** `nip44` presence is checked on the extension + object and reported in `capabilities`; absent calls reject with a typed + "unsupported by this signer" error rather than a TypeError from an + undefined property. +3. **User-visible identity.** After `getPublicKey`, the widget shows the + `npub` it got, so the user can notice a wrong-profile extension state + before the host application acts on it. +4. **No session material of its own.** A NIP-07 session persists nothing — + on reload the extension is simply asked again. This makes NIP-07 the + right first shipping increment (§LM8). + +# LM5. NIP-46: connect to a remote signer + +The user's key lives in a bunker (nsecBunker, Amber, or similar); the widget +holds only a client keypair and a connection secret, and requests signatures +over relays. This is the method most aligned with BitLogin's security story — +the widget custodies a *revocable credential*, never the key. If a +compromised host exfiltrates the connection, the user revokes it at the +signer; contrast a stolen `nsec`, which is unrotatable (§LM6). + +Design points: + +1. **Connect flow.** Accept a `bunker://` URI or nostrconnect handshake; + surface the signer's `auth_url` approval ceremony inside the widget UI + (open in a new tab, show pending state, resume on approval). The connect + ceremony is widget UI work of the same kind as the NWC REVEAL flow that + shipped 2026-07-30. +2. **Latency is a feature of the method, not a bug to hide.** Every + `signEvent` is a relay round trip to a device that may be offline. The + facade shows a pending state, applies a timeout, and fails with a typed, + retryable error. A host that got instant signatures from the derived + path must not discover NIP-46 latency as a hang. +3. **Persistence of the connection secret — deliberately phased.** + - *Phase one: memory-only.* The connection lives for the session; a + reload means reconnecting. Nothing new is stored anywhere, so nothing + new needs review. + - *Phase two: a Connection Vault record.* A NIP-46 connection is shaped + like the NWC credentials the vault already models — relay URLs plus a + secret, revocable at the far end, scoped in authority. Persisting it as + a vault record profile falls squarely under the standing gate: + **external security review before the vault holds anything beyond a + budget-capped NWC connection.** Phase two therefore waits on that + review; phase one does not. +4. **Client keypair hygiene.** The ephemeral client key for the NIP-46 + conversation is generated by the CSPRNG chokepoint like every other key + in the codebase, and lives in the worker, not the DOM thread. + +# LM6. Rejected: nsec paste as a login method + +Considered and rejected, on the record. + +A paste box for `nsec` on an embedded, same-origin widget means the root key +transits every host page that embeds BitLogin, at every sign-in, forever. An +XSS on *any* embedding site exfiltrates a key that Nostr provides no way to +rotate. §SF10 already states the principle: *the paste is the risk* — entering +an `nsec` into a web page is the most dangerous single action in Nostr use. +A login method institutionalizes that action as a habit; it also breaks the +claim §LM2 leans on, that no BitLogin method ever puts a root key on the page. + +The sanctioned path for a user holding a raw key is the **implemented §SF10 +import flow**: paste once, on a build you trust, and the key is wrapped into +a full BitLogin account — capsules built around the imported key, a fresh +recovery phrase, password sign-in from then on. The login surface should +route to it: an "I have an nsec" affordance that opens the import flow, so +the user's need is met without the recurring paste. + +The distinction in one line: **import custodies the key once, under the +account's full protections; a login method would handle it on every sign-in, +under none of them.** + +# LM7. Sessions are thin for non-BitLogin methods + +Everything in the portable account layer — Connection Vault, portable app +state, entitlements, second factor — is built on capsule custody. A NIP-07 or +NIP-46 session has no capsule, so in this design those sessions are **signer +sessions only**: the host gets `window.nostr`, the sign-in event with +`method` and `capabilities`, and nothing else. No vault, no portable state, +no second factor. + +This is deliberate: + +- It keeps the aggregator commodity-cheap — the expensive, review-gated + machinery stays exclusive to derived accounts. +- It creates the honest upsell: "create a BitLogin password to get your + settings and connections on every device." The widget may offer this to a + signed-in NIP-07/NIP-46 user; note that upgrading an extension or bunker + identity to a *full* account requires the private key BitLogin never sees + for those methods, so the upsell mints a derived account (new identity) or, + for a user who separately holds their key, points at §SF10 import. + +Extending capsule-backed features to non-derived identities is possible in +principle but is a protocol-surface expansion this document explicitly does +not propose. + +# LM8. Phasing + +1. **NIP-07** — smallest increment: detection, delegation, capability + probing, arbitration inversion. No new secrets, no relay traffic, no + review gate touched. +2. **NIP-46, memory-only** — connect ceremony, pending/timeout UX, typed + errors. Still nothing persisted. +3. **NIP-46 persistence as a vault profile** — after the external security + review the vault is already gated on. + +Each step ships independently; the login UI grows one option at a time. + +# LM9. Open questions + +1. Method presentation: is password-first with an "other ways to sign in" + expander the right default, or should extension detection promote NIP-07 + when present? +2. Should a NIP-07 session survive reload by silently re-asking the + extension, or require an explicit click each visit (extension prompt + fatigue vs. surprise sessions)? +3. Does the sign-in event schema need versioning now that `method` and + `capabilities` are added to it? +4. Whether the widget should expose "which methods are enabled" as element + attributes so a host can, e.g., disable NIP-46 (`methods="bitlogin,nip07"`).