mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-27 08:31:21 +00:00
Owner decision: static only, and no per-site provider registration --
which every OAuth path requires (and GitHub/Facebook additionally require
a server). Both on-ramp implementations (service handshake, serverless
Google Drive) are removed the day they shipped; centralized-onramps.md
stays on the record as the analysis of why, superseded by
passkey-login.md.
The passkey rail: a WebAuthn PRF-capable passkey (synced for most users
by Google Password Manager or iCloud Keychain) deterministically derives
the login name and password of a standard capsule account -- passkey.ts
holds the derivation behind a FROZEN v1 known-answer contract with the
same standing rule as knownAnswer.test.ts. The ordinary password flows
do everything else; nothing is stored outside the user's authenticator,
and the rail needs no network of its own.
Flow is idempotent by construction: sign-in against a passkey with no
account routes into setup with the derived credential
(AccountNotFoundError); setup against an existing account signs in
(AccountAlreadyExistsError). Fresh identity or §SF10 nsec import at
setup; Tier B2 phrase card retained ('your account stays yours even if
you lose your passkey'); rotation is the graduation step and says so.
Pins updated: passkey row with honest custody label inside the menu,
OAuth actions provably absent, element still network-free. 11 passkey
tests incl. the frozen vectors; menu and screens verified in Chrome.
Co-Authored-By: Claude Fable 5 <[email protected]>
251 lines
13 KiB
Markdown
251 lines
13 KiB
Markdown
# BitLogin as the Single Login Surface — NIP-07 and NIP-46 Methods
|
|
|
|
**Document version:** 0.3
|
|
**Status:** §LM4 (NIP-07) and §LM5 (NIP-46, memory-only phase) are
|
|
**implemented** (2026-08-04): `signers/` in `@bitlogin/widget`, the NIP-46
|
|
client in `@bitlogin/core`'s `nostr/nip46.ts` with the ephemeral key held in
|
|
the crypto worker, both `bunker://` paste and the nostrconnect QR (rendered
|
|
as dot-style inline SVG), thin sessions per §LM7, password-first per §LM9.1.
|
|
NIP-46 *persistence* (§LM5.3 phase two) remains gated on the external vault
|
|
review. §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 `<bitlogin-auth>` 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).
|
|
|
|
A fourth method — passkey sign-in, where a WebAuthn PRF-capable passkey
|
|
(synced by the user's Google/Apple account) deterministically derives the
|
|
credential of a standard capsule account with zero site setup and zero
|
|
servers — is designed in [`passkey-login.md`](./passkey-login.md). The OAuth
|
|
on-ramps considered before it are superseded and kept on the record in
|
|
[`centralized-onramps.md`](./centralized-onramps.md).
|
|
|
|
# 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 / <bitlogin-auth> 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. **Shipped 2026-08-04.**
|
|
2. **NIP-46, memory-only** — connect ceremony (bunker:// paste and a
|
|
nostrconnect QR), pending/timeout UX, typed errors. Still nothing
|
|
persisted. **Shipped 2026-08-04.** Transport is NIP-44 only, per the
|
|
current NIP-46 spec; legacy NIP-04-only bunkers are not supported.
|
|
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. **Resolved 2026-08-04 (owner decision):** username/password stays the
|
|
primary presentation; alternative methods appear as secondary affordances
|
|
below it, never promoted above the password path even when an extension is
|
|
detected. Pinned by `securityPolicy.test.ts`.
|
|
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"`).
|