import { r as w, e as k, E as Q, p as x, R as C, i as p, a as d } from "./bitlogin-shared-B2Rc9khL.js"; const S = 6, f = 64; function u(r = S) { const e = k(), t = []; for (let s = 0; s < r; s++) t.push(e[w(e.length)]); const a = r * Q; if (a < f) throw new Error(`Passphrase of ${r} words provides only ~${a.toFixed(1)} bits; must exceed ${f}.`); return { kind: "passphrase", secret: t.join(" "), entropyBits: a }; } const b = 64, m = 12, P = /* @__PURE__ */ new Set([ "password", "password1", "password123", "123456", "123456789", "12345678", "qwerty", "qwerty123", "letmein", "welcome", "monkey", "dragon", "master", "abc123", "iloveyou", "admin", "login", "starwars", "sunshine", "princess", "football", "baseball", "trustno1", "000000", "111111", "123123", "1234567890", "changeme", "passw0rd", "correcthorsebatterystaple" ]); function L(r) { let e = 0; return /[a-z]/u.test(r) && (e += 26), /[A-Z]/u.test(r) && (e += 26), /[0-9]/u.test(r) && (e += 10), /[^a-zA-Z0-9]/u.test(r) && (e += 33), e || 1; } function E(r) { return r.length * Math.log2(L(r)); } function $(r) { if (/^(.)\1*$/u.test(r) || /(.)\1{2,}/u.test(r)) return !0; const e = r.toLowerCase(), t = ["0123456789", "abcdefghijklmnopqrstuvwxyz"]; for (const a of t) for (let s = 0; s + 4 <= a.length; s++) { const n = a.slice(s, s + 4), o = [...n].reverse().join(""); if (e.includes(n) || e.includes(o)) return !0; } return !1; } function g(r, e) { const t = E(r); return r.length < m ? { ok: !1, entropyBits: t, reason: `Must be at least ${m} characters.` } : P.has(r.toLowerCase()) ? { ok: !1, entropyBits: t, reason: "This is on a list of extremely common passwords." } : e.length >= 3 && r.toLowerCase().includes(e) ? { ok: !1, entropyBits: t, reason: "Must not contain your login name." } : $(r) ? { ok: !1, entropyBits: t, reason: "Too predictable (repeated characters or a simple sequence)." } : t < b ? { ok: !1, entropyBits: t, reason: `Estimated entropy (~${t.toFixed(0)} bits) is below the required ${b}. Use a longer password or mix character types.` } : { ok: !0, entropyBits: t }; } const R = 6e4; class N { worker; pending = /* @__PURE__ */ new Map(); counter = 0; dead = !1; constructor() { const e = ["cryptoWorker", ".js"].join(""), t = new URL(e, import.meta.url); this.worker = new Worker(t, { type: "module" }), this.worker.addEventListener("message", (s) => { const n = s.data; if (!n || typeof n != "object" || typeof n.id != "string") return; const o = this.pending.get(n.id); if (o) if (this.pending.delete(n.id), clearTimeout(o.timer), n.ok) o.resolve(n.result); else { const l = new Error(n.error); l.name = n.errorName ?? "Error", o.reject(l); } }); const a = (s) => () => this.failAll(s); this.worker.addEventListener("error", a("The BitLogin crypto worker stopped unexpectedly.")), this.worker.addEventListener( "messageerror", a("The BitLogin crypto worker sent an unreadable message.") ); } /** Settles every outstanding call with an error. Idempotent. */ failAll(e) { const t = [...this.pending.values()]; this.pending.clear(); for (const a of t) clearTimeout(a.timer), a.reject(new Error(e)); } call(e, t) { if (this.dead) return Promise.reject(new Error("This BitLogin session was torn down; reload the page to sign in again.")); const a = `${Date.now().toString(36)}-${(this.counter++).toString(36)}`; return new Promise((s, n) => { const o = setTimeout(() => { this.pending.delete(a), n(new Error(`BitLogin's ${e} call timed out.`)); }, R); this.pending.set(a, { resolve: s, reject: n, timer: o }); const l = { id: a, action: e, payload: t }; this.worker.postMessage(l); }); } configure(e) { return this.call("configure", e); } register(e) { return this.call("register", e); } previewImportKey(e) { return this.call("previewImportKey", e); } login(e) { return this.call("login", e); } recover(e) { return this.call("recover", e); } completeRecovery(e) { return this.call("completeRecovery", e); } changePassword(e) { return this.call("changePassword", e); } publishProfileAndRelayLists(e) { return this.call("publishProfileAndRelayLists", e); } getPublicKey() { return this.call("getPublicKey", {}); } signEvent(e) { return this.call("signEvent", e); } nip44Encrypt(e) { return this.call("nip44Encrypt", e); } nip44Decrypt(e) { return this.call("nip44Decrypt", e); } nip04Encrypt(e) { return this.call("nip04Encrypt", e); } nip04Decrypt(e) { return this.call("nip04Decrypt", e); } exportIdentity() { return this.call("exportIdentity", {}); } buildRecoveryExport() { return this.call("buildRecoveryExport", {}); } repairReplicas() { return this.call("repairReplicas", {}); } getSessionStatus() { return this.call("getSessionStatus", {}); } restoreSession() { return this.call("restoreSession", {}); } vaultStatus() { return this.call("vaultStatus", {}); } vaultList() { return this.call("vaultList", {}); } vaultSaveNwc(e) { return this.call("vaultSaveNwc", e); } vaultFindForOrigin() { return this.call("vaultFindForOrigin", {}); } vaultRevealNwc(e) { return this.call("vaultRevealNwc", e); } vaultSetBinding(e) { return this.call("vaultSetBinding", e); } vaultDelete(e) { return this.call("vaultDelete", e); } vaultOfferCheck(e) { return this.call("vaultOfferCheck", e); } logout() { return this.call("logout", {}); } terminate() { this.dead = !0, this.worker.terminate(), this.failAll("The BitLogin crypto worker was terminated."); } } const M = "BitLogin: no identity is unlocked yet. Add to the page and let the user sign in, or call it programmatically before invoking window.nostr."; function c(r) { return r.catch((e) => { throw e.message.includes("No identity is unlocked") ? new Error(M) : e; }); } function B(r, e) { return { async getPublicKey() { const { publicKey: t } = await c(r.getPublicKey()); return t; }, async signEvent(t) { return c(r.signEvent(t)); }, async getRelays() { const t = {}; for (const a of e()) t[a] = { read: !0, write: !0 }; return t; }, nip44: { async encrypt(t, a) { const { ciphertext: s } = await c(r.nip44Encrypt({ peerPublicKey: t, plaintext: a })); return s; }, async decrypt(t, a) { const { plaintext: s } = await c(r.nip44Decrypt({ peerPublicKey: t, payload: a })); return s; } }, nip04: { async encrypt(t, a) { const { ciphertext: s } = await c(r.nip04Encrypt({ peerPublicKey: t, plaintext: a })); return s; }, async decrypt(t, a) { const { plaintext: s } = await c(r.nip04Decrypt({ peerPublicKey: t, payload: a })); return s; } }, _bitlogin: !0 }; } function T(r) { const e = r.getAttribute("vault-relays"), t = r.getAttribute("discovery-relays"); return { vaultRelayUrls: e ? e.split(",").map((a) => a.trim()).filter(Boolean) : void 0, discoveryRelayUrls: t ? t.split(",").map((a) => a.trim()).filter(Boolean) : void 0 }; } const q = ( /* css */ ` :host { --bl-accent: #6d28d9; --bl-accent-hover: #5b21b6; --bl-accent-fg: white; --bl-bg: #ffffff; --bl-fg: #16151a; --bl-muted: #6b7280; --bl-border: #e5e7eb; --bl-input-bg: #f9fafb; --bl-danger: #b91c1c; --bl-danger-bg: #fef2f2; --bl-warn: #92400e; --bl-warn-bg: #fffbeb; --bl-radius: 14px; /* Caps the widget's own width; a host page that wants it to fill a wider container (rather than stay a fixed-width card) overrides this to a larger value or "none". */ --bl-max-width: 380px; --bl-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; all: initial; display: inline-block; width: 100%; max-width: var(--bl-max-width); font-family: var(--bl-font-family); color-scheme: light dark; } @media (prefers-color-scheme: dark) { :host { --bl-bg: #17151f; --bl-fg: #f4f2f8; --bl-muted: #9a94ab; --bl-border: #322d40; --bl-input-bg: #201d2b; --bl-danger: #fca5a5; --bl-danger-bg: #3a1414; --bl-warn: #fcd34d; --bl-warn-bg: #3a2a0a; } } :host([data-theme="dark"]) { --bl-bg: #17151f; --bl-fg: #f4f2f8; --bl-muted: #9a94ab; --bl-border: #322d40; --bl-input-bg: #201d2b; } :host([data-theme="light"]) { --bl-bg: #ffffff; --bl-fg: #16151a; --bl-muted: #6b7280; --bl-border: #e5e7eb; --bl-input-bg: #f9fafb; } * { box-sizing: border-box; } .card { position: relative; width: 100%; background: var(--bl-bg); color: var(--bl-fg); border: 1px solid var(--bl-border); border-radius: var(--bl-radius); padding: 24px; font-size: 14px; line-height: 1.5; } h2 { font-size: 18px; font-weight: 600; margin: 0 0 4px; } /* One fixed SVG (icon + wordmark, same file as the marketing site's assets/lockup.svg) with the relative size and spacing between them baked in -- only the overall height is ever set here, so the lockup can never end up looking different in one place than another the way independently-sized icon + text elements could drift apart. */ .brand-lockup { display: block; height: 20px; width: auto; margin: 0 0 6px; color: var(--bl-fg); } /* Brief confirmation stamp shown over the destination screen right after a real security-relevant success (see flashSuccess() in element.ts) -- the same brass ring and checkmark as the brand mark, so a successful sign-in/create/recover/rotate reads as "sealed," not just a silent screen change. Never blocks interaction, and fades out on its own; the JS timer that removes it entirely doesn't depend on the animation. */ .success-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: var(--bl-bg); border-radius: var(--bl-radius); pointer-events: none; animation: bl-overlay-fade-out 0.4s ease 0.7s both; } .success-stamp svg { width: 56px; height: 56px; animation: bl-stamp-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; } .success-label { margin: 0; font-size: 14px; font-weight: 600; color: var(--bl-fg); opacity: 0; animation: bl-label-in 0.3s ease 0.35s both; } @keyframes bl-stamp-in { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.15); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes bl-label-in { to { opacity: 1; } } @keyframes bl-overlay-fade-out { 0% { opacity: 1; } 100% { opacity: 0; } } @media (prefers-reduced-motion: reduce) { .success-overlay { animation: none; } .success-stamp svg { animation: none; } .success-label { animation: none; opacity: 1; } } p.sub { color: var(--bl-muted); margin: 0 0 18px; font-size: 13px; } label { display: block; font-size: 13px; font-weight: 500; margin: 14px 0 6px; } input[type="text"], input[type="password"] { width: 100%; padding: 10px 12px; border-radius: 9px; border: 1px solid var(--bl-border); background: var(--bl-input-bg); color: var(--bl-fg); font-size: 14px; font-family: inherit; } input:focus { outline: 2px solid var(--bl-accent); outline-offset: 1px; } .credential-box { background: var(--bl-input-bg); border: 1px solid var(--bl-border); border-radius: 9px; padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13.5px; word-break: break-word; margin: 6px 0 2px; user-select: all; } .phrase-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 10px 0; } .phrase-word { background: var(--bl-input-bg); border: 1px solid var(--bl-border); border-radius: 7px; padding: 6px 8px; font-size: 12.5px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; display: flex; gap: 4px; } .phrase-word span { color: var(--bl-muted); } button { font-family: inherit; font-size: 14px; font-weight: 600; border-radius: 9px; border: none; padding: 11px 14px; cursor: pointer; width: 100%; margin-top: 14px; } button.primary { background: var(--bl-accent); color: var(--bl-accent-fg); } button.primary:hover { background: var(--bl-accent-hover); } button.primary:disabled { opacity: 0.55; cursor: default; } button.secondary { background: transparent; color: var(--bl-fg); border: 1px solid var(--bl-border); } button.secondary:hover { background: var(--bl-input-bg); } button.link { display: block; background: none; color: var(--bl-accent); width: auto; padding: 0; margin: 12px 0 0; font-weight: 500; font-size: 13px; } .row { display: flex; gap: 8px; } .row > * { flex: 1; } .notice { border-radius: 9px; padding: 10px 12px; font-size: 13px; margin: 12px 0; } .notice.warn { background: var(--bl-warn-bg); color: var(--bl-warn); } .notice.error { background: var(--bl-danger-bg); color: var(--bl-danger); } .notice.info { background: var(--bl-input-bg); color: var(--bl-fg); border: 1px solid var(--bl-border); } .pubkey { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--bl-muted); word-break: break-all; } .divider { border-top: 1px solid var(--bl-border); margin: 18px 0; } .small { font-size: 12px; color: var(--bl-muted); } .checkbox-row { display: flex; align-items: flex-start; gap: 8px; margin: 14px 0; font-size: 13px; } .checkbox-row input { margin-top: 3px; } .spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.4); border-top-color: white; border-radius: 50%; animation: bl-spin 0.7s linear infinite; vertical-align: -2px; margin-right: 6px; } @keyframes bl-spin { to { transform: rotate(360deg); } } ` ), y = "bc:config", U = /^nostr\+walletconnect:\/\//iu; let h = null; function F(r) { if (!h) { try { localStorage.removeItem(y); } catch { } h = import("./bitlogin-shared-IsyHQNSG.js").then((e) => (e.init({ appName: r, filters: ["nwc"], showBalance: !1, autoConnect: !1, persistConnection: !1, providerConfig: { nwc: { authorizationUrlOptions: { name: r, requestMethods: ["pay_invoice"] } } } }), e)).catch((e) => { throw h = null, e; }); } return h; } function I(r) { const e = r instanceof Error ? r.message : String(r); return /closed|cancell?ed|dismissed|aborted/iu.test(e); } async function O(r) { const e = await F(r); try { const t = await e.requestProvider(), a = e.getConnectorConfig()?.nwcUrl, s = t.client?.nostrWalletConnectUrl, n = typeof a == "string" ? a : typeof s == "string" ? s : ""; if (!U.test(n.trim())) throw new Error("The wallet did not return an NWC connection. Try again or paste one manually."); return n.trim(); } catch (t) { if (I(t)) return null; throw t; } finally { try { e.disconnect(); } catch { } try { localStorage.removeItem(y); } catch { } } } class D extends HTMLElement { root; worker; vaultRelayUrls = []; discoveryRelayUrls = []; installedProvider = null; screen = "welcome"; busy = !1; errorMessage; loginName = ""; generatedCredential = ""; savedCheckbox = !1; recoveryPhrase = ""; confirmSlots = []; // Manual password opt-in (§9.3). Off by default; the generated passphrase remains the // recommendation. manualPasswordFeedback is updated on every keystroke WITHOUT a full // re-render (see onInput) so the field never loses focus while the user is typing. // manualPasswordDraft/manualPasswordConfirmDraft re-populate the fields' value attribute // across a validation-failure re-render, so a rejected password isn't silently wiped, // forcing the user to retype it (and re-check "I saved it") from scratch. manualPasswordMode = !1; manualPasswordFeedback = null; manualPasswordDraft = ""; manualPasswordConfirmDraft = ""; // Import flow (§SF10). importKey holds the pasted nsec/hex only until registration completes. importKey = ""; importPreviewNpub = ""; recoverPhraseInput = ""; recoveredPreview = null; newCredentialAfterRecovery = ""; // Optional recovery-export-file fallback (§19.5) for when live relays can't be reached. // Never a substitute for the phrase -- the file never contains it or any phrase-derived key. offlineExportFile = null; offlineExportFileNotice; session = null; sessionWarnings = []; lastSignedEventJson = ""; exportedNsec = ""; changePasswordNewCredential = ""; // Brief animated brass-seal stamp (matching the widget's own brand mark) shown once over // the destination screen right after a real security-relevant success -- see flashSuccess(). pendingSuccessLabel = null; successDismissTimer = null; // Rollback confirmation (§16.2 step 6). A RollbackDetectedError from either login or // password-change means this device has already seen a newer credential generation than the // one just read -- most likely a rotated-away password being replayed from a relay that never // processed its tombstone. Rather than a passive dashboard banner shown after the fact (which // let an old, "revoked" password fully unlock a session), this blocks BEFORE claiming the // signer or dispatching bitlogin-login, and requires an explicit second step to proceed. pendingRollback = null; rollbackMessage = ""; // ---- Connection Vault request flow (vault-ux.md §2-§4, reveal mode) ---- // stage "auth": waiting for the user to sign in first; the goto("dashboard") // hook resumes the request exactly once. stage "active": the vault screens // own navigation until finishVaultRequest() settles the promise. vaultRequest = null; vaultCandidate = null; /** True when the account cannot store the connection (no vault root); the * flow still hands the URI to the app, labeled as unsaved. */ vaultUnsaved = !1; vaultUnsavedReason; vaultConnections = null; /** offerNwcConnection state: the app already holds this URI; the only * question on screen is whether a copy enters the user's vault. */ /** Claimed synchronously by offerNwcConnection before any await (see there). */ offerInFlight = !1; vaultOffer = null; constructor() { super(), this.root = this.attachShadow({ mode: "open" }); const e = new CSSStyleSheet(); e.replaceSync(q), this.root.adoptedStyleSheets = [e], this.worker = new N(); } connectedCallback() { const e = T(this); if (this.vaultRelayUrls = e.vaultRelayUrls ?? [], this.discoveryRelayUrls = e.discoveryRelayUrls ?? [], this.worker.configure({ vaultRelayUrls: this.vaultRelayUrls, discoveryRelayUrls: this.discoveryRelayUrls }).then(() => this.tryRestoreSession()), this.root.addEventListener("click", (t) => this.onClick(t)), this.root.addEventListener("submit", (t) => this.onSubmit(t)), this.root.addEventListener("input", (t) => this.onInput(t)), this.root.addEventListener("change", (t) => void this.onFileChange(t)), this.render(), this.installedProvider = B(this.worker, () => this.vaultRelayUrls), !window.nostr) try { window.nostr = this.installedProvider; } catch { } } disconnectedCallback() { this.successDismissTimer !== null && clearTimeout(this.successDismissTimer), this.releaseSigner(), this.worker.terminate(); } // ---- Public API mirroring window.nostr, scoped to this element instance ---- async getPublicKey() { return (await this.worker.getPublicKey()).publicKey; } async signEvent(e) { return this.worker.signEvent(e); } /** * Element-scoped NIP-44 encryption, matching getPublicKey/signEvent above. A host page * embedding multiple signing methods (its own extension detection, another widget) should * prefer this over reaching through `window.nostr.nip44` -- window.nostr is a single global * slot that whichever provider signed in last currently owns, so a host holding a direct * reference to ITS OWN `` element can talk to it deterministically instead of * racing other providers for that slot. */ async nip44Encrypt(e, t) { return (await this.worker.nip44Encrypt({ peerPublicKey: e, plaintext: t })).ciphertext; } async nip44Decrypt(e, t) { return (await this.worker.nip44Decrypt({ peerPublicKey: e, payload: t })).plaintext; } /** * Legacy relative to nip44Encrypt/nip44Decrypt above, but still what a real NIP-07 * extension exposes as window.nostr.nip04 -- implemented for drop-in parity with sites * (or their older DM code paths) that still expect NIP-04 rather than NIP-44. */ async nip04Encrypt(e, t) { return (await this.worker.nip04Encrypt({ peerPublicKey: e, plaintext: t })).ciphertext; } async nip04Decrypt(e, t) { return (await this.worker.nip04Decrypt({ peerPublicKey: e, payload: t })).plaintext; } async logout() { await this.worker.logout(), this.session = null, this.releaseSigner(), this.dispatchEvent(new CustomEvent("bitlogin-logout")), this.goto("welcome"); } /** * Connection Vault request API (connection-vault.md §12, vault-ux.md §2-§4). * * Asks the user to share an NWC wallet connection with THIS page's origin * and resolves the raw `nostr+walletconnect://` URI, or null if the user * declines or dismisses. REVEAL MODE, stated plainly: the caller receives * the full bearer credential and everything its wallet-side budget allows — * an embedded same-origin widget cannot broker (§CV12.3), so it does not * pretend to. The consent copy tells the user the same thing. * * If nobody is signed in, the widget shows its sign-in flow first and * resumes the request after. If the account already has a connection bound * to this origin, the user sees a one-tap approval; otherwise a guided * import (Bitcoin Connect chooser, or paste). */ async requestNwcConnection(e = {}) { if (this.vaultRequest) throw new Error("A wallet connection request is already in progress."); const t = window.location.origin, a = e.appName?.trim() || window.location.hostname || "This app"; return new Promise((s) => { this.vaultRequest = { appName: a, reason: e.reason, origin: t, stage: "auth", resolve: s }, this.dispatchEvent(new CustomEvent("bitlogin-request-pending")), (async () => { try { if (!(await this.worker.getSessionStatus()).unlocked) { this.goto("login"); return; } await this.continueVaultRequest(); } catch (n) { this.fail(n); } })(); }); } /** * Offer-to-save (the inverse of requestNwcConnection): the app OBTAINED an * NWC URI by its own means — its own wallet chooser, its own paste box — * and offers the user a portable copy. Consent-gated in this widget's own * UI, never silent: the write goes to the user's account (encrypted events * under their vault identity), and nothing enters or leaves the vault * without the user seeing it happen in BitLogin's chrome. * * Resolves "saved", "declined", "already-saved" (same wallet + secret * exists; its origin binding was refreshed, no UI shown), or "unavailable" * (no session or no vault root — the offer is quietly impossible, and an * app should treat that as a no-op rather than an error). */ async offerNwcConnection(e, t = {}) { if (this.vaultOffer || this.vaultRequest || this.offerInFlight) return "unavailable"; this.offerInFlight = !0; try { return await this.runOfferNwcConnection(e, t); } finally { this.offerInFlight = !1; } } async runOfferNwcConnection(e, t) { if (!(await this.worker.getSessionStatus().catch(() => ({ unlocked: !1 }))).unlocked || !(await this.worker.vaultStatus()).enabled) return "unavailable"; if ((await this.worker.vaultOfferCheck({ uri: e })).duplicate) return "already-saved"; const o = t.appName?.trim() || window.location.hostname || "This app"; return new Promise((l) => { this.vaultOffer = { uri: e, appName: o, label: t.label?.trim() || `${o} wallet`, resolve: l }, this.dispatchEvent(new CustomEvent("bitlogin-offer-pending")), this.goto("vault-offer"); }); } finishVaultOffer(e) { const t = this.vaultOffer; t && (this.vaultOffer = null, t.resolve(e), this.goto(this.session ? "dashboard" : "welcome")); } async acceptVaultOffer() { const e = this.vaultOffer; if (e) { this.setBusy(!0); try { const t = this.field("vaultOfferLabel").trim() || e.label; await this.worker.vaultSaveNwc({ uri: e.uri, label: t }), this.setBusy(!1), this.finishVaultOffer("saved"), this.flashSuccess(this.screen, "Wallet saved"), this.dispatchEvent( new CustomEvent("bitlogin-connection-granted", { detail: { origin: window.location.origin } }) ); } catch (t) { this.setBusy(!1), this.fail(t); } } } /** Settles the pending request exactly once and returns to a neutral screen. */ finishVaultRequest(e) { const t = this.vaultRequest; t && (this.vaultRequest = null, this.vaultCandidate = null, this.vaultUnsaved = !1, this.vaultUnsavedReason = void 0, t.resolve(e), e !== null && this.dispatchEvent( new CustomEvent("bitlogin-connection-granted", { detail: { origin: t.origin } }) ), this.goto(this.session ? "dashboard" : "welcome")); } async continueVaultRequest() { const e = this.vaultRequest; if (!e) return; e.stage = "active"; const t = await this.worker.vaultStatus(); if (!t.enabled) { this.vaultUnsaved = !0, this.vaultUnsavedReason = t.reason, this.goto("vault-import"); return; } this.vaultUnsaved = !1; const a = await this.worker.vaultFindForOrigin(); a.connection ? (this.vaultCandidate = a.connection, this.goto("vault-consent")) : this.goto("vault-import"); } async approveVaultCandidate() { const e = this.vaultCandidate; if (!(!e || !this.vaultRequest)) { this.setBusy(!0); try { const { uri: t } = await this.worker.vaultRevealNwc({ connectionId: e.connectionId }); this.setBusy(!1), this.finishVaultRequest(t), this.flashSuccess(this.screen, "Wallet shared"); } catch (t) { this.setBusy(!1), this.fail(t); } } } async runVaultBcChooser() { const e = this.vaultRequest; if (e) { this.setBusy(!0); try { const t = await O(e.appName); if (t === null) { this.setBusy(!1); return; } await this.saveAndShareVaultUri(t, this.field("vaultLabel")); } catch (t) { this.setBusy(!1), this.fail(t); } } } async handleVaultImportSubmit() { const e = this.field("vaultUri").trim(); if (e) { this.setBusy(!0); try { await this.saveAndShareVaultUri(e, this.field("vaultLabel")); } catch (t) { this.setBusy(!1), this.fail(t); } } } async saveAndShareVaultUri(e, t) { const a = this.vaultRequest; if (!a) return; if (this.vaultUnsaved) { this.setBusy(!1), this.finishVaultRequest(e); return; } const s = t.trim() || `${a.appName} wallet`; await this.worker.vaultSaveNwc({ uri: e, label: s }), this.setBusy(!1), this.finishVaultRequest(e), this.flashSuccess(this.screen, "Wallet connected"); } async loadVaultManage() { this.vaultConnections = null, this.goto("vault-manage"); try { const e = await this.worker.vaultList(); this.vaultConnections = e.connections; } catch (e) { this.errorMessage = e instanceof Error ? e.message : String(e), this.vaultConnections = []; } this.render(); } async vaultUnbind(e) { this.setBusy(!0); try { await this.worker.vaultSetBinding({ connectionId: e, origin: null }), this.busy = !1, await this.loadVaultManage(); } catch (t) { this.setBusy(!1), this.fail(t); } } async vaultDeleteConnection(e) { this.setBusy(!0); try { await this.worker.vaultDelete({ connectionId: e }), this.busy = !1, await this.loadVaultManage(); } catch (t) { this.setBusy(!1), this.fail(t); } } /** * (Re)installs this element's own provider as window.nostr, taking over from whatever is * currently there (an extension, a different BitLogin instance, or nothing). Called * automatically whenever a user completes sign-in through this widget — that's an * explicit signal they want BitLogin active. Also public: a host page offering several * signing methods can call it directly (e.g. `document.querySelector('bitlogin-auth').claimSigner()`) * when the user re-selects BitLogin from its own method picker, without a page reload. * * Best-effort: some NIP-07 extensions install window.nostr as a non-configurable, * non-writable property specifically to prevent another script from overwriting it, and * the plain assignment below throws a TypeError in that case ("Cannot assign to read only * property 'nostr'..."). This is never fatal to BitLogin's own session -- a host page * holding a reference to this element (getPublicKey/signEvent/nip44Encrypt/nip44Decrypt * above) never depends on window.nostr at all -- so the failure is caught and reported * through the returned boolean and the event detail rather than thrown, and every caller * below proceeds to complete sign-in regardless. Only a host page that reads window.nostr * directly (instead of talking to this element) is actually affected, and only for as long * as the other extension holds the slot. */ claimSigner() { try { return window.nostr = this.installedProvider, this.dispatchEvent(new CustomEvent("bitlogin-signer-claimed", { detail: { windowNostrClaimed: !0 } })), !0; } catch (e) { const t = e instanceof Error ? e.message : String(e); return this.dispatchEvent(new CustomEvent("bitlogin-signer-claimed", { detail: { windowNostrClaimed: !1, error: t } })), !1; } } /** * Surfaces a claimSigner() failure on the dashboard itself (not just the event detail), * using the same sessionWarnings/renderWarnings mechanism already shown for rollback and * relay-disagreement warnings -- so a user isn't left wondering why some other app still * seems to be using a different signer. */ noteSignerClaim(e) { e || (this.sessionWarnings = [ ...this.sessionWarnings, "Another Nostr signer (browser extension) is active in this browser and couldn't be replaced. You're signed in to BitLogin, but any app that reads window.nostr directly may still use that other signer instead of this one." ]); } /** * Releases window.nostr back to undefined, but only if it's still this element's own * provider — never clobbers a different signing method a host page (or another widget * instance) may have since taken over. Called automatically on logout and on removal from * the DOM, so a page offering multiple signing methods (an extension, a NIP-46 bunker, * BitLogin) can let a user switch away from BitLogin without a full page reload — before * this existed, window.nostr stayed pointed at a signed-out BitLogin provider forever. * Returns whether it actually released anything. Best-effort for the same reason as * claimSigner above: a property some other extension made non-configurable can also fail * to `delete`, and that must not block logout either. */ releaseSigner() { const e = window; if (this.installedProvider && e.nostr === this.installedProvider) { try { delete e.nostr; } catch { return !1; } return this.dispatchEvent(new CustomEvent("bitlogin-signer-released")), !0; } return !1; } goto(e) { if (e === "dashboard" && this.vaultRequest?.stage === "auth") { this.continueVaultRequest().catch((t) => this.fail(t)); return; } if (e === "welcome" && this.vaultRequest) { this.finishVaultRequest(null); return; } if (e === "welcome" && this.vaultOffer) { this.finishVaultOffer("declined"); return; } this.screen = e, this.errorMessage = void 0, this.render(); } setBusy(e) { this.busy = e, this.render(); } fail(e) { this.errorMessage = e instanceof Error ? e.message : String(e), this.busy = !1, this.render(); } field(e) { return this.root.querySelector(`[name="${e}"]`)?.value ?? ""; } /** * Shared manual-password field markup (§9.3), reused on every screen that lets a user set * their own password (initial creation, phrase recovery, and password rotation) so the * option isn't only available at signup. */ renderManualPasswordFields() { return `
`; } /** * Reads and validates the manual-password fields against a caller-supplied login name. * Always stashes what was typed into manualPasswordDraft/manualPasswordConfirmDraft so a * validation-failure re-render can restore it (see renderManualPasswordFields). */ readManualPassword(e) { const t = this.field("manualPassword"), a = this.field("manualPasswordConfirm"); this.manualPasswordDraft = t, this.manualPasswordConfirmDraft = a; const s = g(t, e); return s.ok ? t !== a ? { password: "", error: "Passwords do not match. Please re-enter both." } : { password: t } : { password: "", error: `Password not accepted: ${s.reason}` }; } /** * Explicitly asks the browser to offer saving this credential via the Credential * Management API (Chromium-based browsers only — Firefox and Safari never implemented * `PasswordCredential`). This is the primary fix for password-manager integration here, * not just a nice-to-have: the generated-password screen never puts the password into a * real `` for a browser to observe being filled (it's shown as text), and even on * the login screen — which does use real, correctly-`autocomplete`d inputs — save/autofill * heuristics are unreliable inside a shadow root, and this widget's JS-driven submit * (`preventDefault`, no real form POST) doesn't produce the passive signal most heuristics * expect anyway. An explicit `navigator.credentials.store()` call sidesteps all of that. * Best-effort only: never blocks or fails the surrounding flow. */ async offerToSaveCredential(e, t) { try { const a = window.PasswordCredential; if (!a || !navigator.credentials?.store) return; const s = new a({ id: e, password: t, name: e }); await navigator.credentials.store(s); } catch { } } async onClick(e) { if (!e.isTrusted) return; const t = e.target.closest("[data-action]"); if (!t) return; switch (t.dataset.action) { case "vault-offer-save": return this.acceptVaultOffer(); case "vault-offer-decline": this.finishVaultOffer("declined"); return; case "vault-approve": return this.approveVaultCandidate(); case "vault-different": this.vaultCandidate = null, this.goto("vault-import"); return; case "vault-bc": return this.runVaultBcChooser(); case "vault-cancel": this.finishVaultRequest(null); return; case "goto-vault-manage": return this.loadVaultManage(); case "vault-unbind": return this.vaultUnbind(t.dataset.id); case "vault-delete": return this.vaultDeleteConnection(t.dataset.id); case "goto-create": this.loginName = "", this.importKey = "", this.importPreviewNpub = "", this.goto("create-name"); return; case "goto-import": this.importKey = "", this.importPreviewNpub = "", this.goto("import-key"); return; case "preview-import": return this.handlePreviewImport(); case "import-continue": return this.handleImportContinue(); case "goto-login": this.goto("login"); return; case "goto-recover": this.offlineExportFile = null, this.offlineExportFileNotice = void 0, this.goto("recover-phrase"); return; case "goto-welcome": this.pendingRollback = null, this.goto("welcome"); return; case "goto-dashboard": this.goto("dashboard"); return; case "goto-verify-phrase": this.goto("verify-phrase"); return; case "goto-confirm-phrase": this.goto("confirm-phrase"); return; case "goto-change-password": this.changePasswordNewCredential = u().secret, this.manualPasswordMode = !1, this.manualPasswordFeedback = null, this.manualPasswordDraft = "", this.manualPasswordConfirmDraft = "", this.goto("change-password"); return; case "goto-export": this.goto("export"); return; case "regenerate-credential": this.generatedCredential = u().secret, this.savedCheckbox = !1, this.render(); return; case "toggle-manual-password": this.manualPasswordMode = !this.manualPasswordMode, this.manualPasswordFeedback = null, this.manualPasswordDraft = "", this.manualPasswordConfirmDraft = "", this.savedCheckbox = !1, this.render(); return; case "copy-credential": { const s = this.root.querySelector("#credential-box"), n = t, o = n.textContent ?? "Copy", l = (v) => { n.textContent = v, setTimeout(() => { n.isConnected && (n.textContent = o); }, 2e3); }; if (!s || !navigator.clipboard?.writeText) { l("Copy not available — select the text manually"); return; } navigator.clipboard.writeText(s.textContent ?? "").then( () => l("Copied"), () => l("Copy failed — select the text manually") ); return; } case "download-recovery-export": return this.handleDownloadRecoveryExport(); case "sign-test-event": return this.handleSignTestEvent(); case "reveal-nsec": return this.handleRevealNsec(); case "logout": return this.logout(); case "rollback-retry": { const s = this.pendingRollback?.kind; this.pendingRollback = null, this.goto(s === "change-password" ? "change-password" : "login"); return; } case "rollback-continue": return this.handleRollbackContinue(); default: return; } } async onSubmit(e) { if (e.preventDefault(), !e.isTrusted) return; const a = e.target.dataset.form; try { switch (a) { case "import-key": return await this.handlePreviewImport(); case "create-name": return this.handleCreateNameSubmit(); case "create-credential": return await this.handleCreateCredentialSubmit(); case "verify-phrase": return this.handleVerifyPhraseSubmit(); case "login": return await this.handleLoginSubmit(); case "recover-phrase": return await this.handleRecoverPhraseSubmit(); case "recover-new-credentials": return await this.handleRecoverNewCredentialsSubmit(); case "change-password": return await this.handleChangePasswordSubmit(); case "vault-import": return await this.handleVaultImportSubmit(); default: return; } } catch (s) { this.fail(s); } } /** * Live entropy/strength and match feedback for the manual-password fields, updated by * DIRECTLY patching feedback elements' text — never calling this.render() — so neither * input loses focus or cursor position while the user is typing (§9.3). */ onInput(e) { const t = e.target; if (!(t instanceof HTMLInputElement) || t.name !== "manualPassword" && t.name !== "manualPasswordConfirm") return; const a = this.root.querySelector('input[name="manualPassword"]')?.value ?? "", s = this.root.querySelector('input[name="manualPasswordConfirm"]')?.value ?? ""; this.manualPasswordFeedback = a ? g(a, this.loginName) : null; const n = this.root.querySelector("#manual-password-feedback"); if (n) if (!this.manualPasswordFeedback) n.textContent = "", n.className = "notice info"; else { const l = this.manualPasswordFeedback.entropyBits.toFixed(0); this.manualPasswordFeedback.ok ? (n.textContent = `Looks good (~${l} bits estimated).`, n.className = "notice info") : (n.textContent = `${this.manualPasswordFeedback.reason} (~${l} bits estimated)`, n.className = "notice warn"); } const o = this.root.querySelector("#manual-password-match-feedback"); o && (s ? s === a ? (o.textContent = "Passwords match.", o.className = "notice info") : (o.textContent = "Passwords do not match.", o.className = "notice warn") : (o.textContent = "", o.className = "notice info")); } /** * Reads and validates an optional recovery-export file (§19.5) for the recover-phrase * screen. Purely a relay-outage fallback -- the file never contains the phrase or any * phrase-derived key, so it's ignored entirely unless the user also enters their phrase. */ async onFileChange(e) { const t = e.target; if (!(t instanceof HTMLInputElement) || t.name !== "offlineExportFile" || t.type !== "file") return; const a = t.files?.[0]; if (!a) { this.offlineExportFile = null, this.offlineExportFileNotice = void 0, this.render(); return; } try { const s = await a.text(); this.offlineExportFile = x(JSON.parse(s)), this.offlineExportFileNotice = `Loaded recovery export from ${new Date(this.offlineExportFile.created_at * 1e3).toLocaleString()}.`; } catch (s) { this.offlineExportFile = null, this.offlineExportFileNotice = s instanceof C || s instanceof SyntaxError ? `Couldn't read that file: ${s.message}` : `Couldn't read that file: ${String(s)}`; } this.render(); } async handlePreviewImport() { const e = this.field("importKey").trim(); if (!e) { this.errorMessage = "Paste your nsec or hex private key first.", this.render(); return; } this.setBusy(!0); try { const t = await this.worker.previewImportKey({ nsecOrHex: e }); this.importKey = e, this.importPreviewNpub = t.npub, this.busy = !1, this.render(); } catch (t) { this.importKey = "", this.importPreviewNpub = "", this.fail(t); } } handleImportContinue() { if (!this.importKey || !this.importPreviewNpub) { this.errorMessage = "Check your key before continuing.", this.render(); return; } this.loginName = "", this.goto("create-name"); } handleCreateNameSubmit() { const e = this.field("loginName").trim().toLowerCase(); if (!p(e)) { this.errorMessage = "Login name must be 3-32 characters: a-z, 0-9, '.', '_', '-', and not start/end with punctuation.", this.render(); return; } this.loginName = e, this.generatedCredential = u().secret, this.savedCheckbox = !1, this.manualPasswordMode = !1, this.manualPasswordFeedback = null, this.manualPasswordDraft = "", this.manualPasswordConfirmDraft = "", this.goto("create-credential"); } async handleCreateCredentialSubmit() { this.savedCheckbox = this.root.querySelector("#saved-check")?.checked ?? !1; let e = this.generatedCredential; if (this.manualPasswordMode) { const { password: n, error: o } = this.readManualPassword(this.loginName); if (o) { this.errorMessage = o, this.render(); return; } e = n; } if (!this.savedCheckbox) { this.errorMessage = this.manualPasswordMode ? "Please confirm you've saved your password (or written it down) before continuing." : "Please confirm you saved your password before continuing.", this.render(); return; } this.setBusy(!0); const t = await this.worker.register({ loginName: this.loginName, password: e, importKey: this.importKey || void 0 }); this.importKey = "", this.importPreviewNpub = "", this.recoveryPhrase = t.recoveryPhrase; const a = this.recoveryPhrase.split(" "), s = K(a.length, 3); this.confirmSlots = s.map((n) => ({ index: n, value: "" })), this.session = { publicKey: t.everydayPublicKey, npub: d(t.everydayPublicKey), accountId: t.accountId }, this.busy = !1, this.goto("confirm-phrase"), this.worker.publishProfileAndRelayLists({ name: this.loginName, generalRelays: this.vaultRelayUrls, dmRelays: this.vaultRelayUrls }), this.offerToSaveCredential(this.loginName, e); } handleVerifyPhraseSubmit() { const e = this.recoveryPhrase.split(" "); for (const t of this.confirmSlots) if (this.field(`confirm-${t.index}`).trim().toLowerCase() !== e[t.index]) { this.errorMessage = `Word #${t.index + 1} doesn't match. Please check your saved phrase and try again.`, this.render(); return; } this.sessionWarnings = [], this.noteSignerClaim(this.claimSigner()), this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: this.session?.publicKey } })), this.flashSuccess("dashboard", "Account created"); } async handleLoginSubmit() { const e = this.field("loginName").trim().toLowerCase(), t = this.field("password"); await this.attemptLogin(e, t); } /** * Called once per connectedCallback, right after "configure" -- restores whatever * a prior login/register/rotate cached locally (§21), so a page reload lands * straight on the dashboard instead of asking for the login name + password * again. Silent by design: no flashSuccess() stamp (that's reserved for a * deliberate action the user just took) and no offerToSaveCredential (there's * no password in hand to save). If the welcome screen already rendered by the * time this resolves, goto() just re-renders over it -- a brief flash, not a * correctness issue. */ async tryRestoreSession() { try { const e = await this.worker.restoreSession(); if (!e.restored || !e.everydayPublicKey) return; this.session = { publicKey: e.everydayPublicKey, npub: d(e.everydayPublicKey), accountId: e.accountId }, this.noteSignerClaim(this.claimSigner()), this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: e.everydayPublicKey } })), this.goto("dashboard"); } catch { } } /** * Shared by the login form and the "continue anyway" rollback-confirmation step so both * paths grant a session identically -- claimSigner() and the bitlogin-login event only ever * fire once a RollbackDetectedError (if any) has been resolved one way or the other. */ async attemptLogin(e, t, a = !1) { this.setBusy(!0); try { const s = await this.worker.login({ loginName: e, password: t, acknowledgeRollback: a }); this.loginName = e, this.session = { publicKey: s.everydayPublicKey, npub: d(s.everydayPublicKey), accountId: s.accountId }, this.sessionWarnings = [s.rollbackWarning, s.relayDisagreementWarning].filter((n) => !!n), this.busy = !1, this.noteSignerClaim(this.claimSigner()), this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: s.everydayPublicKey } })), this.flashSuccess("dashboard", "Signed in"), this.offerToSaveCredential(e, t); } catch (s) { if (s instanceof Error && s.name === "RollbackDetectedError") { this.pendingRollback = { kind: "login", loginName: e, password: t }, this.rollbackMessage = s.message, this.busy = !1, this.goto("rollback-confirm"); return; } this.fail(s); } } async handleRollbackContinue() { const e = this.pendingRollback; if (this.pendingRollback = null, !e) { this.goto("welcome"); return; } e.kind === "login" ? await this.attemptLogin(e.loginName, e.password, !0) : await this.attemptChangePassword(e.oldPassword, e.newPassword, !0); } async handleRecoverPhraseSubmit() { const e = this.field("phrase").trim(); this.setBusy(!0); const t = await this.worker.recover({ phrase: e, offlineExportFile: this.offlineExportFile ?? void 0 }); this.recoverPhraseInput = e, this.recoveredPreview = { generalRelaysCount: t.generalRelays.length, dmRelaysCount: t.dmRelays.length, chainWarning: t.chainWarning }, this.session = { publicKey: t.everydayPublicKey, npub: d(t.everydayPublicKey), accountId: t.accountId }, this.newCredentialAfterRecovery = u().secret, this.manualPasswordMode = !1, this.manualPasswordFeedback = null, this.manualPasswordDraft = "", this.manualPasswordConfirmDraft = "", this.busy = !1, this.goto("recover-new-credentials"); } async handleRecoverNewCredentialsSubmit() { const e = this.field("newLoginName").trim().toLowerCase(); if (!p(e)) { this.errorMessage = "Login name must be 3-32 characters: a-z, 0-9, '.', '_', '-', and not start/end with punctuation.", this.render(); return; } let t = this.newCredentialAfterRecovery; if (this.manualPasswordMode) { const { password: a, error: s } = this.readManualPassword(e); if (s) { this.errorMessage = s, this.render(); return; } t = a; } this.setBusy(!0), await this.worker.completeRecovery({ newLoginName: e, newPassword: t }), this.loginName = e, this.busy = !1, this.sessionWarnings = [], this.noteSignerClaim(this.claimSigner()), this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: this.session?.publicKey } })), this.flashSuccess("dashboard", "Account recovered"), this.offerToSaveCredential(e, t); } async handleChangePasswordSubmit() { const e = this.field("oldPassword"); let t = this.changePasswordNewCredential; if (this.manualPasswordMode) { const { password: a, error: s } = this.readManualPassword(this.loginName); if (s) { this.errorMessage = s, this.render(); return; } t = a; } await this.attemptChangePassword(e, t); } /** Shared by the rotation form and the "continue anyway" rollback-confirmation step; see attemptLogin. */ async attemptChangePassword(e, t, a = !1) { this.setBusy(!0); try { await this.worker.changePassword({ loginName: this.loginName, oldPassword: e, newPassword: t, acknowledgeRollback: a }), this.busy = !1, this.sessionWarnings = [], this.noteSignerClaim(this.claimSigner()), this.flashSuccess("dashboard", "Password updated"), this.offerToSaveCredential(this.loginName, t); } catch (s) { if (s instanceof Error && s.name === "RollbackDetectedError") { this.pendingRollback = { kind: "change-password", oldPassword: e, newPassword: t }, this.rollbackMessage = s.message, this.busy = !1, this.goto("rollback-confirm"); return; } this.fail(s); } } async handleDownloadRecoveryExport() { const e = await this.worker.buildRecoveryExport(), t = new Blob([JSON.stringify(e, null, 2)], { type: "application/json" }), a = URL.createObjectURL(t), s = document.createElement("a"); s.href = a, s.download = "bitlogin-recovery-export.json", s.click(), URL.revokeObjectURL(a); } async handleSignTestEvent() { const e = await this.worker.signEvent({ kind: 1, content: `Hello from BitLogin at ${(/* @__PURE__ */ new Date()).toISOString()}` }); this.lastSignedEventJson = JSON.stringify(e, null, 2), this.render(); } async handleRevealNsec() { const { nsec: e } = await this.worker.exportIdentity(); this.exportedNsec = e, this.render(); } render() { const e = this.pendingSuccessLabel ? this.renderSuccessOverlay(this.pendingSuccessLabel) : ""; this.root.innerHTML = `
${this.renderScreen()}${e}
`; } renderSuccessOverlay(e) { return `

${i(e)}

`; } /** * Navigates to `screen` and briefly stamps it with the same brass-ring-and-checkmark mark * used in the logo, for the handful of moments that are a genuine security-relevant success * -- signing in, creating an account, recovering one, or rotating a password -- rather than * every validation step or navigation. Purely decorative: `pointer-events: none` (see * styles.ts) means it never blocks interacting with the screen underneath, and a JS timer * (not just the CSS animation) guarantees it's removed even if the animation can't run. */ flashSuccess(e, t) { this.goto(e), this.successDismissTimer !== null && clearTimeout(this.successDismissTimer), this.pendingSuccessLabel = t, this.render(), this.successDismissTimer = setTimeout(() => { this.pendingSuccessLabel = null, this.successDismissTimer = null, this.root.querySelector("[data-success-overlay]")?.remove(); }, 1100); } renderError() { return this.errorMessage ? `
${i(this.errorMessage)}
` : ""; } renderWarnings() { return this.sessionWarnings.map((e) => `
${i(e)}
`).join(""); } /** The widget's own brand lockup (mark + wordmark), inlined as vector paths rather than * relying on the host page having loaded any particular font -- shown once, on the * welcome screen, as the widget's one branding moment. */ renderBrandLockup() { return ' '; } renderScreen() { switch (this.screen) { case "welcome": return ` ${this.renderBrandLockup()}

A portable Nostr identity with a familiar login name and password.

${this.renderError()} `; case "import-key": { const e = !!this.importPreviewNpub; return `

Import an existing Nostr key

Wrap a Nostr identity you already control in a BitLogin login name and password. Your key never changes — you just get a friendlier way in.

Pasting a private key into any web page is risky. Only do this on a BitLogin build you trust, and clear your clipboard afterward. BitLogin can't secure copies of this key that already exist elsewhere.
${this.renderError()}
${e ? `
This key's public identity:
${i(this.importPreviewNpub)}
` : ""} `; } case "create-name": return `

${this.importKey ? "Set up your login" : "Create your BitLogin"}

${this.importKey ? "Choose a login name for your imported identity. It's a convenience, not a secret." : "Choose a login name. It's a convenience, not a secret (it contributes no security)."}

${this.renderError()}
`; case "create-credential": { const e = this.busy ? `${this.importKey ? "Importing…" : "Creating account…"}` : this.importKey ? "Import account" : "Create account"; return this.manualPasswordMode ? `

Choose your own password

Not recommended: a downloadable encrypted file can be guessed against forever, offline, with no rate limit. BitLogin can only run basic checks here — not a breach-database lookup — so weak or reused passwords are still your risk to carry.

Offline guessing against this password can never be fully prevented. The generated passphrase remains the safer default.
${this.renderError()}
${this.renderManualPasswordFields()}
` : `

Your generated password

BitLogin generates your password because no server can rate-limit guesses against a downloadable encrypted file.

${i(this.generatedCredential)}
${this.renderError()}
`; } case "confirm-phrase": return `

Save your recovery phrase

These 12 words can recover your identity if you forget your password or lose all your devices. We cannot recover these words for you.

Do not enter a Bitcoin or other cryptocurrency-wallet recovery phrase into BitLogin. This is a BitLogin-only phrase.

${this.recoveryPhrase.split(" ").map((e, t) => `
${t + 1}.${i(e)}
`).join("")}
${this.renderError()} `; case "verify-phrase": return `

Verify your recovery phrase

Your phrase is hidden now so you can confirm you actually saved it. Enter the requested words below.

${this.renderError()}
${this.confirmSlots.map( (e) => ` ` ).join("")}
`; case "login": return `

Sign in

${this.renderError()}
`; case "recover-phrase": return `

Recover with phrase

Enter your 12-word BitLogin recovery phrase.

Do not enter a Bitcoin or other cryptocurrency-wallet recovery phrase into BitLogin.

${this.renderError()}

Only needed if relays are unreachable -- the phrase above is still required either way. The file alone can never recover an account by itself.

${this.offlineExportFileNotice ? `
${i(this.offlineExportFileNotice)}
` : ""}
`; case "recover-new-credentials": { const e = this.recoveredPreview?.chainWarning ? `
${i(this.recoveredPreview.chainWarning)}
` : "", t = this.busy ? 'Finishing recovery…' : "Finish recovery"; return `

Identity recovered

Found your account. Restored ${this.recoveredPreview?.generalRelaysCount ?? 0} general relay(s) and ${this.recoveredPreview?.dmRelaysCount ?? 0} DM relay(s) from your public events.

${e}

Now set a new login name and password.

${this.manualPasswordMode ? '
Offline guessing against this password can never be fully prevented. The generated passphrase remains the safer default.
' : `
${i(this.newCredentialAfterRecovery)}
`} ${this.renderError()}
${this.manualPasswordMode ? this.renderManualPasswordFields() : ""}
`; } case "dashboard": return `

Signed in

${this.renderWarnings()}

${i(this.session?.npub ?? "")}

${this.renderError()} ${this.lastSignedEventJson ? `
${i(this.lastSignedEventJson)}
` : ""}
`; case "vault-consent": { const e = this.vaultRequest, t = this.vaultCandidate; if (!e || !t) return '
No wallet request is in progress.
'; const a = new Date(t.createdAt * 1e3).toLocaleDateString(); return `

Share a wallet with ${i(e.appName)}?

${e.reason ? `

Reason given: ${i(e.reason)}

` : ""} ${this.renderError()}
${i(t.label)}
Connected ${i(a)} · wallet ${i((t.walletPubkey ?? "").slice(0, 10))}…
${i(e.appName)} will receive this connection and can spend within the budget your wallet enforces, until you revoke it from Wallet connections.
`; } case "vault-import": { const e = this.vaultRequest; if (!e) return '
No wallet request is in progress.
'; const t = this.vaultUnsaved ? `
${this.vaultUnsavedReason === "no-vault" ? "This account predates the Connection Vault, so the connection will be handed to the app but not saved to your account. Enable the vault from your account manager (recovery phrase required) to make wallets portable." : "Sign in again to save connections to your account — until then the connection will be handed to the app but not saved."}
` : ""; return `

Connect a wallet for ${i(e.appName)}

${e.reason ? `

Reason given: ${i(e.reason)}

` : ""} ${t} ${this.renderError()}
Set a spending budget on the wallet's own authorization page — the wallet is the only place a budget is actually enforced.
`; } case "vault-offer": { const e = this.vaultOffer; return e ? `

Save this wallet to your BitLogin?

${i(e.appName)} just connected a wallet on this device. Saved to your BitLogin, the connection follows you — on a new device it's one tap instead of another paste.

${this.renderError()}
Stored encrypted on your account; ${i(e.appName)} keeps working either way. Remove it any time from Wallet connections.
` : '
No wallet offer is in progress.
'; } case "vault-manage": { const e = this.vaultConnections, t = e === null ? '

Loading…

' : e.length === 0 ? `

No connections stored yet. They're added when you connect a wallet inside an app.

` : e.map( (a) => `
${i(a.label)} (${i(a.connectionType)})
${a.origin ? `Linked to ${i(a.origin)}` : "Not linked to an app"}
${a.origin ? `` : ""}
` ).join(""); return `

Wallet connections

Connections your apps use, stored encrypted on your account and restored on any device you sign in to.

${this.renderError()} ${t}
"Remove from BitLogin" deletes the stored copy only — the connection itself keeps working for any app that already has it. To revoke spending authority, delete the connection inside your wallet app.
`; } case "rollback-confirm": return `

This looks like a stale or revoked credential

${i(this.rollbackMessage)}

This usually means the password just entered was rotated away in an earlier session on this device, and a relay hasn't caught up with that change. If you rotated your password, use the new one instead. Only continue if you're confident this is relay lag, not a stale credential.

`; case "change-password": return `

Rotate password

Your old password's capsule will be tombstoned and a deletion request issued. This does not erase copies an attacker may already have downloaded, and a relay that hasn't processed the deletion may keep serving the old password's capsule until a device that has already seen the new generation refuses it.

${this.manualPasswordMode ? '
Offline guessing against this password can never be fully prevented. The generated passphrase remains the safer default.
' : `
${i(this.changePasswordNewCredential)}
`} ${this.renderError()}
${this.manualPasswordMode ? this.renderManualPasswordFields() : ""}
`; case "export": return `

Export identity

Your public identity (npub) is safe to share. Your private key (nsec) is not.

${i(this.session?.npub ?? "")}
${this.renderError()} ${this.exportedNsec ? `
Never share this. Anyone with it controls your identity.
${i( this.exportedNsec )}
` : ""} `; default: return ""; } } } function K(r, e) { const t = Array.from({ length: r }, (s, n) => n), a = []; for (let s = 0; s < e && t.length > 0; s++) { const n = Math.floor(Math.random() * t.length); a.push(t.splice(n, 1)[0]); } return a.sort((s, n) => s - n); } function i(r) { return r.replace(/[&<>"']/gu, (e) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[e]); } customElements.get("bitlogin-auth") || customElements.define("bitlogin-auth", D); window.bitlogin = { version: "0.1.0", isActiveSigner() { return window.nostr?._bitlogin === !0; }, releaseSigner() { const r = window; return r.nostr?._bitlogin === !0 ? (delete r.nostr, window.dispatchEvent(new CustomEvent("bitlogin-signer-released")), !0) : !1; } }; export { D as BitLoginAuthElement, N as WorkerClient, B as createNip07Provider };