Files
bitlogin/packages/widget/src/index.ts
T
Claude a24719184e Add signer switching, manual-password parity, phrase-quiz split, and offline recovery import
Signer interop:
- <bitlogin-auth> gains claimSigner()/releaseSigner() instance methods, and
  window.bitlogin gains a matching releaseSigner(). Before this, once the
  widget claimed window.nostr it held it forever (even after logout), so a
  site offering multiple signing methods (an extension, a NIP-46 bunker,
  BitLogin) had no way to let a user switch away from BitLogin without a
  page reload. claimSigner() is now also called automatically on every
  successful sign-in/sign-up/recovery, and releaseSigner() on logout and
  DOM removal. Both fire bitlogin-signer-claimed/-released events.
  docs.html and llms.txt document the new API and a switch-signer example.

Manual password parity + fixes:
- The opt-in custom-password path (added earlier per spec §9.3) was only
  available at initial account creation; it's now offered on password
  rotation and phrase recovery too, via a shared renderManualPasswordFields()/
  readManualPassword() pair.
- Added a required confirm-password field with live match feedback,
  wherever a manual password can be set.
- Found and fixed a real bug while testing this: a validation failure
  (mismatched confirm, weak password) on any manual-password screen fully
  re-rendered the form, silently wiping the typed password and
  un-checking "I saved it" -- forcing a full retype. Fixed via draft
  fields that repopulate the inputs' value attribute across the
  failure re-render, and by reading the checkbox before validation
  instead of after.

Recovery-phrase display split (§anti-cheat):
- The 12-word phrase and its 3-word verification quiz were shown on the
  same screen, defeating the point of the quiz. Split into a
  display-only "confirm-phrase" screen and a separate "verify-phrase"
  screen where the phrase is hidden while the quiz is answered.

Offline recovery-export import (§19.5):
- recoverWithPhrase() now accepts optional offlineRecoveryCapsuleEvents,
  merged with whatever the live relay read returns; recovery still
  requires the phrase either way, since the export file never contains
  it or a phrase-derived key. Covered by a new core e2e test that
  recovers successfully against genuinely unreachable relay URLs using
  only the offline candidate.
- The recover-phrase screen gets an optional file input for a previously
  downloaded recovery export, parsed and validated client-side before
  being passed through to the worker.

Landing page copy updated to reflect that manual passwords are allowed
(with a real entropy floor and common-password check), not disallowed.

60/60 tests pass; core and widget typecheck clean. Verified live in
Chromium via Playwright against a 3-relay in-memory mock quorum: account
creation (generated and manual, including the mismatch/retry-preserves-
draft path), phrase display/quiz split, signer claim/release/reclaim
across logout and re-login, password rotation and phrase recovery with
manual passwords (including logging back in with the result), and the
offline recovery-export file path. Zero page or console errors across
every run.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
2026-07-21 16:41:35 +00:00

66 lines
3.0 KiB
TypeScript

/** Entry point: any static site includes this file and drops in <bitlogin-auth>. */
import { BitLoginAuthElement } from "./element.js";
if (!customElements.get("bitlogin-auth")) {
customElements.define("bitlogin-auth", BitLoginAuthElement);
}
export interface BitLoginGlobal {
readonly version: string;
/**
* True when BitLogin currently occupies window.nostr. Only one provider can hold that
* global at a time (a browser extension like Alby or nos2x may also implement it, or
* install itself later and overwrite BitLogin's). This check survives that overwrite —
* unlike checking `window.nostr._bitlogin` directly — since it's read fresh each call.
*/
isActiveSigner(): boolean;
/**
* Releases window.nostr back to undefined, but only if BitLogin currently occupies it —
* a no-op otherwise, so it's always safe to call speculatively. Lets a host page that
* offers several signing methods (an extension, a NIP-46 bunker, BitLogin) hand the slot
* back when a user switches away from BitLogin, without needing a reference to a specific
* <bitlogin-auth> element or a page reload. The element itself also exposes an
* instance-scoped `releaseSigner()`/`claimSigner()` pair for finer control when a page
* holds several widget instances. Returns whether it actually released anything.
*/
releaseSigner(): boolean;
}
declare global {
interface Window {
bitlogin?: BitLoginGlobal;
}
}
// Installed once per page regardless of whether <bitlogin-auth> has been used yet, so a
// host site can feature-detect BitLogin's presence and active-signer status independently
// of window.nostr's current occupant (§26.2, §26.4 — "active signer" support for sites that
// offer multiple signing methods: BitLogin, a NIP-07 extension, or a NIP-46 remote signer).
//
// This assignment is deliberately unconditional (not `if (!window.bitlogin)`): browsers
// auto-expose any element with a matching `id` or `name` attribute as a same-named global
// (e.g. `<div id="bitlogin">` becomes `window.bitlogin`). A guard would silently see that
// element instead of ever installing this object on any page that happens to use that id —
// including our own demo before it was renamed. A direct assignment creates a real own
// property that takes priority over that fallback, on any page.
window.bitlogin = {
version: "0.1.0",
isActiveSigner(): boolean {
return (window as unknown as { nostr?: { _bitlogin?: boolean } }).nostr?._bitlogin === true;
},
releaseSigner(): boolean {
const w = window as unknown as { nostr?: { _bitlogin?: boolean } };
if (w.nostr?._bitlogin === true) {
delete (w as { nostr?: unknown }).nostr;
window.dispatchEvent(new CustomEvent("bitlogin-signer-released"));
return true;
}
return false;
}
};
export { BitLoginAuthElement } from "./element.js";
export { WorkerClient } from "./worker/workerClient.js";
export { createNip07Provider } from "./provider.js";
export type { BitLoginConfig } from "./config.js";