Preparing to open the project up beyond this repo's own use:
- LICENSE: MIT, and a "license": "MIT" field on the root and all three
workspace package.json files.
- CONTRIBUTING.md: dev workflow, the mock-relay testing setup (most
sandboxes/CI have no route to public Nostr relays, and registration
needs a 2-of-N ack quorum so a single mock relay always fails), the
spec-first process for anything touching wire format or crypto
(docs/spec.md is canonical; speculative ideas go in their own doc, not
the spec), and stricter expectations for crypto/key-handling changes.
- SECURITY.md: private vulnerability disclosure via GitHub's advisory
flow rather than public issues, given this handles private keys and
password-derived secrets client-side; states current audit status
plainly (none yet).
- README: short "Contributing" section pointing at all three.
Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
Signer interop:
- <bitlogin-auth> gains claimSigner()/releaseSigner() instance methods, and
window.bitlogin gains a matching releaseSigner(). Before this, once the
widget claimed window.nostr it held it forever (even after logout), so a
site offering multiple signing methods (an extension, a NIP-46 bunker,
BitLogin) had no way to let a user switch away from BitLogin without a
page reload. claimSigner() is now also called automatically on every
successful sign-in/sign-up/recovery, and releaseSigner() on logout and
DOM removal. Both fire bitlogin-signer-claimed/-released events.
docs.html and llms.txt document the new API and a switch-signer example.
Manual password parity + fixes:
- The opt-in custom-password path (added earlier per spec §9.3) was only
available at initial account creation; it's now offered on password
rotation and phrase recovery too, via a shared renderManualPasswordFields()/
readManualPassword() pair.
- Added a required confirm-password field with live match feedback,
wherever a manual password can be set.
- Found and fixed a real bug while testing this: a validation failure
(mismatched confirm, weak password) on any manual-password screen fully
re-rendered the form, silently wiping the typed password and
un-checking "I saved it" -- forcing a full retype. Fixed via draft
fields that repopulate the inputs' value attribute across the
failure re-render, and by reading the checkbox before validation
instead of after.
Recovery-phrase display split (§anti-cheat):
- The 12-word phrase and its 3-word verification quiz were shown on the
same screen, defeating the point of the quiz. Split into a
display-only "confirm-phrase" screen and a separate "verify-phrase"
screen where the phrase is hidden while the quiz is answered.
Offline recovery-export import (§19.5):
- recoverWithPhrase() now accepts optional offlineRecoveryCapsuleEvents,
merged with whatever the live relay read returns; recovery still
requires the phrase either way, since the export file never contains
it or a phrase-derived key. Covered by a new core e2e test that
recovers successfully against genuinely unreachable relay URLs using
only the offline candidate.
- The recover-phrase screen gets an optional file input for a previously
downloaded recovery export, parsed and validated client-side before
being passed through to the worker.
Landing page copy updated to reflect that manual passwords are allowed
(with a real entropy floor and common-password check), not disallowed.
60/60 tests pass; core and widget typecheck clean. Verified live in
Chromium via Playwright against a 3-relay in-memory mock quorum: account
creation (generated and manual, including the mismatch/retry-preserves-
draft path), phrase display/quiz split, signer claim/release/reclaim
across logout and re-login, password rotation and phrase recovery with
manual passwords (including logging back in with the result), and the
offline recovery-export file path. Zero page or console errors across
every run.
Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
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
core:
- registerAccount gains an optional everydayPrivateKey (validated scalar,
used instead of generating); result carries an `imported` flag
- importAccount() + decodeEverydayPrivateKey() convenience helpers accept
nsec (bech32) or 64-char hex, wiring in the existing decodeNsec primitive
- e2e tests: imported nsec round-trips to the same npub through both
clean-device login and phrase recovery; malformed keys rejected
widget:
- worker gains a `previewImportKey` action (validate + preview npub without
publishing) and passes importKey through `register`
- <bitlogin-auth> gets an "Import an existing Nostr key" screen: paste
nsec/hex, check-key preview shows the resulting npub for confirmation,
then the normal name -> password -> phrase flow, with the pasted key
dropped from the main thread after registration (§11.10) and a
risky-paste warning shown up front
docs: mark §SF10 implemented; note backup codes are redundant with the
phrase and unlikely to ship; README implemented-list updated.
Verified end-to-end in real Chromium (preview npub, import, clean-device
login all reproduce the imported identity). 51/51 tests pass.
Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
Design proposal (not implemented) for three opt-in, serverless additions
layered on the existing capsule/relay/NIP-44 primitives:
- Phone-approval sign-in via a 2-of-2 split credential-capsule key
- Backup codes (each an independent high-entropy recovery path)
- Importing an existing nsec as the everyday identity
Includes a "why not literal TOTP" section and per-feature threat notes.
Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
Builds core -> widget -> demo and publishes packages/demo/dist via
actions/upload-pages-artifact + deploy-pages. Also makes the manifest and
service worker's precache list path-relative (instead of root-absolute)
so the same build works whether it's served from a domain root or a
GitHub Pages project-site subpath like /bitlogin/.
Repo still needs Settings -> Pages -> Source set to "GitHub Actions" once
if it has never been enabled before.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
Builds out the v0.3 spec as an npm workspace monorepo any static site can
consume for login and Nostr signing:
- packages/core: framework-free protocol implementation — Argon2id/HKDF/
ScalarExpand/AES-256-GCM/JCS crypto primitives, NIP-01 event handling and a
quorum-aware relay pool, credential/recovery capsule construction and
validation, and the full account lifecycle (registration, any-device
login with rollback/relay-disagreement detection, phrase recovery with
recovery-capsule refresh, password rotation with mandatory tombstone +
NIP-09 deletion, replica repair, recovery export, NIP-19 encoding, a
NIP-44 v2 implementation, and a NIP-07-shaped signer). 49 tests cover the
spec's Phase 0 byte-level vectors plus full create/login/recover/rotate
end-to-end scenarios against an in-memory mock relay.
- packages/widget: a <bitlogin-auth> custom element with a dedicated crypto
Web Worker (private keys never touch the main thread) and a window.nostr
NIP-07-compatible provider, so existing Nostr web apps work against it
with zero code changes. Built with Vite into two relocatable static files.
- packages/demo: a plain static site (no build step of its own) consuming
the widget — a branding/landing page, an integration guide, and an
account-manager page covering create/login/recover, password rotation,
and identity export.
All flows were verified twice: protocol-level tests against a mock relay,
and the actual built widget driven end-to-end in real Chromium (registration,
phrase confirmation, event signing, clean-device login, window.nostr, and
phrase recovery all reproduce the identical everyday pubkey).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6