mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-28 00:51:23 +00:00
dependabot/github_actions/actions/setup-node-7.1.0
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
86d7ee0114 |
widget: make an unmounted-widget teardown loud, not a silent hang
Removing <bitlogin-auth> from the DOM terminates its crypto worker. A host that re-renders surrounding markup (framework diff, htmx swap, innerHTML reset) can do this incidentally, after which signing calls fail -- and the failure was hard to diagnose. Two guardrails: - disconnectedCallback logs a console.error when the element is removed while a session (BitLogin account or NIP-07/46) is active, naming the unmount as the cause and pointing at the README "Integration gotcha". - WorkerClient.terminate(reason) records the reason; both in-flight calls (via failAll) and any later call (via the dead-flag path) reject with it, so an unmounted widget produces one legible error message instead of a bare or mismatched one. The dead flag already prevented the infinite hang; this makes the cause visible and unifies the wording. Adds worker/workerClient.test.ts (fake Worker) covering both rejection paths. Updates the README gotcha paragraphs from "hangs forever with no error" to the new loud-rejection behavior. Addresses the "unmount guardrail" item of the Tessera integration feedback (docs/TODO.md). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB |
||
|
|
c9791b9840 |
widget: lightweight, widget-free session reader
Reading "who is signed in" on a page required mounting the whole widget
(bitlogin.js + crypto worker + wasm, ~1.6MB) because that was the only
thing that restored the per-origin session. Multi-page sites paid the
full load on every page just to render a "Signed in as …" header.
Write a cleartext, public-only record ({ pubkey, npub, name? } — never
key material) beside the encrypted session in IndexedDB, in lockstep with
it: the worker writes it at the single persistSession() choke point,
self-heals it on restoreSession (so sessions cached before this feature
upgrade with no re-login), and clears it on logout, so a reported session
can never outlive the real one.
Add @bitlogin/widget/session -> bitlogin-session.js: a self-contained
~1KB gzip reader (zero imports, no worker/wasm) exposing
readCachedSession(): Promise<{ pubkey, npub, name? } | null>. Mirror the
same call on window.bitlogin.readCachedSession() for pages that already
load the full widget.
The private key stays in the AES-GCM record in sessionCache.ts, exactly
as protected as before. Reader falls through to null for every failure
mode (DB/store absent, IndexedDB blocked), which reads as "not signed in".
Addresses the top item of the Tessera integration feedback (docs/TODO.md).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
|
||
|
|
25df5140b3 |
Audit round 3: live-sub budgets, revocation-before-courtesy, pending-key catch, CI semgrep, input bounds
BL-25 (medium): live subscriptions get the same adversarial-relay posture buffered queries got in round 2 -- a bounded already-seen id cache (2048 ids, FIFO eviction) so replaying one retained valid event costs one verification and ONE delivery ever, plus a fixed-window verification budget (100 per 10s) that quarantines distinct-event spray for the rest of the window. The audit's 2,000-replay probe now produces one callback, bounded by test. BL-26 (medium): teardown order inverted. retireNip46Client pre-signs the courtesy logout event while the key exists, then close() runs IMMEDIATELY -- rejecting every in-flight request and wiping keys -- and only then is the pre-signed event delivered, asynchronously and bounded (publishCourtesy, safe after close). The audit's probe -- a pending get_public_key resolving after logout began -- is now impossible and regression-tested: in-flight requests reject within the same tick of close(). BL-27 (low): a nostrconnect listen that fails naturally (timeout, unreachable relays) discards the pending ephemeral key in a guarded catch -- ownership-transfer on success unchanged. BL-28 (low): the hosted verify job installs the pinned Semgrep 1.171.0 before security:ci, which round 2 taught to require it; the job was red at the previous HEAD and skipping its SBOM/checksum artifacts. Informational: bunker URI input bounds -- 4096 chars total, 256-char secret (rejected beyond), 512-char relay URLs (filtered beyond). 8 new/updated adversarial tests; 266 total, all green. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
bb9788588f |
Audit round 2: resource ceilings, key lifecycle, local gate, courtesy logout
BL-21 (medium): untrusted relays and crafted bunker URIs can no longer buy CPU, memory, or sockets. Frames over 256K chars are dropped before JSON.parse; buffered queries run every cheap check (id shape, replay dedup, capacity) BEFORE Schnorr verification and carry a per-query verification budget (3x limit, floor 50) so distinct forged events exhaust the budget rather than the CPU; bunker URIs are deduplicated and capped at 8 relays. The audit's exact probe -- thousands of replayed copies against limit 1 -- now costs one verification, bounded by test. Documented trade: a relay that sprays forgeries past the budget forfeits the query. BL-22 (low): pending nostrconnect attempts now have a real lifecycle. Superseding or abandoning an attempt aborts its 180s listener (new AbortSignal support) and wipes its ephemeral key; on success the key's ownership transfers into the adopted client, which wipes on close. Per-candidate conversation keys in the listen loop are wiped in a finally instead of left to GC. BL-23 (low): the named local gate no longer skips static analysis -- security:ci runs the custom Semgrep rules via security:semgrep (same config and pin as the hosted workflow). BL-24 (informational): the courtesy protocol logout defined by current NIP-46 is sent on every teardown path (disconnect, supersession), bounded at 2s and never allowed to block closing; TODO's outdated 'no stable method name' note is corrected. switch_relays remains the one deferred interop item. 6 new adversarial tests; 262 total, all green; semgrep gate exit 0. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
2e7b1e19e3 |
Welcome-screen polish: alternative methods behind a collapsed options menu
Import, recovery, extension, and remote signer no longer stack as four links under the primary buttons -- they live behind a 'More sign-in options' disclosure that expands into an icon+description option menu. Rows keep a 56px minimum touch target, the toggle carries aria-expanded, chevron rotation respects prefers-reduced-motion, and buttons get a focus-visible outline. The disclosure state survives re-renders so an error doesn't snap the menu shut. Contextual renderings stay put: the login screen keeps 'Forgot password?' and the bunker screen its retry. Also cap the nostrconnect URI at two relays: five relay params pushed the QR several versions denser, and tiny modules hurt both the dot aesthetic and camera lock-on. Verified in the browser: collapsed and expanded welcome, and the sparser QR. Policy pins updated to the new structure: every alternative action must render inside the option-menu template, with exact occurrence counts. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
49682dd75c |
NIP-46 sign-in: remote-signer sessions behind the same facade, memory-only
Implements login-methods.md §LM5 phase one. Core gains nostr/nip46.ts -- a Nip46Client speaking kind-24133 RPC with NIP-44 transport encryption, bunker:// parsing, nostrconnect listen (secret echo is proof-of-scan), auth_url surfacing, and typed deadline errors -- plus subscribeLive on RelayConnection with re-REQ on reconnect, and live post-EOSE delivery in the MockRelay so tests exercise real round trips against a fake bunker. The ephemeral client key and Nip46Client live in the crypto worker (§LM5.4); nip46* actions bypass the serial queue so a signer waiting minutes for human approval cannot block other worker calls, and the signer's auth_url reaches the UI as the worker's first notification frame. The widget's connect screen runs both legs at once: a nostrconnect QR -- rendered as dot-style inline SVG with rounded finder eyes, per owner preference -- for phone signers, and a bunker:// paste box (password input; the URI carries a token). window.nostr is now an element-routed provider, so a NIP-46 session claims the slot and hosts reading it get the remote signer transparently; NIP-07 still leaves the slot to the extension. Sessions stay thin per §LM7: no vault, no persistence. Persistence remains gated on the external vault review (§LM5.3). 14 new tests (core fake-bunker e2e, QR geometry, signer delegation) and two policy pins: bunker option below the password path, element stays web-storage-free. Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
7d2209d706 |
Second-audit hardening: phrase-gated sudo key, deterministic migration, policy tests
Authored by ChatGPT/Codex in a separate review pass. Reviewed and verified here before committing rather than taken on trust; findings below. The substantive change is that the vault sudo key now lives ONLY in the phrase-gated recovery capsule. A newly written password credential capsule carries just connection_vault_root, so an ordinary login restores connectable-tier records without putting personal-tier material behind a password capsule that is downloadable and therefore offline-guessable forever. Pre-hardening capsules stay readable, password login does not return their sudo key, and the next rotation scrubs it. That closes the gate this project had explicitly set on personal-tier UI. Legacy vault-enable migration now derives roots deterministically from phrase-protected material and account_id, so two concurrent attempts cannot mint divergent roots -- the failure mode the docs previously mitigated with "enable the vault once, from one device". Manual (user-chosen) passwords are REMOVED, not merely discouraged, and a new securityPolicy.test.ts pins that along with several things tests do not usually reach: the deployment CSP (default-src 'none', frame-ancestors 'none') plus six baseline headers, demo pages free of inline scripts and remote fonts, and README documenting same-origin self-hosting only rather than a remote demo embed. llms.txt was updated to match, which I checked because that test only reads README. What I verified, since a passing suite proves less than it appears to: - All 12 fixes from the 2026-07-30 audit survived the rewrite of recover.ts, changePassword.ts, validation.ts, relay.ts, session.ts and element.ts. Checked semantically where grep cannot help: changePassword still checks publish success BEFORE building the tombstone, and vaultSetBinding still unbinds on origin === null rather than silently rebinding to the worker global. - No dependency changes; the package.json diff is scripts only. - Argon2id and KDF parameters unchanged. The NIP-44 diff ADDS a base64 length cap before decoding, a memory-DoS guard. - Validation increased rather than decreased: 44 throws added, 33 removed. - No disabled or skipped tests, no secrets in the diff. typecheck clean, 165/165 tests, build green, and the account manager exercised in a browser: creation correctly refused when relay quorum could not be met, and on retry failed at the recovery capsule with "No login credential was published" -- the §18.1 fail-closed ordering doing its job against a flaky public relay set. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1 |
||
|
|
efd994c4de |
Fix revoke-rebinds bug; correct the website's custody and revoke claims
The audit's most important finding was not a doc issue at all. In vaultSetBinding the identifier `origin` resolved to the WORKER GLOBAL self.origin rather than the intended value, so the dashboard's "Revoke app access" REBOUND the record to the calling page instead of clearing it -- the precise cross-origin adoption the binding guard exists to prevent, wearing the label of the button meant to prevent it. It typechecked because the DOM lib declares that global. I introduced this hours ago: a string replace intended for one call site silently matched two. Fixed, with the reasoning written where the next reader will hit it. Relay/account audit fixes: - Phrase recovery permanently tripped the rollback alarm. The mark is keyed by the everyday pubkey (unchanged by recovery) while recovery legitimately restarts at generation 0, and raiseHighWaterMark keeps the maximum -- so anyone who had ever rotated a password met "possibly revoked credentials" on EVERY later login, forever, with no way to clear it. The phrase is the master authority; it now resets the mark. - quorumMet counted TCP connections, not answers: a query timeout RESOLVED, so a connectable-but-silent relay counted as responding and every "not enough relays answered" guard was near-vacuous. Only EOSE is an answer now. - Relay responses were never checked against the address requested, so one junk event from one relay blocked registration, rotation, and recovery for every login name and password a user tried. Events must now match author, kind, and d-tag. Website honesty (the surface a stranger reads first, and the one that had drifted furthest from the spec): - docs.html and llms.txt still carried "private keys never leave the worker" -- false on its face, since the widget ships a reveal-nsec button. Both now state the same limit the README does. - "you revoke" became honest everywhere: removing a connection deletes the stored copy; spending authority is revoked at the wallet. - index.html dropped a pre-softening recovery claim, now says an account lives only while relays retain its capsule, and says "independently unaudited" out loud. - llms.txt gains the two warnings that were missing entirely: losing BOTH password and phrase is permanent, and removal is not revocation. - Stale headers: nwc-connections.md no longer claims "Not implemented", docs/README no longer says the authorization layer is unbuilt, the test count and spec version are current, and the demo domain points at bitlogin.network. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1 |
||
|
|
d040c2f5af |
Tier 1 hardening: no more silent hangs, no writes before consent
Reliability, which is where most of these reach users: - The worker RPC had no message validation, no error/messageerror listener, no timeout, and terminate() left every pending promise unsettled. A malformed frame threw inside the listener (posting no response at all) and a dead worker stranded its callers -- the documented innerHTML-remount hazard manifested as a permanent freeze rather than an error. Both sides now validate the envelope, calls time out at 60s, and worker death rejects everything outstanding. - requestNwcConnection fired no pending event, so a host using the RECOMMENDED hidden-mount pattern navigated a hidden element to a consent screen nobody could see and waited forever. Now symmetric with bitlogin-offer-pending. Consent integrity: - vaultOfferCheck published an origin rebinding to relays -- a durable mutation of the user's account -- from a plain host API call before any consent screen existed. It is read-only now, and a wallet stored under a DIFFERENT origin no longer reports as already-saved, so the binding can only change through the consented path. - offerNwcConnection's guard sat above three awaits; two concurrent calls both passed it and the second dropped the first's promise. The slot is claimed synchronously. Vault reads fail closed: - listConnections threw rather than returning a relay-shaped subset when quorum is not met -- a short listing is indistinguishable from "you have no connections", and one relay should not get to say that. - The 500-record page limit is reported (truncated) instead of silently deciding which connections exist. Supply chain: - All eight crypto dependencies pinned to exact versions (they held users' keys behind caret ranges), and Bitcoin Connect with them. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1 |
||
|
|
b414cc53d2 |
Security audit: fix 8 findings across crypto, vault, and widget
An adversarial audit (five independent reviewers over crypto/protocol, the vault, the widget, relay flows, and doc honesty) found real bugs in code this repo shipped. Fixed here, worst first: CRITICAL - the widget worker ignored application_binding.origin on vaultRevealNwc, so any caller naming a connection id could pull a wallet credential the user granted to a DIFFERENT site. The worker now derives its own origin (self.location) instead of trusting a payload field -- which also closes binding forgery via vaultSaveNwc / vaultSetBinding / vaultOfferCheck, where a host could bind another site's wallet to itself and inherit its one-tap approval. CRITICAL - VaultSession.destroy() left the vault root live (the root regenerates every derived key) and, worse, left the session USABLE with a zeroed prk: deriveRecordKey/deriveVaultSigningKey over 32 zero bytes are deterministic and world-computable, so a stale reference could seal a wallet secret under a key any observer can derive and report success. Now wipes the root and fails loudly. CRITICAL - changePassword published the tombstone that DESTROYS the old capsule before checking that the new one reached quorum: a failed rotation could leave neither password working, phrase-only recovery. Reordered to fail closed with the old password intact, plus a regression test that drives it with a partially-hostile relay set. HIGH - enableConnectionVault never checked quorum on the recovery read, so one stale relay could make it mint a SECOND vault root and orphan every stored credential -- the outcome its own header calls unforgivable. HIGH - the rollback high-water mark used strict `<` and never compared the event id it stored, so a same-second sibling of a revocation could resurrect a revoked credential forever. Ties now break on event id. HIGH - §11.10 claimed derived secrets are wiped; wipe() had two callers and neither was on the password or recovery path. The Argon2id output, both PRKs, and the 64-byte BIP-39 seed now die at their derivation site. MEDIUM - one malformed record threw out of the listing loop and made every OTHER connection unlistable; recovery lacked the already-registered-at-this-locator guard that registration and rotation both carry; the recovery chain check silently passed on gaps and forks. Synthetic (untrusted) clicks and submits no longer drive consent UI. Docs corrected where they overstated: the README's "private keys never leave the worker" now says plainly that a same-origin host can reach exportIdentity, and connection-vault.md gains §19 recording six OPEN limitations -- no CAS on capsule writes, per-device-only rollback state, tombstones not destroying prior ciphertext, the sudo key shipping with the root, fetches not failing closed, and a plaintext mark store. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1 |
||
|
|
6b48de6168 |
Widget: offerNwcConnection() -- consent-gated offer-to-save
The inverse of requestNwcConnection: an app that obtained an NWC URI by its own means (its own wallet chooser, its own paste box) offers the user a portable copy. The credential is already in the app's hands, so the only question on screen is whether a copy enters the user's vault -- and that write happens in BitLogin's own chrome or not at all, never silently, because a page that can pump records into someone's account without a tap is a spam vector wearing a feature's clothes. Dedupe by connection identity (wallet pubkey + secret; relays and labels may drift without making a new connection -- sameNwcCredential in core): a duplicate resolves 'already-saved' with a silent origin-binding refresh and no UI, which is what makes the call safe to fire after every successful connect. 'unavailable' (no session, no vault root) is a quiet no-op by contract. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1 |
||
|
|
c3e55add16 |
Widget: requestNwcConnection() -- vault authorization layer, reveal mode
The Phase D surface from vault-ux.md, scoped to NWC and honest about its
boundary: an embedded same-origin widget cannot broker (§CV12.3), so the
API reveals, says so in its consent copy, and leans on the wallet-side
budget as the real security boundary.
One public call owns the whole ceremony. If nobody is signed in, the
sign-in flow runs first and the request resumes via the goto("dashboard")
hook exactly once; if the account has a connection bound to the
requesting origin, the user gets the one-tap approval screen the vault
exists for; otherwise a guided import -- Bitcoin Connect's wallet
chooser or a pasted URI. The BC integration lifts Satisfied's
production discipline verbatim: chooser only, lazy chunks (never
downloaded unless the flow opens), no numeric budget in
authorizationUrlOptions (the unit is not interoperable), disconnect and
clear bc:config so no plaintext copy outlives the ceremony.
The worker holds the vault root (never the sudo key -- wiped on
arrival) and gains vaultStatus/List/SaveNwc/FindForOrigin/RevealNwc/
SetBinding/Delete actions; the root rides the encrypted session cache
so restored sessions keep working, with a vaultEnabled marker so a
stale cache says "sign in again" instead of falsely claiming the
account has no vault. Publishing failures are loud -- a connection that
reached no relay is not saved anywhere and says so. The dashboard gains
Wallet connections management with "revoke app access" and "remove
from BitLogin" honestly separated from wallet-side revocation.
VaultSession gains buildDeletionRequest so the NIP-09 courtesy ask is
signed without exposing the signing key.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
|
||
|
|
e9a5a41ff7 | fix(widget): encrypt persistent session cache | ||
|
|
9e3787d301 |
Add local session persistence so a page reload doesn't require re-login
Every reload previously destroyed the crypto worker, forcing the full login-name + password flow again -- re-running Argon2id and a relay quorum read, both deliberately expensive (the whole point of Argon2id is to resist offline brute force), on every single page navigation. No real site can expect users to accept that. After a successful register/login/completeRecovery/changePassword, cache the everyday private key plus the active credential/recovery capsule events in IndexedDB (persistSession() in cryptoWorker.ts). connectedCallback() now calls a new "restoreSession" worker action right after "configure", before rendering the welcome screen; if a valid cache exists, the widget lands straight on the dashboard and fires "bitlogin-login" exactly as a fresh login would, with no password prompt. Logout clears the cache. This trades "nothing persists locally" for "the everyday private key sits decrypted in this browser's IndexedDB until logout" -- the same tradeoff every NIP-07 extension and crypto wallet already makes for the same reason, and scoped by IndexedDB's own per-origin isolation (one site's cached session is never visible to another, same boundary window.nostr already relies on). The recovery phrase and login name/password are never cached. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX |
||
|
|
283840261f |
Add NIP-04 support for drop-in NIP-07 parity
signEvent() already handles any event kind and NIP-44 was already implemented, but window.nostr.nip04 (and the equivalent element-scoped nip04Encrypt/nip04Decrypt) didn't exist anywhere -- every real NIP-07 extension exposes it, and sites with legacy DM code paths still expect it. A site whose own code called window.nostr.nip04.encrypt(...) while BitLogin was the active signer got a bare "Cannot read properties of undefined" instead of a working call. - packages/core/src/crypto/nip04.ts: AES-256-CBC + secp256k1 ECDH shared secret (raw X coordinate, no HKDF -- this is legacy NIP-04, not NIP-44), "<ciphertext>?iv=<iv>" payload shape, cross-checked against the NIP-04 spec's algorithm description - NostrSigner.nip04Encrypt/nip04Decrypt, mirroring the existing nip44Encrypt/nip44Decrypt methods - crypto worker: new nip04Encrypt/nip04Decrypt actions and protocol payload types - <bitlogin-auth>.nip04Encrypt/nip04Decrypt (element-scoped, like the existing nip44 methods) and window.nostr.nip04.encrypt/decrypt Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX |
||
|
|
8c01a121df |
Never overwrite an existing profile or relay list on import
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.
|
||
|
|
1dc5e6802c |
Fail closed on credential-generation rollback; fix NIP-44 extended-length payloads
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]> |
||
|
|
a24719184e |
Add signer switching, manual-password parity, phrase-quiz split, and offline recovery import
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 |
||
|
|
0801371566 |
Fix live-deployment bugs, add manual passwords, and improve integration support
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
|
||
|
|
a76c8484f7 |
Add nsec import: use an existing Nostr identity as the everyday key (§SF10)
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 |
||
|
|
5ee63ae6b0 |
Implement BitLogin as a modular protocol library, embeddable widget, and demo site
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 |