widget: headless signer — restore + sign without the custom element

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
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-08-11 22:54:14 -04:00
1 parent 5ae41ed9db
commit b8b19599e0
7 files changed
+245 -5

No files matched your search

+24
View File
@@ -128,6 +128,30 @@ call is also on the full widget as `window.bitlogin.readCachedSession()`, so
pages that already load `bitlogin.js` can use it without a second import.
Deploy `bitlogin-session.js` alongside `bitlogin.js`.
**Signing without the widget (headless).** `readCachedSession` reads identity
but can't sign. When a host needs to *sign* programmatically — a background job,
a framework rendering its own account UI, a headless test — and doesn't want the
custom element, use the headless signer. It runs the same crypto worker without
any DOM:
```javascript
import { createHeadlessSigner } from "@bitlogin/widget/headless";
const signer = await createHeadlessSigner();
const who = await signer.restore(); // { pubkey } | null (null → no cached session)
if (who) {
const event = await signer.signEvent({ kind: 1, content: "gm", tags: [] });
const pubkey = await signer.getPublicKey();
// also: nip44Encrypt / nip44Decrypt / isUnlocked / logout / destroy
}
```
It deliberately can't *log in* (that needs the password UI): `restore()` returns
null when no session is cached, at which point mount `<bitlogin-auth>` once so
the user signs in — after that the headless signer works across reloads from the
cached session. Deploy `bitlogin-headless.js` next to `cryptoWorker.js` and the
shared chunks, same as `bitlogin.js`.
The `<bitlogin-auth>` element also mirrors `window.nostr` directly on the
element instance (`getPublicKey`, `signEvent`, `nip44Encrypt`, `nip44Decrypt`)
— prefer this over `window.nostr` when your host page holds a reference to