97 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Fable 5 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
2026-07-30 13:10:39 -04:00
thePR0M3TH3ANandClaude Fable 5 a465e1d8fa Implement the Connection Vault protocol layer (phases A-C)
connection-vault.md 5-13 as code, with the implementation-time decisions
recorded in a new section 18 and vault-ux.md holding the agreed UX
walkthroughs as the authorization layer's acceptance criteria.

Capsules gain paired optional fields connection_vault_root and
vault_sudo_key (both or neither, validated); registration mints them,
login/change-password/recovery carry them forward, and
enableConnectionVault is the phrase-gated migration for existing
accounts -- recovery capsule written first, fail closed, idempotent by
reusing roots found in either capsule so a partial failure can never
mint twice and strand records.

src/vault/ holds the finalized key hierarchy (HKDF/ScalarExpand
constants pinned by test vectors that must only ever be reverted, never
updated), per-connection encrypted records signed by a derived vault
identity with the capsule JCS+padding+AES-GCM pipeline and shared
buckets so record types are indistinguishable by size, the NWC profile
with lossless URI parse/export, relay sync reusing the publish-and-
verify quorum bar, and per-record high-water marks so a stale replica
is withheld and reported rather than silently resurrecting a revoked
credential.

The two-tier model makes sudo mode cryptographically real: personal-
tier record keys require vault_sudo_key as HKDF input, VaultSession
holds it only inside an enableSudo/endSudo window (wiping on the way in
and out), and outside a window personal records are invisible -- the
same trial-decrypt returns null, not an error. The app-facing API is
structurally blind to the tier by construction.

118 tests passing, including four end-to-end scenarios against mock
relays: clean-device restore, password-change carry, legacy-account
migration (wrong phrase refused), and rollback detection.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 12:29:46 -04:00
thePR0M3TH3AN e9a5a41ff7 fix(widget): encrypt persistent session cache 2026-07-25 23:28:41 -04:00
thePR0M3TH3AN fe9a7afde2 Add BitLogin documentation index 2026-07-22 20:56:59 -04:00
thePR0M3TH3AN 82539c2057 Document relay and messaging portability 2026-07-22 20:56:45 -04:00
thePR0M3TH3AN f656f014bb Document experimental portable entitlements 2026-07-22 20:56:07 -04:00
thePR0M3TH3AN 66c97820db Document experimental portable application state 2026-07-22 20:55:24 -04:00
thePR0M3TH3AN 6f0bc2a056 Document experimental multi-account switching 2026-07-22 20:54:46 -04:00
thePR0M3TH3AN 0c9011e064 Document experimental S3C connection string format 2026-07-22 17:41:33 -04:00
thePR0M3TH3AN 6000cfd06f Document experimental NWC Connection Vault profile 2026-07-22 17:40:33 -04:00
thePR0M3TH3AN 98a8c60821 Document experimental BitLogin Connection Vault 2026-07-22 17:39:45 -04:00
thePR0M3TH3ANandClaude Sonnet 5 681cec7734 Document session persistence in README and CHANGELOG
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 07:47:49 -04:00
thePR0M3TH3ANandClaude Sonnet 5 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
2026-07-22 07:47:07 -04:00
thePR0M3TH3ANandClaude Sonnet 5 92a87ff13e CHANGELOG: document the NIP-04 addition
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 23:29:10 -04:00
thePR0M3TH3ANandClaude Sonnet 5 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
2026-07-21 23:28:51 -04:00
thePR0M3TH3ANandClaude Sonnet 5 66cae69c7e README: document the wasm-unsafe-eval CSP requirement and the DOM-move gotcha
Both discovered via a live bitunlock integration bug this session:
BitUnlock's script-src lacked 'wasm-unsafe-eval' (silently broke every
password-hashing action), and its attempt to relocate the static
#bitloginMount via replaceWith() (to work around the innerHTML gotcha)
turned out to trigger the exact disconnectedCallback problem it was
trying to avoid -- moving a custom element still tears down and
recreates it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 22:46:53 -04:00
thePR0M3TH3ANandClaude Sonnet 5 e3270012e9 CHANGELOG: document the style-src CSP fix
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 22:09:45 -04:00
thePR0M3TH3ANandClaude Sonnet 5 e8712f9aa9 Fix widget styles being silently dropped under strict style-src CSP
element.ts re-injected the widget's CSS as a fresh inline <style> tag
on every render() call. A host with a strict style-src (e.g.
`style-src 'self'`, no 'unsafe-inline') silently drops an inline
<style>'s rules entirely -- every element inside the shadow root fell
back to unstyled browser defaults. The visible symptom: the brand SVG
rendered at its raw intrinsic ~590x119px instead of the intended
20px-tall lockup, since its `display: block; height: 20px` rule never
took effect.

Build the stylesheet once as a CSSStyleSheet and assign it via
adoptedStyleSheets instead -- not subject to style-src the way a
parsed inline <style> element is, since it's never parsed as untrusted
markup by the HTML parser. render() no longer touches styling at all.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 22:09:30 -04:00
thePR0M3TH3ANandClaude Sonnet 5 863f035990 Add CHANGELOG.md and an "Updating your integration" README section
Vendoring sites had no documented way to learn a fix landed here or how
to pull it in. Adds a changelog (integrator-relevant entries only, each
noting whether it applies to a site's config) and a README section
explaining why vendoring is manual by design -- a shared auto-updating
script would make a bad deploy here an instant multi-site incident for
a widget that handles private key crypto.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 21:31:45 -04:00
thePR0M3TH3ANandClaude Sonnet 5 681f622ef8 Expand built-in vault/discovery relay lists for redundancy
relay.nostr.band (shared by both lists) is currently unreachable, and
publishAndVerify() enforces a fixed minAcks/minReadbacks=2 floor
regardless of relay count -- so a 3-relay vault list had zero margin
the moment one relay went down, surfacing as "Registration did not
reach the required relay acknowledgement and readback quorum."

Add nostr.wine and relay.snort.social to the vault list, and
nostr-pub.wellorder.net to discovery, so losing any single relay no
longer risks failing registration.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 21:28:38 -04:00
thePR0M3TH3AN a4e9b1d2ae Fix .link buttons collapsing onto one line at wide --bl-max-width
button.link overrode width back to auto (from the general button
rule's width: 100%) but never set display: block, so it stayed at the
<button> default of inline-block. At narrow widths (the default
380px card) two adjacent .link buttons happened to wrap onto separate
lines simply because they didn't fit side by side, masking the bug.
At --bl-max-width: none in a wide host container (found integrating
BitLogin into BitUnlock's /connect page), "Import an existing Nostr
key" and "Forgot password? Recover with phrase" rendered running into
each other on the same line.
2026-07-21 19:17:32 -04:00
thePR0M3TH3AN b2f4b96b84 Simplify hero credential card example
Drop the "river ·" login-name prefix in front of the example
passphrase -- it read as an unexplained, unlabeled extra word rather
than a distinct login name, and the separator dot didn't make the
distinction clear either. The card already says "One credential";
showing just the passphrase matches that label with nothing left to
be confused about.
2026-07-21 18:48:54 -04:00
thePR0M3TH3AN ed8d8eb1be Stamp genuine successes with the brand's brass ring + checkmark
Signing in, creating an account, recovering one, and rotating a
password all used to just silently swap to the dashboard screen --
nothing marked the moment as a success versus any other navigation.
These are exactly the security-relevant "you're safely in/done"
moments, so mark them with the same brass ring + checkmark mark used
in the logo: a brief overlay stamps the destination screen with the
mark and a short label ("Signed in", "Account created", "Account
recovered", "Password updated"), then fades on its own.

Deliberately narrow: this only fires on those four real completions,
not on every validation step or plain navigation (e.g. the "Back to
dashboard" button), so it stays a rare, meaningful beat instead of
becoming visual noise. It's also inert by design -- pointer-events:
none means it can never block interacting with the screen underneath,
a JS timer (not just the CSS animation) guarantees it's removed even
if the animation can't run, and prefers-reduced-motion drops the
motion but keeps the same brief hold-then-clear timing.
2026-07-21 18:44:58 -04:00
thePR0M3TH3AN f725e2cd83 Drop the "B" monogram, use one true logo lockup everywhere
The monogram read as a Bitcoin symbol, which BitLogin has nothing to
do with -- revert the icon/mark back to the brass ring + checkmark
(now brass/gold to match the ring, not white) with no letterform.

Also stop composing the icon and wordmark as two independently-sized
elements in each place they appear (a --mark height here, a
--wordmark height there) -- that let their relative size and spacing
drift apart between the site nav and the widget's header. Bake both
into a single SVG (assets/lockup.svg) with the layout fixed once, and
use that same file's markup everywhere a caller only ever sets one
overall height. Site nav and the widget's welcome screen now render
from the exact same source.

Also fills in the hero credential card's placeholder passphrase
("a few good words") with a realistic 6-word example matching what
the generator actually produces, and centers the last two cards of
the 6-card "Why this is different" grid under the middle two columns
instead of left-aligning them under columns 1-2.
2026-07-21 18:35:38 -04:00
thePR0M3TH3AN 2b272eb093 Give BitLogin a real vector logo, consistent across the site and widget
Replace the placeholder gradient-circle favicon and the widget's plain
"BitLogin" text heading with an actual brand: a monogram seal (a "B"
drawn from Fraunces, the same serif already used for headings, set in
a violet seal with a brass ring) plus a vector wordmark traced from
the live Fraunces glyphs.

Both are baked as static SVG path data (via fontkit) rather than
depending on the webfont at render time, so the widget can show the
exact same wordmark the site uses without loading any font of its
own -- solving the "vector logo instead of a typeface" problem for a
component meant to drop into arbitrary host pages.

- assets/icon.svg: updated favicon/app-icon with the new monogram
- assets/mark.svg (new): transparent-background version for inline
  use in a host site's own "Sign in with BitLogin" button
- assets/wordmark.svg (new): text-only logotype, fill=currentColor
- assets/lockup.svg (new): mark + wordmark combined, for anyone
  wanting a single-file logo
- All three site pages' nav now use the same inlined mark + wordmark
  instead of the ad-hoc checkmark-circle SVG that was duplicated
  across them
- <bitlogin-auth>'s welcome screen shows the same lockup instead of
  plain text, themed via --bl-fg like everything else in the widget
- docs.html gains a short "showing it as one option among several"
  step pointing integrators at mark.svg for a compact button icon
2026-07-21 17:54:21 -04:00
thePR0M3TH3AN 01fc7c5ed3 Refuse to overwrite an existing account on register or password change
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).
2026-07-21 16:54:32 -04:00
thePR0M3TH3AN 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.
2026-07-21 16:39:00 -04:00
thePR0M3TH3AN 207f4cba10 Redesign demo site around a "one login, every site" pitch
The old landing page led with generic Nostr-signer framing and a
templated violet/pink gradient look. Reframe the pitch around the
thing that's actually differentiated: one login name and password
that works on every BitLogin site, explicitly positioned against
NIP-07 extensions and NIP-46 bunkers, with Nostr terminology kept
light for the non-technical end users who'll actually be typing a
password rather than integrating a widget.

Visual redesign: warm parchment/brass accent alongside the existing
violet, Fraunces display serif paired with the existing system-ui
body font, and a new "one credential, many doors" hero illustration
with a staggered stamp-in animation (prefers-reduced-motion aware)
replacing the generic feature-grid-only layout. Applied consistently
across index.html, docs.html, and account.html.
2026-07-21 16:17:37 -04:00
thePR0M3TH3ANandClaude Sonnet 5 ce6ade73b4 docs: bring the Integrate page and version references up to date
docs.html was missing everything from this session's work:
- The disconnectedCallback/crypto-worker-teardown gotcha (found via the
  BitRoad integration) -- now a callout right where the element is first
  mounted, since that's the step where it actually bites.
- Talking to the element directly (getPublicKey/signEvent/nip44Encrypt/
  nip44Decrypt as instance methods) as an alternative to window.nostr,
  with BitRoad's adapter as the worked example.
- claimSigner() failing gracefully (returns false, doesn't throw) when a
  NIP-07 extension has locked window.nostr down -- was previously
  undocumented entirely; this is the exact bug just fixed.
- Theming via CSS custom properties, with the full property list and
  BitRoad's mapping as a real example.

Also fixed two stale "(v0.3)" references to docs/spec.md in README.md and
CONTRIBUTING.md -- the spec was bumped to v0.4 earlier this session for the
rollback-detection changes but these two mentions were missed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 15:53:17 -04:00
thePR0M3TH3ANandClaude Sonnet 5 1dbb4fca51 Fail closed (not open-and-crash) when window.nostr can't be claimed
Some NIP-07 extensions install window.nostr as a non-configurable,
non-writable property to stop another script from overwriting it.
claimSigner() did a plain, unguarded `window.nostr = ...` assignment, so on
a page where such an extension is active, this threw
"Cannot assign to read only property 'nostr' of object '#<Window>'" --
and because it ran synchronously before dispatching bitlogin-login or
moving to the dashboard screen, it froze account creation/import at the
final "verify your recovery phrase" step, with that raw TypeError as the
only feedback shown. Reported from a real BitRoad integration session.

claimSigner() and releaseSigner() now catch this and return false instead
of throwing; connectedCallback's initial claim attempt is guarded the same
way. Every caller (registration, login, recovery, password change) now
completes sign-in regardless -- a host page using this element's own
methods directly (getPublicKey/signEvent/nip44Encrypt/nip44Decrypt) was
never actually affected by a failed window.nostr takeover, only a page that
reads window.nostr itself. The dashboard now shows a plain-language warning
when this happens, reusing the existing rollback/relay-disagreement warning
banner mechanism.

Also fixed while auditing this area: the "Copy" button on the generated-
password screen silently did nothing if the clipboard write failed (e.g.
Firefox's stricter permissions, an unfocused document) -- it now flashes
"Copied" or "Copy failed -- select the text manually" either way.

Verified live in Chrome: reproduced the exact crash by locking down
window.nostr the way a real extension does, confirmed account
creation/import now completes end-to-end (in both the standalone demo and
embedded in BitRoad) with the new warning shown instead of a stuck flow.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 15:44:52 -04:00
thePR0M3TH3ANandClaude Sonnet 5 1eff23aca8 Expose widget theming as CSS custom properties, and a max-width override
<bitlogin-auth> renders in a shadow root, but every color/radius/font value
already flowed through a CSS custom property on :host -- these inherit
through the shadow boundary, so a host page can already override the
widget's look from ordinary light-DOM CSS. This adds the two properties
that weren't yet exposed:

- --bl-accent-fg (default: white): text color on top of --bl-accent. Was
  hardcoded on button.primary, so a host with a light accent color and its
  own dark-on-accent convention had no way to match it.
- --bl-max-width (default: 380px): the widget stays a fixed-width card by
  default (unchanged for existing embedders), but a host page that wants it
  to fill a wider container can now override this instead of being stuck
  with the hardcoded cap.

Also routes the hardcoded font-family through --bl-font-family so that's
themeable too. Documented in the README with a worked example (BitRoad,
which maps every one of these onto its own design tokens to render the
widget in its own colors at full width).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 15:05:55 -04:00
thePR0M3TH3ANandClaude Sonnet 5 6640e7adf6 Link the GitHub repo and MIT license from the demo site footer
Every page's footer now links to the repo (github.com/PR0M3TH3AN/bitlogin)
and its LICENSE file, so visitors can see this is open source without
digging through docs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-21 14:39:26 -04:00
thePR0M3TH3ANandClaude Sonnet 5 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]>
2026-07-21 14:36:35 -04:00
thePR0M3TH3AN e17885d0e5 update 2026-07-21 13:03:12 -04:00
thePR0M3TH3AN b7f74041a5 Merge pull request #4 from PR0M3TH3AN/claude/bitlogin-protocol-spec-q18ust
Add MIT license, CONTRIBUTING, and SECURITY docs for open-sourcing
2026-07-21 13:00:42 -04:00
Claude 9408fef484 Add MIT license, CONTRIBUTING, and SECURITY docs for open-sourcing
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
2026-07-21 16:54:55 +00:00
thePR0M3TH3AN ca2dc09bf6 Merge pull request #3 from PR0M3TH3AN/claude/bitlogin-protocol-spec-q18ust
Add signer switching, manual-password parity, phrase-quiz split, and offline recovery import
2026-07-21 12:44:05 -04:00
Claude 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
2026-07-21 16:41:35 +00:00
thePR0M3TH3AN 4a8dfb8cde Merge pull request #2 from PR0M3TH3AN/claude/bitlogin-protocol-spec-q18ust
Fix live-deployment bugs, add manual passwords, and improve integration support
2026-07-21 11:58:34 -04:00
Claude 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
2026-07-21 15:35:10 +00:00
Claude 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
2026-07-21 10:59:01 +00:00
Claude 991c2ee639 Add draft spec addendum: second factor, backup codes, imported identities
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
2026-07-21 10:30:19 +00:00
thePR0M3TH3AN 3cde67574b Merge pull request #1 from PR0M3TH3AN/claude/bitlogin-protocol-spec-q18ust
Implement BitLogin: protocol core, embeddable widget, demo site, and deploy configs
2026-07-20 23:57:27 -04:00
Claude 21dd474781 Add GitHub Pages deployment workflow for the demo site
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
2026-07-21 03:53:52 +00:00
Claude 0991a5c68f Add vercel.json for zero-config deploys of the demo site
Builds core -> widget -> demo and serves packages/demo/dist as the output
directory, so importing the repo in Vercel needs no manual build settings.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
2026-07-21 03:46:56 +00:00
Claude 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
2026-07-21 03:44:10 +00:00
thePR0M3TH3AN c1adfc15bd Initial commit 2026-07-20 22:52:52 -04:00