widget: ship generated type declarations + typed element/events

Integrators had to read the demo to learn the API: the package built via
vite with no .d.ts and no types field, so @bitlogin/widget and
@bitlogin/widget/session resolved to `any`.

Emit declarations from source (tsconfig.types.json -> dist-types,
build:types wired into build) and point the package types/exports
conditions at them, for both the root and the /session entry. Generated,
so they can't drift from the implementation.

Add the type surface integrators actually want:
- HTMLElementTagNameMap["bitlogin-auth"], so querySelector/createElement
  return the typed element.
- Typed addEventListener/removeEventListener overloads (declaration-merged
  interface) for the bitlogin-* events.
- Exported event-detail types and BitLoginEventMap (src/events.ts), matching
  element.ts's dispatchEvent shapes.

Verified with a consumer conformance typecheck (typed detail with no cast,
typed element methods, readCachedSession return type). README gains a
"TypeScript" section.

Addresses the ".d.ts / documented API surface" 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 19:37:32 -04:00
1 parent 1b39e0f60c
commit 5ae41ed9db
7 files changed
+154 -6

No files matched your search

+27
View File
@@ -242,6 +242,33 @@ Nostr signer (browser extension) is active in this browser and couldn't be
replaced...") using the same warning banner already shown for rollback and
relay-disagreement conditions.
### TypeScript
The package ships type declarations (`@bitlogin/widget` and
`@bitlogin/widget/session`), generated from source so they can't drift.
Registering the widget augments the DOM types, so integrators get real
autocomplete instead of reading the demo:
```ts
import "@bitlogin/widget"; // registers <bitlogin-auth> and its types
const el = document.querySelector("bitlogin-auth"); // typed as BitLoginAuthElement
el?.addEventListener("bitlogin-login", (e) => {
// e.detail is typed: { publicKey?: string; method; capabilities }
console.log(e.detail.method, e.detail.capabilities.nip44);
});
await el?.getPublicKey(); // typed element methods
import { readCachedSession } from "@bitlogin/widget/session";
const who = await readCachedSession(); // { pubkey, npub, name? } | null
```
Exported types include `BitLoginAuthElement`, the `bitlogin-*`
`...EventDetail` shapes and `BitLoginEventMap`, `PublicSessionInfo`,
`SignerMethod`/`SignerCapabilities`, `BitLoginConfig`, and the
`window.bitlogin` (`BitLoginGlobal`) interface. Run `npm run build` (or
`build:types`) to emit them into `dist-types/`.
### Theming
`<bitlogin-auth>` renders in a shadow root, but its colors, radius, font, and
+12 -3
View File
@@ -416,6 +416,15 @@ how much they cost.
### Small
- Ship a `.d.ts` (or documented event/method surface) for the element:
`getPublicKey`, `signEvent`, `nip44Encrypt/Decrypt`, and the event
`detail` shapes. Integrators currently read the demo to learn the API.
- **[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 via `build:types` and the
package `types`/`exports` conditions for both `@bitlogin/widget` and
`@bitlogin/widget/session`) — zero-drift because they're generated, not
hand-written. Added the type surface integrators actually want: a
`HTMLElementTagNameMap["bitlogin-auth"]` augmentation (typed
`querySelector`/`createElement`), typed `addEventListener` overloads via a
declaration-merged interface, and exported event-detail types
(`BitLoginLoginEventDetail` etc. + `BitLoginEventMap`) in `src/events.ts`.
Verified with a consumer conformance typecheck; README "TypeScript" section
added.
+11 -3
View File
@@ -6,12 +6,20 @@
"type": "module",
"description": "BitLogin embeddable login widget: a <bitlogin-auth> custom element plus a window.nostr NIP-07-compatible provider, backed by a dedicated crypto Web Worker.",
"main": "./dist/bitlogin.js",
"types": "./dist-types/index.d.ts",
"exports": {
".": "./dist/bitlogin.js",
"./session": "./dist/bitlogin-session.js"
".": {
"types": "./dist-types/index.d.ts",
"default": "./dist/bitlogin.js"
},
"./session": {
"types": "./dist-types/session.d.ts",
"default": "./dist/bitlogin-session.js"
}
},
"scripts": {
"build": "vite build",
"build": "vite build && npm run build:types",
"build:types": "tsc -p tsconfig.types.json",
"dev": "vite build --watch",
"typecheck": "tsc --noEmit"
},
+37
View File
@@ -20,6 +20,7 @@ import { Nip46Signer } from "./signers/nip46.js";
import type { Signer, SignerCapabilities, SignerMethod } from "./signers/types.js";
import { renderQrSvg } from "./qr.js";
import { setActiveSession, clearActiveSession } from "./globalSession.js";
import type { BitLoginEventMap } from "./events.js";
import {
buildPasskeyCreateOptions,
buildPasskeyGetOptions,
@@ -2217,3 +2218,39 @@ function pickRandomIndices(max: number, count: number): number[] {
function escapeHtml(input: string): string {
return input.replace(/[&<>"']/gu, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]!);
}
// Declaration merge: give BitLoginAuthElement typed addEventListener/
// removeEventListener overloads for its own bitlogin-* events, so a host gets
// `e.detail` typed without a cast. The string overload preserves the base
// signature for every other event. Merging an interface of the same name adds
// these to the class type; the runtime implementation stays HTMLElement's.
export interface BitLoginAuthElement {
addEventListener<K extends keyof BitLoginEventMap>(
type: K,
listener: (this: BitLoginAuthElement, ev: BitLoginEventMap[K]) => void,
options?: boolean | AddEventListenerOptions
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void;
removeEventListener<K extends keyof BitLoginEventMap>(
type: K,
listener: (this: BitLoginAuthElement, ev: BitLoginEventMap[K]) => void,
options?: boolean | EventListenerOptions
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void;
}
declare global {
interface HTMLElementTagNameMap {
// So document.querySelector("bitlogin-auth") and
// document.createElement("bitlogin-auth") are typed as the element.
"bitlogin-auth": BitLoginAuthElement;
}
}
+44
View File
@@ -0,0 +1,44 @@
/**
* Public event contract for `<bitlogin-auth>`. These are the CustomEvents the
* element dispatches on itself; the detail shapes here match element.ts's
* `dispatchEvent` calls exactly. Integrators previously had to read the demo to
* learn them — the typed `addEventListener` overloads on BitLoginAuthElement
* (see element.ts) now surface them in editors.
*/
import type { SignerMethod, SignerCapabilities } from "./signers/types.js";
/** `bitlogin-login` — a session was established OR silently restored on page
* load (the two are intentionally indistinguishable to a listener). */
export interface BitLoginLoginEventDetail {
/** The signed-in account's public key (hex), when known. */
publicKey?: string;
/** How the session was established. */
method: SignerMethod;
/** What the active signer supports (NIP-07 extensions vary). */
capabilities: SignerCapabilities;
}
/** `bitlogin-connection-granted` — the user shared an NWC wallet connection with
* an origin (Connection Vault, reveal mode). */
export interface BitLoginConnectionGrantedEventDetail {
origin: string;
}
/** `bitlogin-signer-claimed` — the element tried to install itself as
* `window.nostr`; `windowNostrClaimed` says whether it won the slot. */
export interface BitLoginSignerClaimedEventDetail {
windowNostrClaimed: boolean;
error?: string;
}
/** Maps each `bitlogin-*` event name to its concrete event type. Events without
* a detail payload are plain `CustomEvent<undefined>`. */
export interface BitLoginEventMap {
"bitlogin-login": CustomEvent<BitLoginLoginEventDetail>;
"bitlogin-logout": CustomEvent<undefined>;
"bitlogin-connection-granted": CustomEvent<BitLoginConnectionGrantedEventDetail>;
"bitlogin-signer-claimed": CustomEvent<BitLoginSignerClaimedEventDetail>;
"bitlogin-signer-released": CustomEvent<undefined>;
"bitlogin-request-pending": CustomEvent<undefined>;
"bitlogin-offer-pending": CustomEvent<undefined>;
}
+7
View File
@@ -96,6 +96,13 @@ window.bitlogin = {
export { readCachedSession } from "./session.js";
export type { PublicSessionInfo } from "./publicSession.js";
export { BitLoginAuthElement } from "./element.js";
export type {
BitLoginEventMap,
BitLoginLoginEventDetail,
BitLoginConnectionGrantedEventDetail,
BitLoginSignerClaimedEventDetail
} from "./events.js";
export type { SignerMethod, SignerCapabilities } from "./signers/types.js";
export { WorkerClient } from "./worker/workerClient.js";
export { createNip07Provider } from "./provider.js";
export type { BitLoginConfig } from "./config.js";
+16
View File
@@ -0,0 +1,16 @@
{
"//": "Emits public .d.ts for @bitlogin/widget (and /session) into dist-types. Kept separate from tsconfig.json (which is noEmit typecheck-only). Tests and the e2e tree are excluded so they don't ship as declarations.",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist-types",
"rootDir": "src",
"noEmit": false,
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": false,
"sourceMap": false,
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src"],
"exclude": ["src/e2e", "**/*.test.ts"]
}