The element was the only way to sign, so a host that renders its own UI, a background job, or a headless test couldn't sign without mounting <bitlogin-auth>. Add @bitlogin/widget/headless: createHeadlessSigner() → a DOM-free wrapper over the crypto worker exposing restore, getPublicKey, signEvent, nip44Encrypt/Decrypt, isUnlocked, logout, destroy. It restores the cached session and signs with no element and no window.nostr. It deliberately cannot interactively log in (that needs the password UI): restore() returns null when nothing is cached, at which point the host mounts <bitlogin-auth> once; after that the headless signer works across reloads. New bitlogin-headless.js build entry + @bitlogin/widget/headless export (also re-exported from @bitlogin/widget). Covered by headless.test.ts with a scripted worker; README gains "Signing without the widget (headless)". Addresses the "headless session-restore API" item of the Tessera integration feedback (docs/TODO.md). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
24 KiB
BitLogin — open items
Updated: 2026-08-04
Login-surface expansion (2026-08-04) — shipped, with owner follow-ups
One login surface, four methods (login-methods.md, passkey-login.md):
password (primary), NIP-07 extension, NIP-46 remote signer (memory-only),
and passkey via WebAuthn PRF. OAuth on-ramps were built, then removed the
same day for requiring per-site provider registration
(centralized-onramps.md, superseded, kept on the record). Also shipped:
the deployment-CSP 'wasm-unsafe-eval' fix (live password flows were
broken since 2026-07-31), the service-worker cache: "reload" precache
fix, window.bitlogin.activeMethod()/activeSession(), and welcome/sign-in
UI polish.
Security audit response (2026-08-04, same day)
All confirmed findings fixed and regression-tested: BL-16 (auth_url now
https-only via sanitizeAuthUrl, hostname shown on the link, hostile-scheme
tests), BL-17 (post-creation PRF fallback pinned via allowCredentials to
the created credential), BL-18 (dead-socket CLOSE can no longer strand a
query promise), BL-19 (local filter.limit enforcement + duplicate-id drop
- 1000-event ceiling per buffered query), BL-20 (.semgrep.yml try/catch
pattern was unparseable — fixed and verified firing on hostile code under
the pinned Semgrep 1.171.0). Lows: NIP-07 and NIP-46 now verify returned
signed events against the request (signature, identity, field equality);
Nip46Client.close()wipes the client and conversation keys; passkey labels are distinguishable; postcss ≥8.5.25; Actions pinned to commit SHAs.
Second-pass response (same day): BL-21 — bunker relays capped at 8 and
deduplicated; relay frames over 256K chars dropped before JSON.parse; all
cheap checks (id dedup, capacity) run before Schnorr verification, plus a
per-query verification budget. BL-22 — superseded/abandoned nostrconnect
attempts are cancelled via AbortSignal and their ephemeral keys wiped
(ownership transfers, not dangles, on success); per-candidate conversation
keys wiped in a finally. BL-23 — security:ci now runs the custom Semgrep
rules (security:semgrep; requires a local semgrep install, same pin as
CI). BL-24 — the courtesy protocol logout (current NIP-46 defines it,
explicitly not a security boundary) is now sent on teardown; this corrects
the earlier "no stable method name" note. Still deferred, interop only:
switch_relays.
Third-pass response (same day): BL-25 — live subscriptions now carry a bounded already-seen id cache (2048, FIFO eviction; a replayed retained event costs one verification and one delivery, ever) and a fixed-window verification budget (100 per 10s; distinct-event spray is quarantined for the window). BL-26 — teardown order inverted: the courtesy logout event is pre-signed, then close() IMMEDIATELY rejects every in-flight request and wipes keys, then the pre-signed event is delivered asynchronously and bounded — nothing can resolve after logout begins (regression-tested). BL-27 — a nostrconnect listen failing naturally (timeout, unreachable relays) now discards the pending ephemeral key in a guarded catch instead of orphaning it. BL-28 — the hosted verify job installs the pinned Semgrep before running security:ci (it was red at the previous HEAD, skipping SBOM and checksums). Informational — bunker URIs are bounded: 4096 chars total, 256-char secret, 512-char relay URLs (oversized relays filtered, oversized URI/secret rejected).
Owner action items:
- Live-fire the NIP-46 QR with Amber on bitlogin.network (flow was verified against a fake bunker in tests, not yet against a real phone signer end-to-end).
- Live-fire passkey sign-in on a real device. Partly discharged
2026-08-04:
packages/widget/src/e2e/passkey.e2e.test.tsdrives real headless Chromium through the real WebAuthn ceremonies with a CDP virtual authenticator (hasPrf: true), against the built demo and in-process mock relays — create passkey → register → dashboard npub → capsules verified on the relays → logout → sign in with the same passkey → same npub; plus a second passkey deriving a different account. Runs innpm test, skips automatically withoutnpx playwright install chromium. What that still does NOT cover: a real platform authenticator (Google Password Manager, iCloud Keychain, Bitwarden, a hardware key) and the create-time-PRF-absent fallback path, which Chromium's virtual authenticator does not exercise (it returns PRF results at creation). - Passkey derivation is a new compatibility contract —
packages/widget/src/passkey.test.tsknown-answer vectors carry the same never-update-the-expected-value rule asknownAnswer.test.ts. - NIP-46 credential persistence (vault record) stays gated on the external security review, as before.
Entropy audit (2026-07-31)
entropy-audit-2026-07-31.md — RNG and
randomness-integration audit. No predictable-fallback defect: random.ts
is the single chokepoint, throws rather than degrading, and rejection-samples to
avoid modulo bias. Three hardening items, in order:
- M2 — done. Nine tests in
packages/core/src/crypto/crypto.test.tspin fail-closed behaviour andrandomUniformInt's uniformity. Mutation-verified: a zero-filling stub in place of the throw turns five of them red, and swapping rejection sampling for bare modulo gives chi-square 5614 against a threshold of 147. - M1 — done.
nip44Encrypt/nip04Encryptno longer take a nonce or IV; the capability lives in the non-exportedcrypto/testing.ts, andcrypto/index.tsre-exports explicitly instead ofexport *. Only one test ever used an override andivOverridehad no callers, so nothing was lost. Verified from a real consumer against the built package: all three deep-import routes fail withERR_PACKAGE_PATH_NOT_EXPORTED.knownAnswer.test.tsgreen throughout — the derivation chain is untouched. - L1 — done. Both
Math.randomcalls (backup-quiz picker, relay subscription id) now use the CSPRNG, so the ban has no standing exception..semgrep.ymlenforces it in CI alongside two more rules: no fallback aroundgetRandomValues, and no timestamp-seeded secrets. Verified the rules fire — reintroducing the old call gives 1 blocking finding and exit 1.
The entropy audit is closed for this repo. The only gate left is an independent review of the RNG path by someone other than the author, which is not something a coding agent can sign off.
Owner action items
-
Close the Dependabot PR on
dependabot/npm_and_yarn/npm-security-and-maintenance-83024bc549. It bundles seven major bumps into one change (@noble/hashes1.8→2.2,@noble/curves1.9→2.2,@scure/base1.2→2.2,@scure/bip391.6→2.2, TypeScript 5→7, Vite 6→8, Vitest 3→4) and fails the build: noble v2 dropped the extensionless subpath exports this code imports. It should be closed, not merged or fixed —.github/dependabot.ymlnow groups only minor/patch and ignores majors of the derivation chain, so this PR will not recur.The four
dependabot/github_actions/*PRs are unaffected and fine to merge. -
Decide whether the manual-password removal is final. User-chosen passwords were removed entirely (not merely discouraged) in the 2026-07-31 hardening, and
packages/widget/src/securityPolicy.test.tspins that. The security argument is sound — a user-chosen password against a downloadable capsule is offline-guessable forever — but it is a product decision that changes what users can do, and it is now enforced by a test rather than by preference.
Standing rule: the derivation chain is a compatibility contract
Account capsules live on public relays. The login name plus password derive the locator, signing scalar and capsule key deterministically, so those outputs are a contract with every account that already exists — change them and users are locked out permanently, with no server-side record to migrate from.
packages/core/src/crypto/knownAnswer.test.ts pins HKDF-Extract, HKDF-Expand,
ScalarExpand (scalar, counter, and resulting public key), BIP-39 phrase and
seed, base64url framing, and JCS ordering.
A failure there is never "update the expected value". It means the change being attempted is account-breaking and needs a migration plan or rejection. The rest of the crypto suite only checks self-consistency — same input, same output within one run — which stays green through exactly the kind of upgrade that would lock everyone out.
Deferred hardening
Carried from the 2026-07-30 audit; none are live defects.
- Tier 2 —
packages/widget/src/element.tsis ~1,600 lines with no direct test coverage.securityPolicy.test.tsnow asserts a few properties of its source text, which is a start, not coverage. - Tier 3 — compare-and-swap on capsule writes (relays offer no atomic primitive, so this needs a protocol-level answer), and an encrypted high-water-mark store.
- Tier 4 — moving high-value authorization into a cross-origin or extension
boundary. An embedded widget is same-origin with its host page, so a
compromised host can reach the worker directly. Documented honestly in
llms.txtand the README rather than papered over.
Gates still in force
- External security review before the vault holds anything beyond a budget-capped NWC connection. The S3 and password profiles stay designs until then.
- Personal-tier UI was gated on the sudo key leaving the credential capsule. That gate is now met — the sudo key lives only in the phrase-gated recovery capsule — but the UI still needs a separately reviewed key ceremony before it ships.
Additive browser distribution and integrator API roadmap
These are additive compatibility goals. The existing multi-file build,
<bitlogin-auth> behavior, window.nostr provider, derivation chain, session
formats, and current consuming applications are compatibility contracts. Do
not replace or silently alter them while adding standalone distribution or a
new public API. Every release must continue to produce and test the standard
build before the standalone build is considered successful.
Priority 1 — Distribution
- Add a standalone browser build target:
npm run build:standalone. - Produce one self-contained
bitlogin.jsfor standalone consumers. - Keep the standard multi-file build unchanged and available alongside it:
dist/standard/anddist/standalone/. - Remove standalone runtime dependence on external
cryptoWorker.js, hashed shared chunks, or other sibling files; preserve Web Worker isolation internally where possible using a Blob/module worker. - Support this minimal static-site integration:
<script type="module" src="./bitlogin.js"></script>and<bitlogin-auth></bitlogin-auth>. - Verify standalone hosting from arbitrary static HTTPS origins, including GitHub Pages, Cloudflare Pages, Netlify, Vercel, and ordinary nginx.
- Verify localhost development usage.
- Document
file://limitations clearly, including the recommended local HTTPS or local development-server path. - Define CSP requirements for standalone Blob/module workers and report incompatible policies instead of silently degrading crypto isolation.
Priority 2 — Developer API
- Define and document one canonical signer interface with capability-based
methods:
getPublicKey(),signEvent(),getRelays(), and supportednip04.encrypt/decrypt,nip44.encrypt/decrypt, and NWC operations. - Keep
window.nostrcompatibility, including its existing NIP-07-shaped behavior, as an adapter over the canonical signer rather than a breaking replacement. - Expose a clean additive
window.bitloginAPI, includingactiveSession(),login(),logout(), andgetSigner(). - Add and document stable lifecycle event aliases:
bitlogin:ready,bitlogin:login,bitlogin:logout, andbitlogin:error. Preserve the existing hyphenated events (bitlogin-login,bitlogin-logout, and related current event names) for compatibility; the colon-named events must be additive and must not replace them silently. - Version and test event payloads; preserve existing event names and payload fields used by current apps while adding new fields additively.
- Expose capability detection, for example
signer.capabilities(), forsignEvent,nip04,nip44,nip46,nwc, andpasskey. - Ensure capability reporting describes the active session and never implies support for a method that the active signer cannot perform.
Priority 3 — Embedding and UI
- Support a headless mode so applications can use BitLogin without
rendering
<bitlogin-auth>. - Keep
<bitlogin-auth>as the default easy-to-integrate UI. - Allow applications to open the login dialog programmatically, including
await bitlogin.login(). - Allow configurable login methods, for example
<bitlogin-auth methods="passkey,password,nip07,nip46"></bitlogin-auth>. - Add basic theming hooks: CSS custom properties, light/dark support, accent color, and compact mode.
- Expose supported configuration through documented attributes/options; do not require integrators to reach into widget internals or private DOM.
- Preserve current login-method defaults and existing method ordering unless an explicit versioned compatibility decision says otherwise.
Priority 4 — Build, versioning, and releases
- Give browser artifacts predictable filenames:
bitlogin.jsand, if useful,bitlogin.min.js. - Expose version information through
window.bitlogin.versionand the module/API surface where applicable. - Emit integrity/hash manifests for releases and document exactly which artifact each hash covers.
- Create downloadable release artifacts:
bitlogin-standard.zipandbitlogin-standalone.js. - Publish release notes specifically for integrators, including changed browser requirements, CSP requirements, API additions, and compatibility notes.
- Evaluate public package exports for the widget, standalone entry point,
and core package (for example
@bitlogin/widget,@bitlogin/widget/standalone, and@bitlogin/core) without exposing unsupported private paths or weakening package encapsulation.
Priority 5 — Integration and compatibility testing
- Add a minimal vanilla HTML integration test for both standard and standalone builds.
- Test standalone flows for password account, passkey account, recovery phrase, NIP-07 extension, and NIP-46 bunker.
- Test
getPublicKey(),signEvent(),logout(), and session restore through both the canonical signer and the compatibility adapter. - Test signed kind-1 publication against real or mock relays.
- Test multiple BitLogin-enabled applications on separate origins and confirm origin-scoped storage/session behavior remains isolated.
- Test CSP compatibility, including the documented Blob/module-worker requirements and a fail-closed result when a required policy is absent.
- Test Blob Worker behavior across Chromium, Firefox, and Safari.
- Test passkeys on real platform authenticators in addition to the existing virtual-authenticator coverage.
- Run the existing derivation-chain known-answer tests and regression suite unchanged; never update expected values to make a compatibility failure pass.
- Verify current consuming applications (including AdZap, Satisfied, BitUnlock, BitRoad, and Bitvid where their checkouts are available) still load the standard artifact and retain login, signing, logout, and session restore behavior.
- Add a release gate that builds/tests the standard artifact before and after standalone bundling, proving standalone work cannot replace or mutate the standard output accidentally.
Priority 6 — Documentation
- Put a "30-second integration" section at the top of the README.
- Provide a complete single-page standard and standalone example.
- Provide a headless example.
- Provide a Nostr publishing example:
const signer = await bitlogin.getSigner();followed byconst event = await signer.signEvent(...);. - Provide a migration guide from the standard build to the standalone build, with a rollback path that requires no application-code rewrite.
- Clearly explain the security tradeoff between the standard worker/chunk build and the standalone bundled build.
- Clearly state that standalone is a distribution convenience, not a stronger security boundary; an embedding host remains trusted and same-origin with the widget unless a separate isolation architecture is used.
Definition of done
A developer can download one JavaScript file, add approximately 3–5 lines of HTML/JavaScript, and obtain a real BitLogin-backed Nostr signer without npm, Vite, React, or a backend. Existing standard-build consumers continue to work without source changes, and the compatibility and security claims above are backed by automated tests and release artifacts.
Context warnings to resolve
- Replace unresolved package references such as
@file:bitlogin/widget,@file:bitlogin/widget/standalone, and@file:bitlogin/corewith actual documented package paths or mark them as examples only; do not publish links to files or exports that do not exist.
BitBlocks App Kit compatibility layer
Do this only after BitLogin, BitGate, and BitFeed each have clean standalone APIs. BitBlocks is an optional integration layer, not a replacement for any of the three libraries. It must not merge their implementations or make an app depend on the kit when it only needs BitLogin.
-
Create a tiny optional BitBlocks integration layer.
-
Keep BitLogin, BitGate, and BitFeed as separate replaceable libraries; the App Kit must compose them rather than absorb them.
-
Provide shared initialization, for example:
const app = await BitBlocks.create({ login: {}, gate: {}, feed: {}, relays: [...] }); -
Standardize the viewer context exchanged between the components without changing BitLogin's signer/session contract.
-
Centralize optional common Nostr plumbing in the kit: relay connections, subscriptions, EOSE handling, reconnects, deduplication, timeouts, publishing, OK responses, profile lookup, follow-list lookup, and mute-list lookup.
-
Expose
app.viewer,app.signer,app.relays,app.gate, andapp.feedthrough documented interfaces. -
Keep each underlying library independently usable and independently versioned.
-
Do not make applications depend on BitBlocks when they use only BitLogin's identity/signing capabilities.
-
Add integration tests proving the kit is an adapter over the standalone APIs, not a second implementation of identity, policy, or feed logic.
BitBlocks definition of done
A developer or AI agent can take bitlogin.js, bitgate.js, bitfeed.js, and
bitblocks.js and build a functional static Nostr application with almost no
infrastructure or integration boilerplate, while each individual library
remains usable without BitBlocks.
Feedback from integrating into Tessera (2026-08-11)
Notes from embedding @bitlogin/widget in the Tessera static site. Grouped by
how much they cost.
High-value
- [DONE 2026-08-11] Lightweight session-state read without the full widget.
The single biggest cost: to know whether a user is signed in on a page, you
used to have to mount the whole widget (bitlogin.js + crypto worker + wasm,
~1.6MB) because it was the only thing that restored the per-origin session.
Shipped: the widget now writes a cleartext, public-only record
(
{ pubkey, npub, name? }— never key material) beside its encrypted session in IndexedDB and clears it on logout, and a new@bitlogin/widget/sessionentry point (bitlogin-session.js, ~1 KB gzip, zero deps, no worker/wasm) exposesreadCachedSession(): Promise<{ pubkey, npub, name? } | null>. The same call is mirrored onwindow.bitlogin.readCachedSession()for pages that already load the full widget. Seesrc/publicSession.ts,src/session.ts, and the README "Reading who's signed in without loading the widget" section. Remaining polish: have the widget also record the profile displaynameonce fetched (currentlynameis only set when the caller supplies it), and add an@bitlogin/widget/session.d.ts(tracked under the Small item below). - [DONE 2026-08-11] A headless session-restore API. Shipped
@bitlogin/widget/headless(bitlogin-headless.js):createHeadlessSigner()→restore()/getPublicKey()/signEvent()/nip44Encrypt|Decrypt/isUnlocked()/logout()/destroy(), a DOM-free wrapper over the crypto worker. It restores the cached session and signs with no custom element and nowindow.nostr; it deliberately can't interactively log in (restore()returns null when nothing is cached → mount<bitlogin-auth>once). Covered byheadless.test.ts(scripted-worker); also re-exported from@bitlogin/widget; README "Signing without the widget (headless)". Deploy alongsidecryptoWorker.js+ shared chunks.
Medium
- [DONE 2026-08-11] The unmount-kills-the-worker footgun now has a guardrail.
The docs warned that removing
<bitlogin-auth>tears down the crypto worker and hangs every later signing call with no error. Shipped both asks: (a)disconnectedCallbacknow logs aconsole.errorwhen the element is removed while a session is active, naming the unmount and pointing at the README "Integration gotcha"; and (b)WorkerClient.terminate(reason)records the reason andsignEvent/getPublicKey/etc. — whether in-flight at teardown or issued afterward — reject with that message instead of hanging (thedead-flag path already prevented the infinite hang; this makes the cause legible and unifies the message). Covered byworker/workerClient.test.ts; README gotcha paragraphs updated to say "rejects loudly," not "hangs forever." - [DONE 2026-08-11] Styling hooks / CSS parts. The widget renders in a
shadow root, so a host could recolor it via the
:hostcustom properties but couldn't restyle structure. Shipped: a stable set of::part()hooks —card,button(+button-primary/button-secondary),link,input,checkbox,heading,text,notice(+notice-warn/-info/-error) — stamped centrally per render inelement.tsexposeParts()(mapped by class so new elements stay exposed without drift). Documented as a contract in the README "Theming" section. Verified in-browser thatbitlogin-auth::part(...)matches through the shadow boundary. Custom properties remain the first tool for color/radius/font; parts are the escape hatch for structure (padding/border/shadow/shape). - [RESOLVED — already the behavior] Session-change event on restore.
Confirmed:
bitlogin-loginalready fires on silent session restore across a page load, not just interactive login, so a host listener needn't distinguish the two (README "Sessions persist across a page reload"). No new event needed. For pages that don't mount the widget at all, usereadCachedSession()above.
Small
- [DONE 2026-08-11] Ship a
.d.ts(or documented event/method surface) for the element. Shipped: the widget now emits type declarations from source (tsconfig.types.json→dist-types/, wired viabuild:typesand the packagetypes/exportsconditions for both@bitlogin/widgetand@bitlogin/widget/session) — zero-drift because they're generated, not hand-written. Added the type surface integrators actually want: aHTMLElementTagNameMap["bitlogin-auth"]augmentation (typedquerySelector/createElement), typedaddEventListeneroverloads via a declaration-merged interface, and exported event-detail types (BitLoginLoginEventDetailetc. +BitLoginEventMap) insrc/events.ts. Verified with a consumer conformance typecheck; README "TypeScript" section added.