mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-02 03:21:22 +00:00
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
162 lines
8.9 KiB
HTML
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><script type="module" src="/vendor/bitlogin/bitlogin.js"></script>
|
|
<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">
|
|
</bitlogin-auth></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><bitlogin-auth></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><bitlogin-auth></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><bitlogin-auth></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>
|