Files
bitlogin/packages/demo/public/docs.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

162 lines
8.9 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Integrate BitLogin — docs</title>
<meta name="description" content="How to add the BitLogin login widget to any static site in a few minutes." />
<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 href="./index.html">Overview</a>
<a class="active" href="./docs.html">Integrate</a>
<a href="./account.html">Try it</a>
</div>
</nav>
<header class="hero" style="padding-top:48px">
<h1 style="font-size:38px">Add BitLogin to your site</h1>
<p class="lead">No npm install required. Two files, one custom element.</p>
</header>
<section class="container narrow">
<ol class="step-list">
<li>
<h3>Copy the widget files next to your site</h3>
<p>Grab <code>bitlogin.js</code> and <code>cryptoWorker.js</code> (plus the shared chunk they both import) from a BitLogin release (or build <code>@bitlogin/widget</code> yourself) and drop them in a folder your site serves, e.g. <code>/vendor/bitlogin/</code>. All of them must live in the same directory — the main script loads the worker by a relative URL.</p>
</li>
<li>
<h3>Include the script and the element</h3>
<pre><code>&lt;script type="module" src="/vendor/bitlogin/bitlogin.js"&gt;&lt;/script&gt;
&lt;bitlogin-auth
vault-relays="wss://relay-one.example,wss://relay-two.example,wss://relay-three.example"
discovery-relays="wss://discovery-one.example,wss://discovery-two.example"&gt;
&lt;/bitlogin-auth&gt;</code></pre>
<p class="section-sub" style="margin:10px 0 0">That's it — the element renders its own sign-in/create/recover UI inside a shadow root, so it won't collide with your site's CSS.</p>
</li>
<li>
<h3>React to sign-in and sign-out</h3>
<pre><code>const bl = document.querySelector('bitlogin-auth');
bl.addEventListener('bitlogin-login', (e) => {
console.log('signed in as', e.detail.publicKey);
});
bl.addEventListener('bitlogin-logout', () => {
console.log('signed out');
});</code></pre>
</li>
<li>
<h3>Sign events from anywhere on the page</h3>
<p>Once a user is signed in, BitLogin installs a <code>window.nostr</code> provider shaped like the de facto NIP-07 interface used by browser extensions (Alby, nos2x, …). Any existing Nostr web app code that already calls <code>window.nostr</code> works unmodified.</p>
<pre><code>const pubkey = await window.nostr.getPublicKey();
const signed = await window.nostr.signEvent({
kind: 1,
content: 'hello from my app',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
// NIP-44 encrypted DMs
const cipher = await window.nostr.nip44.encrypt(peerPubkeyHex, 'hi there');
const plain = await window.nostr.nip44.decrypt(peerPubkeyHex, cipher);</code></pre>
<div class="callout">
<strong>Private keys never leave a dedicated Web Worker.</strong> The main thread — your page's JS — only ever
sees public keys, signed events, and ciphertext. <code>window.nostr</code> calls are proxied into the worker
and back.
</div>
</li>
<li>
<h3>Supporting multiple signing methods (extensions, NIP-46, BitLogin)</h3>
<p><code>window.nostr</code> is a single global slot — only one provider can occupy it at a time. If a user also
has a NIP-07 extension (Alby, nos2x, …) installed, BitLogin will not overwrite it: it only installs
<code>window.nostr</code> when nothing is already there. The reverse isn't in BitLogin's control — an extension
that loads after BitLogin can still overwrite it, same as any two NIP-07 providers would collide.</p>
<p>To know specifically whether BitLogin is the active signer (as opposed to an extension), check
<code>window.bitlogin?.isActiveSigner()</code> rather than just <code>typeof window.nostr !== 'undefined'</code> —
the latter only tells you <em>a</em> signer is present, not which one. <code>window.bitlogin</code> is installed
as soon as the script loads, independent of whether <code>&lt;bitlogin-auth&gt;</code> has signed anyone in yet,
so it's safe to feature-detect immediately.</p>
<pre><code>if (window.bitlogin) {
console.log('BitLogin is available, version', window.bitlogin.version);
}
if (window.bitlogin?.isActiveSigner()) {
// window.nostr is currently backed by BitLogin specifically
}</code></pre>
<p>NIP-46 (remote "bunker" signers) is a different transport — it doesn't typically hook
<code>window.nostr</code> directly, so it isn't something BitLogin needs to avoid colliding with. A site can
offer "Sign in with BitLogin," "Connect an extension," and "Connect a bunker" as separate options; whichever
the user picks becomes the active signer, and your app code should call whatever's currently active rather
than assume a specific one.</p>
<div class="callout">
<strong>Don't give your <code>&lt;bitlogin-auth&gt;</code> element <code>id="bitlogin"</code>.</strong>
Browsers automatically expose any element with a matching <code>id</code> or <code>name</code> attribute as
a same-named global — an element with <code>id="bitlogin"</code> becomes <code>window.bitlogin</code>,
shadowing the API object described above. Use something like <code>id="bitlogin-widget"</code> instead
(as this demo does).
</div>
<h4 style="margin-top:20px">Letting a user switch away from BitLogin without reloading</h4>
<p>By default, once <code>&lt;bitlogin-auth&gt;</code> mounts it claims <code>window.nostr</code> and keeps it —
even after the user logs out of BitLogin, since they might just be about to log back in with the same widget.
If your site offers a signer picker and the user switches to a different method, call
<code>window.bitlogin.releaseSigner()</code> to hand the slot back (it's a no-op, safe to call speculatively,
if BitLogin doesn't currently hold it):</p>
<pre><code>// User picked a different signing method in your own UI
if (window.bitlogin?.releaseSigner()) {
// window.nostr is now undefined; install the other method's provider
}</code></pre>
<p>If they switch back to BitLogin later, call <code>claimSigner()</code> on the element instance to reclaim
the slot (this also happens automatically every time a user completes sign-in, sign-up, or recovery through
the widget itself, so you only need this for a picker UI that lives outside the widget):</p>
<pre><code>document.querySelector('bitlogin-auth').claimSigner();</code></pre>
<p>Both <code>claimSigner()</code>/<code>releaseSigner()</code> on the element and
<code>window.bitlogin.releaseSigner()</code> also fire <code>bitlogin-signer-claimed</code> /
<code>bitlogin-signer-released</code> events (on the element and on <code>window</code>, respectively) if you'd
rather react to the change than check a return value.</p>
</li>
<li>
<h3>Choose your relays</h3>
<p>BitLogin ships with a small built-in bootstrap relay list, but you should point <code>vault-relays</code> and
<code>discovery-relays</code> at relays you trust for your users. Use at least three vault relays for
redundancy — capsule publication and reads use a quorum, so no single relay is a single point of failure.</p>
</li>
<li>
<h3>That's the whole integration</h3>
<p>No account database, no password-reset email, no server-side session. The <a href="./account.html">account manager demo</a>
shows the full loop: create an account, sign a test event, rotate the password, and export a recovery file.</p>
</li>
</ol>
<div class="callout">
<strong>Password-manager save prompts.</strong> Because the widget's login form lives inside
a shadow root and submits via JavaScript (never a real form POST), browsers' passive
"offer to save this password" heuristics don't reliably fire — and the generated-password
screen never puts the password into a real input at all, since it's only shown as text.
The widget works around this by explicitly calling the Credential Management API
(<code>navigator.credentials.store()</code>) after every successful registration, login,
password rotation, and recovery. This is Chromium-only (Chrome, Edge, Brave, Opera) —
Firefox and Safari never implemented <code>PasswordCredential</code>, so users on those
browsers will need to save their generated password manually.
</div>
</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>