Files
bitlogin/packages/demo/public/index.html
T
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

116 lines
5.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BitLogin — a portable login for the open web</title>
<meta name="description" content="A familiar login name and password that unlocks a portable Nostr identity, stored as encrypted capsules across public relays. No account server required." />
<link rel="icon" href="./assets/icon.svg" type="image/svg+xml" />
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="stylesheet" href="./assets/site.css" />
</head>
<body>
<nav class="nav">
<a class="brand" href="./index.html"><span class="dot"></span>BitLogin</a>
<div class="links">
<a class="active" href="./index.html">Overview</a>
<a href="./docs.html">Integrate</a>
<a href="./account.html">Try it</a>
</div>
</nav>
<header class="hero">
<h1>A login your users<br />actually take with them.</h1>
<p class="lead">
Login name and password on the outside. A portable Nostr identity underneath —
encrypted and stored on public relays, with no account server, no password-reset
email, and no vendor lock-in.
</p>
<div class="cta-row">
<a class="btn primary" href="./account.html">Try the demo →</a>
<a class="btn secondary" href="./docs.html">Add it to your site</a>
</div>
<div class="diagram">
<div class="step"><div class="k">Everyday login</div>Login name + password</div>
<div class="arrow">→</div>
<div class="step"><div class="k">Locally derived</div>Password-locked locator key</div>
<div class="arrow">→</div>
<div class="step"><div class="k">Public relays</div>Encrypted capsule lookup</div>
<div class="arrow">→</div>
<div class="step"><div class="k">Result</div>Portable Nostr identity</div>
</div>
</header>
<section class="container">
<h2>Two independent ways in, one identity</h2>
<p class="section-sub">Forgetting a password and losing a device are different emergencies. BitLogin keeps them cryptographically separate.</p>
<div class="grid">
<div class="feature">
<h3>🔑 Password path</h3>
<p>Login name + password deterministically derive a private "locator" key and an encryption key. That key finds and decrypts a small credential capsule on public relays — no lookup table, no server, no email.</p>
</div>
<div class="feature">
<h3>🌱 Recovery phrase</h3>
<p>A 12-word phrase derives a completely independent recovery identity. A cracked password can never sign, replace, or delete the recovery capsule — it has no authority over that path at all.</p>
</div>
<div class="feature">
<h3>🪪 One everyday identity</h3>
<p>Both paths unlock the exact same everyday Nostr keypair — your real, canonical, portable identity. Change your password as often as you like; the identity never moves.</p>
</div>
</div>
</section>
<section class="container">
<h2>Why this is different</h2>
<p class="section-sub">Every design choice traces back to one constraint: it has to work with nothing but static files and public relays.</p>
<div class="grid">
<div class="feature">
<h3>Static-first</h3>
<p>The whole client — crypto, UI, worker — ships as files. Host it anywhere: Pages, IPFS, a folder on a CDN. No backend to run or pay for.</p>
</div>
<div class="feature">
<h3>Relay-redundant storage</h3>
<p>Encrypted capsules are published to multiple Nostr relays with quorum reads and readback verification, so no single relay is a single point of failure.</p>
</div>
<div class="feature">
<h3>Generated by default, never weak</h3>
<p>A downloadable encrypted capsule can be attacked offline forever, so BitLogin generates a high-entropy passphrase for you by default. Prefer your own? It's allowed, but it has to clear a real entropy floor and a common-password check — no "password123", ever.</p>
</div>
<div class="feature">
<h3>Argon2id + AES-256-GCM</h3>
<p>Memory-hard key derivation, authenticated encryption, canonical JSON serialization, and fixed padding buckets so ciphertext size doesn't leak contents.</p>
</div>
<div class="feature">
<h3>Rollback-resistant</h3>
<p>A local generation high-water mark and monotonic timestamps mean a stale or malicious relay can't quietly roll a logged-in device back to an older capsule.</p>
</div>
<div class="feature">
<h3>NIP-07-compatible signing</h3>
<p>Once unlocked, BitLogin exposes a <code>window.nostr</code> provider — the same shape browser extensions use. Existing Nostr apps work with zero code changes.</p>
</div>
</div>
</section>
<section class="container narrow">
<h2>Drop it into any static page</h2>
<p class="section-sub">One script tag, one custom element. No build step, no framework required.</p>
<pre><code>&lt;script type="module" src="/vendor/bitlogin/bitlogin.js"&gt;&lt;/script&gt;
&lt;bitlogin-auth vault-relays="wss://relay.example,wss://relay2.example"&gt;&lt;/bitlogin-auth&gt;</code></pre>
<p><a href="./docs.html">Read the full integration guide →</a></p>
</section>
<footer>
BitLogin is an experimental alpha protocol. This demo is a reference implementation, not a production account service.
</footer>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./sw.js").catch(() => {});
}
</script>
</body>
</html>