mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-27 08:31:21 +00:00
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]>
298 lines
29 KiB
HTML
298 lines
29 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">
|
|
<svg class="lockup" xmlns="http://www.w3.org/2000/svg" viewBox="0 -74.8 511.6 103.3" role="img" aria-label="BitLogin"> <defs> <linearGradient id="bl-lockup-seal" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#8368ff" /> <stop offset="1" stop-color="#6a4de8" /> </linearGradient> <linearGradient id="bl-lockup-brass" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#e6c481" /> <stop offset="1" stop-color="#b3924a" /> </linearGradient> </defs> <g transform="translate(0 -61.88) scale(1.3496)"> <circle cx="32" cy="32" r="21" fill="none" stroke="url(#bl-lockup-brass)" stroke-width="2.5" /> <circle cx="32" cy="32" r="14.5" fill="url(#bl-lockup-seal)" /> <path d="M27 32.5l3.6 3.6L38 28.2" fill="none" stroke="url(#bl-lockup-brass)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" /> </g> <g transform="translate(92.50 0)"> <path d="M68.55 -16.85Q68.55 -9.4 62.13 -4.7Q55.7 0 42.85 0L8.2 0Q6.65 0 5.93 -0.62Q5.2 -1.25 5.2 -2.3Q5.2 -4.2 7.3 -4.85L10.15 -5.5Q11.4 -5.85 12.03 -6.55Q12.65 -7.25 12.65 -8.35L12.65 -61.65Q12.65 -62.75 12.03 -63.45Q11.4 -64.15 10.15 -64.5L7.3 -65.15Q5.2 -65.8 5.2 -67.7Q5.2 -68.8 5.93 -69.4Q6.65 -70 8.2 -70L35.2 -70Q44.5 -70 51.08 -67.45Q57.65 -64.9 61.1 -60.37Q64.55 -55.85 64.55 -49.8Q64.55 -44.75 61.73 -40.8Q58.9 -36.85 53.35 -34.58Q47.8 -32.3 39.65 -32.3L42.45 -33.85Q50.4 -33.85 56.25 -31.67Q62.1 -29.5 65.33 -25.67Q68.55 -21.85 68.55 -16.85ZM37.15 -30.45L23.8 -30.45L23.8 -34.5L35.9 -34.5Q40.35 -34.5 43.45 -36.18Q46.55 -37.85 48.15 -41.1Q49.75 -44.35 49.75 -49Q49.75 -53.95 47.78 -57.62Q45.8 -61.3 42 -63.35Q38.2 -65.4 32.7 -65.4L28.15 -65.4L28.15 -9.45Q28.15 -7 29.68 -5.8Q31.2 -4.6 34.15 -4.6L39.45 -4.6Q43.9 -4.6 46.93 -6.15Q49.95 -7.7 51.5 -10.57Q53.05 -13.45 53.05 -17.25Q53.05 -23.25 48.98 -26.85Q44.9 -30.45 37.15 -30.45ZM94.5 -45.1L94.5 -8Q94.5 -6.45 94.93 -5.72Q95.35 -5 96.25 -4.7L98.15 -4.2Q99.1 -3.9 99.55 -3.37Q100 -2.85 100 -2.05Q100 -1.1 99.35 -0.55Q98.7 0 97.25 0L77.55 0Q76.15 0 75.5 -0.55Q74.85 -1.1 74.85 -2.05Q74.85 -2.8 75.3 -3.32Q75.75 -3.85 76.7 -4.2L78.65 -4.7Q79.55 -5.05 79.98 -5.75Q80.4 -6.45 80.4 -8L80.4 -36.65Q80.4 -37.95 80.03 -38.5Q79.65 -39.05 78.8 -39.2L76.15 -39.4Q75.25 -39.6 74.85 -40.05Q74.45 -40.5 74.45 -41.2Q74.45 -42.05 74.93 -42.55Q75.4 -43.05 76.75 -43.55L86.05 -46.95Q88 -47.65 89.18 -47.95Q90.35 -48.25 91.15 -48.25Q92.85 -48.25 93.68 -47.37Q94.5 -46.5 94.5 -45.1ZM86.35 -55.9Q82.35 -55.9 79.93 -57.97Q77.5 -60.05 77.5 -63.4Q77.5 -66.75 79.93 -68.78Q82.35 -70.8 86.35 -70.8Q90.4 -70.8 92.83 -68.78Q95.25 -66.75 95.25 -63.4Q95.25 -60.05 92.83 -57.97Q90.4 -55.9 86.35 -55.9ZM108.9 -42.45L106.6 -43.15Q105.4 -43.55 104.9 -44.07Q104.4 -44.6 104.4 -45.3Q104.4 -46.3 105.08 -46.82Q105.75 -47.35 106.9 -47.35L109.95 -47.35Q111.15 -47.35 111.95 -47.8Q112.75 -48.25 113.6 -49.4L119.05 -57.05Q120.1 -58.35 121.05 -58.95Q122 -59.55 122.95 -59.55Q124 -59.55 124.6 -58.9Q125.2 -58.25 125.2 -57L125.2 -14.25Q125.2 -10.75 126.58 -8.9Q127.95 -7.05 130.4 -7.05Q132.25 -7.05 133.28 -7.77Q134.3 -8.5 134.9 -9.45Q135.5 -10.4 136.08 -11.12Q136.65 -11.85 137.55 -11.9Q138.3 -11.9 138.73 -11.4Q139.15 -10.9 139.15 -9.75Q139.1 -6.9 137.28 -4.45Q135.45 -2 132.2 -0.47Q128.95 1.05 124.8 1.05Q118.4 1.05 114.78 -2.18Q111.15 -5.4 111.15 -11.9L111.15 -39.5Q111.15 -40.8 110.63 -41.42Q110.1 -42.05 108.9 -42.45ZM119.65 -42.4L119.65 -47.35L136.2 -47.35Q137.35 -47.35 138.03 -46.87Q138.7 -46.4 138.7 -45.5Q138.7 -44.2 137.4 -43.3Q136.1 -42.4 133.2 -42.4ZM174.95 -65.15L172.1 -64.5Q170.9 -64.15 170.25 -63.45Q169.6 -62.75 169.6 -61.65L169.6 -8.55Q169.6 -6.65 170.7 -5.82Q171.8 -5 174.05 -5L182.35 -5Q185.2 -5 187.2 -5.77Q189.2 -6.55 190.8 -8.5Q192.4 -10.45 194.05 -14.05L197.05 -20.95Q197.65 -22.2 198.53 -22.62Q199.4 -23.05 200.55 -22.75Q201.75 -22.45 202.25 -21.62Q202.75 -20.8 202.35 -19.4L197.85 -1.8Q197.3 0.05 196.48 0.95Q195.65 1.85 194.05 1.85Q193 1.85 191.98 1.4Q190.95 0.95 189.73 0.48Q188.5 0 186.65 0L149.7 0Q148.15 0 147.43 -0.62Q146.7 -1.25 146.7 -2.3Q146.7 -4.2 148.8 -4.85L151.65 -5.5Q152.85 -5.85 153.5 -6.55Q154.15 -7.25 154.15 -8.35L154.15 -61.65Q154.15 -62.75 153.5 -63.45Q152.85 -64.15 151.65 -64.5L148.8 -65.15Q146.7 -65.8 146.7 -67.7Q146.7 -68.8 147.43 -69.4Q148.15 -70 149.7 -70L174.05 -70Q175.6 -70 176.33 -69.4Q177.05 -68.8 177.05 -67.7Q177.05 -65.8 174.95 -65.15ZM234.65 -48.5Q242.4 -48.5 248.25 -45.35Q254.1 -42.2 257.38 -36.58Q260.65 -30.95 260.65 -23.5Q260.65 -16.45 257.27 -10.85Q253.9 -5.25 247.95 -2Q242 1.25 234.15 1.25Q226.45 1.25 220.63 -1.95Q214.8 -5.15 211.5 -10.77Q208.2 -16.4 208.2 -23.75Q208.2 -30.9 211.58 -36.47Q214.95 -42.05 220.9 -45.28Q226.85 -48.5 234.65 -48.5ZM238.2 -4Q241.6 -4.5 243.68 -7.12Q245.75 -9.75 246.33 -14.35Q246.9 -18.95 245.7 -25.4Q244.55 -31.85 242.35 -36.02Q240.15 -40.2 237.15 -42.05Q234.15 -43.9 230.65 -43.3Q227.2 -42.75 225.13 -40.12Q223.05 -37.5 222.53 -32.93Q222 -28.35 223.15 -21.85Q224.3 -15.45 226.5 -11.27Q228.7 -7.1 231.7 -5.25Q234.7 -3.4 238.2 -4ZM299.9 -4.6Q292.95 -5.6 289.83 -6.37Q286.7 -7.15 285.9 -7.85Q285.1 -8.55 285.1 -9.3Q285.1 -10.1 285.75 -10.8Q286.4 -11.5 287.9 -12.15L286.85 -12.95Q281.9 -12.45 279.08 -11.12Q276.25 -9.8 275.08 -7.95Q273.9 -6.1 273.9 -4Q273.9 -1.6 275.33 0.1Q276.75 1.8 280.65 3.13Q284.55 4.45 292.05 5.55Q298.15 6.45 301.55 7.48Q304.95 8.5 306.3 9.8Q307.65 11.1 307.65 12.85Q307.65 15.05 306.25 16.63Q304.85 18.2 301.8 19.03Q298.75 19.85 293.7 19.85Q284.9 19.85 281.35 17.23Q277.8 14.6 277.8 10.3Q277.8 7.95 279.77 6.08Q281.75 4.2 285.5 3.6L284.7 2.1Q274.75 3.3 270.9 6.35Q267.05 9.4 267.05 13.35Q267.05 16.55 269.35 19.08Q271.65 21.6 276.97 23.05Q282.3 24.5 291.35 24.5Q305.3 24.5 312.68 20.13Q320.05 15.75 320.05 8.9Q320.05 5.15 318.13 2.5Q316.2 -0.15 311.77 -1.87Q307.35 -3.6 299.9 -4.6ZM301.05 -43.3L304.3 -42.85Q306.15 -46.5 307.35 -47.75Q308.55 -49 310.15 -49Q311.6 -49 312.43 -48.28Q313.25 -47.55 313.9 -46.6Q314.55 -45.65 315.45 -44.92Q316.35 -44.2 317.95 -44.2Q320.25 -44.2 321.6 -45.87Q322.95 -47.55 322.95 -50.3Q322.95 -53.4 320.95 -55.17Q318.95 -56.95 315.8 -56.95Q311.95 -56.95 308.52 -54.42Q305.1 -51.9 302.75 -46.8ZM315.9 -30.45Q315.9 -35.5 313.13 -39.58Q310.35 -43.65 305.23 -46Q300.1 -48.35 293 -48.35Q285.7 -48.35 280.22 -45.9Q274.75 -43.45 271.72 -39.08Q268.7 -34.7 268.7 -28.9Q268.7 -23.85 271.47 -19.78Q274.25 -15.7 279.4 -13.35Q284.55 -11 291.65 -11Q298.95 -11 304.43 -13.45Q309.9 -15.9 312.9 -20.28Q315.9 -24.65 315.9 -30.45ZM290.5 -44.1Q294.8 -44.4 297.55 -40.7Q300.3 -37 301.1 -30Q301.9 -23.15 300.08 -19.45Q298.25 -15.75 294.1 -15.45Q291.25 -15.3 289.05 -16.87Q286.85 -18.45 285.45 -21.67Q284.05 -24.9 283.5 -29.55Q283 -34.15 283.63 -37.3Q284.25 -40.45 286 -42.17Q287.75 -43.9 290.5 -44.1ZM345.55 -45.1L345.55 -8Q345.55 -6.45 345.98 -5.72Q346.4 -5 347.3 -4.7L349.2 -4.2Q350.15 -3.9 350.6 -3.37Q351.05 -2.85 351.05 -2.05Q351.05 -1.1 350.4 -0.55Q349.75 0 348.3 0L328.6 0Q327.2 0 326.55 -0.55Q325.9 -1.1 325.9 -2.05Q325.9 -2.8 326.35 -3.32Q326.8 -3.85 327.75 -4.2L329.7 -4.7Q330.6 -5.05 331.03 -5.75Q331.45 -6.45 331.45 -8L331.45 -36.65Q331.45 -37.95 331.08 -38.5Q330.7 -39.05 329.85 -39.2L327.2 -39.4Q326.3 -39.6 325.9 -40.05Q325.5 -40.5 325.5 -41.2Q325.5 -42.05 325.97 -42.55Q326.45 -43.05 327.8 -43.55L337.1 -46.95Q339.05 -47.65 340.23 -47.95Q341.4 -48.25 342.2 -48.25Q343.9 -48.25 344.73 -47.37Q345.55 -46.5 345.55 -45.1ZM337.4 -55.9Q333.4 -55.9 330.98 -57.97Q328.55 -60.05 328.55 -63.4Q328.55 -66.75 330.98 -68.78Q333.4 -70.8 337.4 -70.8Q341.45 -70.8 343.88 -68.78Q346.3 -66.75 346.3 -63.4Q346.3 -60.05 343.88 -57.97Q341.45 -55.9 337.4 -55.9ZM377.2 -45.1L377.2 -8Q377.2 -6.45 377.65 -5.75Q378.1 -5.05 379 -4.7L380.8 -4.2Q382.4 -3.55 382.4 -2.2Q382.4 0 379.6 0L360.3 0Q358.9 0 358.25 -0.55Q357.6 -1.1 357.6 -2.05Q357.6 -2.8 358.03 -3.32Q358.45 -3.85 359.4 -4.2L361.4 -4.7Q362.3 -5.05 362.73 -5.75Q363.15 -6.45 363.15 -8L363.15 -36.65Q363.15 -37.95 362.78 -38.5Q362.4 -39.05 361.55 -39.2L358.9 -39.4Q358 -39.6 357.6 -40.05Q357.2 -40.5 357.2 -41.2Q357.2 -42.05 357.67 -42.55Q358.15 -43.05 359.5 -43.55L368.8 -46.95Q370.7 -47.65 371.85 -47.95Q373 -48.25 374.05 -48.25Q375.6 -48.25 376.4 -47.37Q377.2 -46.5 377.2 -45.1ZM375.7 -33.95L373.4 -36.3L375.35 -38.05Q381.7 -43.85 386.42 -46.17Q391.15 -48.5 395.3 -48.5Q401.75 -48.5 405.67 -44.4Q409.6 -40.3 409.6 -33.4L409.6 -8.15Q409.6 -6.5 410.08 -5.75Q410.55 -5 411.5 -4.7L413.25 -4.2Q414.25 -3.85 414.67 -3.32Q415.1 -2.8 415.1 -2.05Q415.1 -1.1 414.45 -0.55Q413.8 0 412.4 0L393.05 0Q390.25 0 390.25 -2.2Q390.25 -3.55 391.8 -4.2L393.7 -4.7Q394.7 -5.05 395.13 -5.8Q395.55 -6.55 395.55 -8.15L395.55 -31.35Q395.55 -35.75 393.4 -37.93Q391.25 -40.1 387.65 -40.1Q385.4 -40.1 382.88 -39.05Q380.35 -38 377.7 -35.7Z" fill="currentColor" /> </g> </svg>
|
|
</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; padding-bottom:8px">
|
|
<span class="eyebrow">Integration guide</span>
|
|
<h1 style="font-size:clamp(32px, 4.5vw, 44px)">Add BitLogin to your site</h1>
|
|
<p class="lead" style="max-width:520px">No npm install required. Two files, one custom element — and every user who already has a BitLogin account elsewhere can sign in with the login they already know.</p>
|
|
</header>
|
|
|
|
<section class="container narrow">
|
|
<ol class="step-list">
|
|
<li>
|
|
<h3>Self-host the widget on your site's origin</h3>
|
|
<p>Build <code>@bitlogin/widget</code> from the reviewed source and copy <code>bitlogin.js</code>, <code>cryptoWorker.js</code>, and every <code>bitlogin-shared-*.js</code> chunk into a folder served by the same origin as your app, e.g. <code>/vendor/bitlogin/</code>. Keep every file together because the entrypoint resolves its worker and chunks by relative URL. Do not load the demo deployment directly from another origin.</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>
|
|
<div class="callout">
|
|
<strong>Mount it somewhere your own re-renders won't touch.</strong> Removing
|
|
<code><bitlogin-auth></code> from the DOM (including indirectly, by resetting an
|
|
ancestor's <code>innerHTML</code> — a common pattern in hand-rolled UI without a
|
|
framework's reconciler) tears down its crypto Web Worker. If that happens mid-session,
|
|
every signing call made afterward just hangs forever with no error, because the worker
|
|
is gone and nothing ever replies — a call already in flight when it happens can even
|
|
look like it succeeded. If your app re-renders the container this element lives in,
|
|
give it a container that's never replaced wholesale and toggle the <code>hidden</code>
|
|
attribute instead of removing the element. See the account manager demo's source for a
|
|
single-page example, or BitRoad's <code>src/nostr/bitloginAdapter.mjs</code> /
|
|
<code>#bitloginMount</code> (a sibling Nostr commerce app that embeds BitLogin) for a
|
|
worked example of keeping the element alive across a host app's own re-renders.
|
|
</div>
|
|
</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);
|
|
console.log('via', e.detail.method); // 'bitlogin' | 'nip07' | 'nip46'
|
|
console.log('supports', e.detail.capabilities); // { nip44, nip04, getRelays }
|
|
});
|
|
|
|
bl.addEventListener('bitlogin-logout', () => {
|
|
console.log('signed out');
|
|
});</code></pre>
|
|
<div class="callout">
|
|
<strong>One login surface, several methods.</strong> If the visitor's browser has a NIP-07 signer
|
|
extension (Alby, nos2x, …), the widget's welcome screen offers "Use your Nostr extension instead"
|
|
below the primary username/password sign-in, and every visitor gets "Use a remote signer (bunker)" —
|
|
a NIP-46 connect screen with a scannable QR for phone signers (Amber, nsec.app) and a
|
|
<code>bunker://</code> paste box. You embed one element and all three kinds of users are covered. <code>detail.method</code> tells you which way the user came in, and
|
|
<code>detail.capabilities</code> what the active signer supports (some extensions lack
|
|
<code>nip44</code>). Extension sessions are signer-only: signing and encryption work through the
|
|
element exactly the same, but BitLogin account features — wallet connections, password rotation,
|
|
identity export, cross-device persistence — belong to BitLogin accounts, and
|
|
<code>requestNwcConnection()</code> resolves <code>null</code>. A page reload ends an extension
|
|
session; the extension is simply asked again next visit.
|
|
</div>
|
|
</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 live in a dedicated Web Worker.</strong> The signing APIs return only public keys, signed events, and ciphertext — the main thread never receives a private key through them. Be precise about the limit, though: an embedded widget runs <em>same-origin</em> with the page hosting it, so a malicious or XSS-compromised host can reach the worker directly (including <code>exportIdentity</code>, which backs the widget's own "reveal private key" button) and can visually overlay the consent UI. Treat the host page as trusted and embed BitLogin only in sites you control.
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<h3>Or talk to the element directly — no <code>window.nostr</code> contention</h3>
|
|
<p><code><bitlogin-auth></code> mirrors the same four calls as instance methods, scoped to that specific
|
|
element rather than the single shared <code>window.nostr</code> slot:</p>
|
|
<pre><code>const bl = document.querySelector('bitlogin-auth');
|
|
|
|
const pubkey = await bl.getPublicKey();
|
|
const signed = await bl.signEvent({ kind: 1, content: 'hello', tags: [] });
|
|
const cipher = await bl.nip44Encrypt(peerPubkeyHex, 'hi there');
|
|
const plain = await bl.nip44Decrypt(peerPubkeyHex, cipher);</code></pre>
|
|
<p>Prefer this if your app already holds a reference to the element — e.g. you built the sign-in UI yourself
|
|
instead of relying on <code>window.nostr</code>-reading app code. It keeps working even if something else
|
|
currently owns <code>window.nostr</code> (another extension, a different widget instance), and it's what a host
|
|
app should use if it wants a specific, known signer rather than "whichever provider is active this moment."
|
|
BitRoad's <code>src/nostr/bitloginAdapter.mjs</code> is a real example: its signer adapter calls these four
|
|
methods directly and never touches <code>window.nostr</code> at all.</p>
|
|
</li>
|
|
<li>
|
|
<h3>Ask for a wallet connection (NWC, Connection Vault)</h3>
|
|
<p>If your app takes Lightning payments, stop asking users to paste an NWC string on every device.
|
|
One call asks BitLogin for a wallet connection bound to your origin:</p>
|
|
<pre><code>const uri = await bl.requestNwcConnection({
|
|
appName: 'Satisfied',
|
|
reason: 'Pay for meal analysis',
|
|
});
|
|
if (uri) payWith(uri); // full nostr+walletconnect:// URI, or null if declined</code></pre>
|
|
<p>The widget owns the whole ceremony: sign-in first if nobody is signed in, a <em>one-tap approval</em>
|
|
when the account already has a connection bound to your origin (the connection follows the user to
|
|
every new device), or a guided first-time import — Bitcoin Connect's wallet chooser, or a pasted URI —
|
|
stored as an encrypted record on the user's account. A <code>bitlogin-connection-granted</code> event
|
|
fires on the element after each share, and users manage or revoke grants from the widget dashboard's
|
|
"Wallet connections" screen.</p>
|
|
<p>The inverse direction exists too: if your app obtained a connection by its own means (its own
|
|
wallet chooser, its own paste box), offer the user a portable copy —</p>
|
|
<pre><code>const outcome = await bl.offerNwcConnection(uri, { appName: 'Satisfied' });
|
|
// 'saved' | 'declined' | 'already-saved' | 'unavailable'</code></pre>
|
|
<p>The widget shows its own consent screen ("Save this wallet to your BitLogin?") and never saves
|
|
silently — the write goes to the user's account, so it happens in BitLogin's chrome or not at all.
|
|
Duplicates (same wallet + secret) resolve <code>already-saved</code> with no UI, so it's safe to
|
|
fire after every successful connect. <code>unavailable</code> means nobody is signed in or the
|
|
account has no vault; treat it as a no-op.</p>
|
|
<div class="callout">
|
|
<strong>This is reveal mode, and the consent screen says so.</strong> Your origin receives the full
|
|
bearer credential and everything its wallet-side budget allows — an embedded same-origin widget is
|
|
not a security boundary against its host page, so BitLogin doesn't pretend to broker. Encourage
|
|
budget-capped connections: the budget the user's wallet enforces is the real security boundary.
|
|
Two practical requirements: the wallet-connect flow needs <code>style-src-attr 'unsafe-inline'</code>
|
|
in your CSP (Bitcoin Connect's components set inline style attributes; hashes can't cover those), and
|
|
accounts created before the Connection Vault existed must enable it once from the account manager
|
|
(recovery phrase required) before connections can be saved — the flow still hands your app the URI
|
|
either way, just labeled as unsaved.
|
|
</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>One exception first: when a user signs in <em>through</em> a NIP-07 extension from the widget's own
|
|
welcome screen, BitLogin deliberately leaves <code>window.nostr</code> alone — the extension already owns
|
|
the slot and is the session's actual backend, so there is nothing to claim or release. Everything below
|
|
concerns BitLogin-account sessions, where the widget's own provider competes for the slot.</p>
|
|
<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>
|
|
<div class="callout">
|
|
<strong><code>claimSigner()</code> can fail without breaking anything.</strong> Some NIP-07 extensions
|
|
install <code>window.nostr</code> as a non-configurable, non-writable property specifically to stop another
|
|
script from overwriting it. When that happens, <code>claimSigner()</code> returns <code>false</code> instead
|
|
of throwing (and <code>bitlogin-signer-claimed</code>'s <code>event.detail</code> carries
|
|
<code>{ windowNostrClaimed: false, error }</code>), and every BitLogin sign-in flow completes normally
|
|
regardless — the failure only means <code>window.nostr</code> still points at that other extension. If your
|
|
app talks to the element directly (previous step) this never matters at all; only code that reads
|
|
<code>window.nostr</code> itself is affected, and only for as long as the other extension holds the slot.
|
|
</div>
|
|
</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>Make it look like your site</h3>
|
|
<p>The widget renders in a shadow root, but every color, radius, font, and its max-width are all read from CSS
|
|
custom properties on <code>:host</code> — and custom properties inherit through the shadow boundary, so your
|
|
page's own CSS can override every one of them without touching the widget's internals:</p>
|
|
<pre><code>bitlogin-auth {
|
|
--bl-accent: #3d9bff; /* primary button / link / focus ring color */
|
|
--bl-accent-hover: #2f86e0; /* primary button hover */
|
|
--bl-accent-fg: #04101f; /* text color on top of --bl-accent */
|
|
--bl-bg: #12151f; /* card background */
|
|
--bl-fg: #eaeef6; /* body text */
|
|
--bl-muted: #98a2b6; /* secondary text */
|
|
--bl-border: rgba(255, 255, 255, 0.10);
|
|
--bl-input-bg: #1a1e2b; /* input / credential-box background */
|
|
--bl-danger: #ff6b6b;
|
|
--bl-danger-bg: rgba(255, 107, 107, 0.16);
|
|
--bl-radius: 14px;
|
|
--bl-font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
--bl-max-width: none; /* default 380px keeps it a fixed-width card;
|
|
override to fill a wider container */
|
|
}</code></pre>
|
|
<p class="section-sub" style="margin:10px 0 0">Overriding none of these is a valid choice — the widget falls
|
|
back to its own light theme, a <code>prefers-color-scheme: dark</code> media query, and a
|
|
<code>data-theme="dark"|"light"</code> attribute override for pages with their own theme toggle. See BitRoad's
|
|
<code>bitlogin-auth { ... }</code> rule in its <code>src/styles.css</code> for a real integration that maps
|
|
every one of these onto its own design tokens.</p>
|
|
</li>
|
|
<li>
|
|
<h3>Showing it as one option among several</h3>
|
|
<p>If your sign-in screen lists BitLogin alongside other methods (an extension, a bunker, email) rather than
|
|
embedding the full widget up front, use the brand mark instead — the same seal shown on this site and inside
|
|
the widget's own header, as a plain SVG with a transparent background:</p>
|
|
<pre><code><button>
|
|
<img src="/assets/mark.svg" alt="" width="20" height="20" />
|
|
Sign in with BitLogin
|
|
</button></code></pre>
|
|
<p class="section-sub" style="margin:10px 0 0">Grab <code>mark.svg</code> (icon only), <code>wordmark.svg</code>
|
|
(the "BitLogin" logotype as vector paths — no font to load), or <code>lockup.svg</code> (both combined) from
|
|
this site's own <code>assets/</code> folder. All three are plain SVG with no external dependencies, and
|
|
<code>wordmark.svg</code>'s text is filled with <code>currentColor</code> so it follows your page's own text
|
|
color.</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.
|
|
<div class="footer-links"><a href="https://github.com/PR0M3TH3AN/bitlogin">Source on GitHub</a> · MIT licensed — <a href="https://github.com/PR0M3TH3AN/bitlogin/blob/main/LICENSE">LICENSE</a></div>
|
|
</footer>
|
|
|
|
|
|
<script type="module" src="./assets/site.js"></script>
|
|
</body>
|
|
</html>
|