Fixes reported from the live Vercel deployment:
- vercel.json: remove cleanUrls, which was breaking every internal .html
link (ERR_FAILED) since links already use explicit extensions
- Recovery export was silently broken after login or password rotation:
the crypto worker's session state only picked up the recovery capsule
event on register/recover, never on login/changePassword, even though
the decrypted payload always carries it. Surfaced recoveryCapsuleEvent
(and recoveryPublicKey for changePassword) through core's LoginResult /
ChangePasswordResult and wired them into the worker session; covered by
a new account.e2e.test.ts case
- New accounts never got a Nostr profile: publishProfileAndRelayLists is
now called with name: loginName after registration, so the npub's
profile defaults to the chosen login name
- Password manager never recognized the fields (shadow DOM + JS-driven
submission defeat the passive save-prompt heuristics, and the
generated-password screen never put the password in a real input at
all). Fixed with an explicit navigator.credentials.store() call after
every register/login/recover/changePassword; documented as
Chromium-only in docs.html
New: optional manual/custom password (packages/core/src/account/manualPassword.ts),
opt-in per spec §9.3's "future release may permit manual passwords behind
an advanced setting" allowance. Enforces a length floor, a common-password
blocklist, a login-name-substring check, a low-variety-pattern check, and
an entropy floor; the widget shows live non-blocking feedback as the user
types. The generated high-entropy passphrase remains the default,
recommended option.
Integration/interop support:
- window.nostr provider now carries a readonly _bitlogin marker
- window.bitlogin ({ version, isActiveSigner() }) is installed
unconditionally as soon as the script loads, independent of login
state or window.nostr's current occupant. Found and fixed a real bug
along the way: an unconditional assignment is required because browsers
auto-expose any element with a matching id/name as a same-named global,
so <bitlogin-auth id="bitlogin"> silently shadowed window.bitlogin
under a naive `if (!window.bitlogin)` guard. Renamed the demo's own
element id to avoid the same trap and documented the gotcha in docs.html
- docs.html: new section on supporting multiple signing methods
(extensions, NIP-46, BitLogin) alongside each other
- Added packages/demo/public/llms.txt for AI-assisted integration
Deployment: GitHub Pages requires a paid plan for private repos, so its
workflow and the .github directory are removed; README documents
Vercel-only deployment and the direct-from-CDN integration path.
Docs: added docs/future-recovery-options.md, laying out passkey-based
2FA/recovery options (pure 2FA, full independent recovery, and a
constrained middle ground) with pros/cons for future consideration —
not a development plan, nothing here is decided.
59/59 tests pass; core and widget typecheck clean; all fixes verified
live in Chromium via Playwright against an in-memory mock relay.
Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
7.0 KiB
BitLogin
login for normies.
A static, relay-backed portable account protocol: a familiar login name and password that unlocks a portable Nostr identity, plus an independent 12-word recovery phrase. Encrypted account material lives on public Nostr relays — no account server, no password-reset email, no database.
This repo implements the protocol from docs/spec.md (v0.3) as a modular
system any static site can embed for login and Nostr signing.
Layout
packages/
core/ @bitlogin/core — protocol implementation: crypto, capsules, Nostr
relay I/O, account flows. Framework- and DOM-free,
runs in Node or a browser/worker.
widget/ @bitlogin/widget — <bitlogin-auth> custom element + a NIP-07-shaped
window.nostr provider, backed by a dedicated
crypto Web Worker. Built with Vite into two
static files any site can <script> in.
demo/ @bitlogin/demo — plain static site (no build step of its own)
consuming the widget: a landing/branding page,
an integration guide, and an account-manager
page (create/login/recover, password rotation,
identity export).
Quick start
npm install # installs all three workspaces
npm test # runs @bitlogin/core's test suite (vitest)
npm run build # builds core, then widget
npm run dev:demo # assembles + serves the demo at :4173
@bitlogin/demo's npm run build copies the built widget into
packages/demo/dist/vendor/bitlogin/ alongside the static HTML — the result
in packages/demo/dist/ is a complete, ready-to-host static site.
Embedding BitLogin in your own static site
<script type="module" src="/vendor/bitlogin/bitlogin.js"></script>
<bitlogin-auth
vault-relays="wss://relay-one.example,wss://relay-two.example,wss://relay-three.example"
discovery-relays="wss://discovery-one.example,wss://discovery-two.example">
</bitlogin-auth>
bitlogin.js and cryptoWorker.js (plus the shared chunk they both import)
must be deployed side by side — the main script resolves the worker's URL at
runtime relative to its own location, so this works from any path, not just
the site root.
Once a user signs in, window.nostr is installed with the de facto NIP-07
shape (getPublicKey, signEvent, getRelays, nip44.encrypt/decrypt), so
existing Nostr web apps work against it unmodified. Private keys never leave
the worker; the main thread only ever sees public keys, signed events, and
ciphertext. See packages/demo/public/docs.html for the full integration
guide, and packages/demo/public/account.html for a working example
(create → confirm recovery phrase → sign an event → rotate password →
export identity).
Deployment
The demo site deploys to Vercel: vercel.json at the repo root sets the
build command (core → widget → demo) and output directory, so importing this
repo at vercel.com/new needs no manual
configuration. GitHub Pages was evaluated but dropped — Pages requires a paid
plan for private repositories, whereas Vercel's free tier deploys a private
repo without that restriction.
Because the deployed demo serves the built widget files as plain static
assets at /vendor/bitlogin/, any other site can point directly at the
Vercel deployment today without installing anything locally:
<script type="module" src="https://bitlogin.vercel.app/vendor/bitlogin/bitlogin.js"></script>
<bitlogin-auth></bitlogin-auth>
This is fine for prototyping across multiple projects; for production use in someone else's app, self-hosting the built files (or a future published npm package / CDN release) avoids depending on this demo deployment's uptime.
What's implemented
This build covers the protocol's Phase 0 (cryptographic core + test vectors) and the load-bearing parts of Phase 1 (account MVP), verified two ways:
- Protocol-level tests (
npm test, 49 tests): Argon2id/ScalarExpand/JCS/ padding/timestamp unit tests with the spec's exact byte layouts (§11), and full end-to-end scenarios — create → clean-device login, phrase recovery → new credentials → clean-device login, password change with mandatory tombstone + NIP-09 deletion, relay-loss → replica repair (including keyless recovery-event rebroadcast), rollback-warning and relay-disagreement detection — run against an in-memory mock relay (packages/core/src/test-support/mockRelay.ts), since this sandbox has no route to public Nostr relays. - Real-browser verification: the same flows were driven end-to-end in
actual Chromium against the built widget, confirming the crypto worker,
IndexedDB storage, and
window.nostrprovider work together outside the test harness.
Implemented: registration (§15), any-device password login with rollback and
relay-disagreement detection (§16), phrase recovery with recovery-capsule
refresh (§17), known-password change with mandatory tombstone + NIP-09
deletion (§18), replica repair (§24.4), recovery export (§19.5), NIP-19
nsec/npub encoding, importing an existing Nostr nsec as the everyday
identity (§28.1, §SF10 — with a preview-before-commit UI step), a NIP-44 v2
implementation for the window.nostr.nip44 surface, bootstrap/discovery
relay list plumbing (§19), and the full crypto stack from §11 (Argon2id,
HKDF, ScalarExpand, AES-256-GCM, JCS, fixed padding buckets).
Deferred / not in this pass:
- Phase 2 messaging (NIP-17 inbox/compose) — the NIP-44 primitive is built and tested, but no conversation UI is wired up.
- Phase 3 application redirect-auth flow (§26.3) and NIP-46/NIP-07 browser
extension interop beyond the
window.nostrshim. - NIP-49
ncryptsecexport (§28.2) — deliberately left out rather than shipped unverified against official test vectors; plainnsec/npub(NIP-19) export is implemented instead. - The maintainer-signed bootstrap relay-list channel (§19.1) has the verify/merge logic implemented but ships with a placeholder maintainer public key — a real deployment needs to swap in a real one.
- Native clients, hardware-backed recovery, independent second implementation, and formal security audit (Phase 4).
Development notes
- Crypto primitives use audited libraries:
@noble/curves(secp256k1 Schnorr),@noble/hashes(SHA-256, HKDF, HMAC),@noble/ciphers(ChaCha20 for NIP-44),@scure/bip39(BIP-39), native WebCrypto (AES-256-GCM), andhash-wasm(Argon2id). - The Argon2id profile (
bitlogin-argon2id-v1) uses parallelism 1 rather than RFC 9106's recommended 4, for reasons documented inpackages/core/src/crypto/argon2id.ts(§11.2 of the spec). - Passphrases are generated from the EFF long Diceware wordlist
(
diceware-wordlist-en-eff), matching the spec's ~12.9-bits/word, six-word/~77-bit default (§9.2).