registerAccount() never checked whether a credential capsule already
existed at the locator address it was about to publish to. That
address is a NIP-33 replaceable event fully determined by login name +
password, so registering with a login name + password combination that
was already registered -- by mistake (double-clicking "create account",
confusing it with "sign in"), or by coincidence with someone else's
account under a shared login name -- silently replaced the existing
account's identity binding. The old everyday identity became
unreachable through that login unless the original recovery phrase was
still on hand, with nothing warning the user beforehand.
changePassword() had the identical gap on the new locator: rotating to
a password that happened to already be registered under the same login
name would silently destroy that other account.
Both flows now query for an existing capsule at the target locator
before publishing, and refuse with AccountAlreadyExistsError if one is
found (or a retry-oriented error if no relay could be reached to check).
Two new end-to-end tests reproduce both scenarios and confirm the
pre-existing account survives untouched.
Bumps docs/spec.md to v0.6 (§15.6, §18.1).
After every successful registration -- including nsec imports -- the
widget unconditionally published a default kind 0 profile ({name:
loginName}) plus kind 10002/10050 relay lists. All three are
NIP-01/NIP-65/NIP-17 replaceable events, so importing an nsec that
already had a real profile (display name, bio, avatar, NIP-05) and/or
relay preferences published elsewhere silently erased all of it,
replacing it with just the chosen BitLogin login name and the vault
relays.
publishInitialProfile() (moved into @bitlogin/core so it can be
covered by the existing MockRelay-based test harness) now queries for
an existing event of each kind before publishing, and only fills in
whichever one is genuinely missing. A fresh registration has nothing
to find, so behavior there is unchanged; an import now leaves
whatever the identity already had untouched.
Bumps docs/spec.md to v0.5 (§15.8, §28.1) and adds two new core tests
covering the fresh-identity and existing-profile-preserved cases.
Found during a real integration pass (embedding this widget in BitRoad, a
separate Nostr commerce app):
- Password rotation's old-locator tombstone (§18.1) is only advisory -- a
relay that never processes it can keep serving the pre-rotation capsule
indefinitely. loginWithPassword/changePassword previously accepted that
stale capsule with only a non-blocking warning. A device that has already
seen the newer generation now refuses it (RollbackDetectedError) unless the
caller explicitly passes acknowledgeRollback: true for the narrower
relay-lag case. The widget surfaces this as a blocking confirmation screen
before claiming the signer, not a banner shown after the fact.
- NIP-44 capped plaintext at 65535 bytes; added the extended-length prefix
for payloads up to 2^32-1 bytes, verified byte-for-byte against the
nostr-tools reference implementation (conversation keys, padding at every
bucket boundary, round trips in both directions).
- <bitlogin-auth> now exposes nip44Encrypt/nip44Decrypt directly on the
element instance, alongside the existing getPublicKey/signEvent, so a host
page can reach every capability without racing other providers for the
single window.nostr slot.
docs/spec.md bumped to v0.4 with the full writeup (§16.2, §29.7). 75/75
tests passing (14 new NIP-44 tests, 2 new rollback e2e tests).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
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