# NOSTR_LOGIN_LITE Objective - Deliver a minimal, dependency-light replacement for the current auth/UI stack that: - Preserves all login methods: Nostr Connect (nip46), Extension, Local key, Read-only, OTP/DM. - Exposes the same window.nostr surface: [getPublicKey()](lite/nostr-login-lite.js:1), [signEvent(event)](lite/nostr-login-lite.js:1), [nip04.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1), [nip04.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1), [nip44.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1), [nip44.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1). - Dispatches identical nlAuth/nlLaunch/nlLogout/nlDarkMode events and accepts setAuth calls for compatibility. Key differences vs current project - UI: replace Stencil/Tailwind component library with a single vanilla-JS modal and minimal CSS. - Transport: remove NDK; implement NIP-46 RPC using nostr-tools SimplePool. - Crypto: rely on nostr-tools (via CDN global window.NostrTools) for keygen/signing (finalizeEvent), nip04, nip19, and SimplePool; embed a small NIP-44 codec if window.NostrTools.nip44 is not available. The current project’s codec is in [packages/auth/src/utils/nip44.ts](packages/auth/src/utils/nip44.ts). Nostr Tools via CDN (global window.NostrTools) - Include the bundle once in the page: - Access APIs via window.NostrTools inside the lite build: - [NostrTools.generateSecretKey()](lite/nostr-login-lite.js:1) or [NostrTools.generatePrivateKey()](lite/nostr-login-lite.js:1) (depending on CDN version) - [NostrTools.getPublicKey()](lite/nostr-login-lite.js:1) - [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1) - [NostrTools.nip04](lite/nostr-login-lite.js:1) encrypt/decrypt - [NostrTools.nip19](lite/nostr-login-lite.js:1) npub/nsec encode/decode - [NostrTools.SimplePool](lite/nostr-login-lite.js:1) relay connectivity Supported login methods and crypto summary - Connect (nip46): secp256k1 Schnorr for event signing/hash; nip44 (ChaCha20/HKDF/HMAC) for RPC payloads, with nip04 fallback. Requires relay connectivity (via [NostrTools.SimplePool](lite/nostr-login-lite.js:1)). - Extension: crypto handled by the extension; we bridge calls. - Local key: secp256k1 Schnorr signing ([NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1)) and nip04/nip44 encrypt/decrypt locally. - Read-only: no client crypto. - OTP/DM: no client crypto beyond state persistence; server sends DM and verifies via HTTP. Minimal file layout - Single file drop-in (script tag friendly): - [lite/nostr-login-lite.js](lite/nostr-login-lite.js) - Optional split during development (bundled later): - [lite/core/nostr-lite.js](lite/core/nostr-lite.js) - [lite/core/nip46-client.js](lite/core/nip46-client.js) - [lite/core/extension-bridge.js](lite/core/extension-bridge.js) - [lite/core/store.js](lite/core/store.js) - [lite/ui/modal.js](lite/ui/modal.js) External dependencies strategy - Use the CDN bundle to provide [window.NostrTools](lite/nostr-login-lite.js:1) at runtime. No npm installs required. - Prefer built-ins from window.NostrTools: - Keys/sign: [generateSecretKey()](lite/nostr-login-lite.js:1)/[generatePrivateKey()](lite/nostr-login-lite.js:1), [getPublicKey()](lite/nostr-login-lite.js:1), [finalizeEvent()](lite/nostr-login-lite.js:1). - Encoding: [nip19](lite/nostr-login-lite.js:1). - Encryption: [nip04](lite/nostr-login-lite.js:1). - Relays: [SimplePool](lite/nostr-login-lite.js:1). - Nip44 choice: - If window.NostrTools.nip44 is available, use it directly. - Otherwise embed the existing lightweight codec adapted from [packages/auth/src/utils/nip44.ts](packages/auth/src/utils/nip44.ts) as [Nip44](lite/nostr-login-lite.js:1). Compatibility requirements - Provide the same exports: [init(opts)](lite/nostr-login-lite.js:1), [launch(startScreen)](lite/nostr-login-lite.js:1), [logout()](lite/nostr-login-lite.js:1), [setDarkMode(dark)](lite/nostr-login-lite.js:1), [setAuth(o)](lite/nostr-login-lite.js:1), [cancelNeedAuth()](lite/nostr-login-lite.js:1). - Dispatch identical events and payload shapes as in [onAuth()](packages/auth/src/modules/AuthNostrService.ts:347). Architecture overview - NostrLite: window.nostr facade mirroring current behavior, invokes auth UI when needed. - Auth: manages methods (connect/extension/local/readOnly/otp), state, storage, event dispatch. - NIP46Client: minimal RPC over [NostrTools.SimplePool](lite/nostr-login-lite.js:1) (subscribe, send request, parse/decrypt, dedupe auth_url). - ExtensionBridge: safely handle window.nostr detection, guard against overwrites, switch to extension mode when requested. - Store: localStorage helpers for accounts/current/recents and misc values. - UI: single vanilla modal that lists options and drives flow, with optional inline iframe starter for providers that publish iframe_url. Function-level TODO checklist (execution order) Bootstrap and API surface - [ ] [Deps.ensureNostrToolsLoaded()](lite/nostr-login-lite.js:1) - Verify window.NostrTools presence; throw with actionable message if missing. - [ ] [NostrLite.init(options: NostrLoginOptions)](lite/nostr-login-lite.js:1) - Call [Deps.ensureNostrToolsLoaded()](lite/nostr-login-lite.js:1). - Bind window.nostr to the facade. - Initialize ExtensionBridge checking loop. - Mount modal UI, persist options (theme, perms, bunkers, methods, otp URLs, default relays). - Wire nlLaunch/nlLogout/nlDarkMode/nlSetAuth listeners. - [ ] [NostrLite.launch(startScreen?: string)](lite/nostr-login-lite.js:1) - Open modal with selected start screen or switch-account if accounts exist. - [ ] [NostrLite.logout()](lite/nostr-login-lite.js:1) - Clear current account; dispatch nlAuth logout; reset signer/client state. - [ ] [NostrLite.setDarkMode(dark: boolean)](lite/nostr-login-lite.js:1) - Persist and apply theme to modal. - [ ] [NostrLite.setAuth(o: NostrLoginAuthOptions)](lite/nostr-login-lite.js:1) - Validate o.type in {login, signup, logout}; o.method in {connect, extension, local, otp, readOnly}. - Delegate to [Auth.switchAccount()](lite/nostr-login-lite.js:1); dispatch consistent nlAuth. - [ ] [NostrLite.cancelNeedAuth()](lite/nostr-login-lite.js:1) - Cancel current connect/listen flow and close modal section if waiting. window.nostr facade - [ ] [NostrLite.ensureAuth()](lite/nostr-login-lite.js:1) - If no user, open modal; await until authenticated or rejected. - [ ] [NostrLite.getPublicKey()](lite/nostr-login-lite.js:1) - Ensure auth, return current pubkey or throw. - [ ] [NostrLite.signEvent(event: NostrEvent)](lite/nostr-login-lite.js:1) - Ensure auth; if local key then sign locally via [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1); else sign via [NIP46Client.sendRequest()](lite/nostr-login-lite.js:1). - [ ] [NostrLite.nip04.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1) - [ ] [NostrLite.nip04.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1) - Ensure auth; route to local ([NostrTools.nip04](lite/nostr-login-lite.js:1)) or remote signer via NIP-46 nip04_{encrypt|decrypt}. - [ ] [NostrLite.nip44.encrypt(pubkey, plaintext)](lite/nostr-login-lite.js:1) - [ ] [NostrLite.nip44.decrypt(pubkey, ciphertext)](lite/nostr-login-lite.js:1) - Ensure auth; use window.NostrTools.nip44 if present or local [Nip44](lite/nostr-login-lite.js:1); remote via nip44_{encrypt|decrypt}. Auth module - [ ] [Auth.startAuthSession()](lite/nostr-login-lite.js:1) - Create a promise used by ensureAuth/waitReady; allows Connect flow to resolve or be cancelled. - [ ] [Auth.endAuthSession()](lite/nostr-login-lite.js:1) - Resolve session; if iframeUrl present, bind iframe port to NIP46Client before resolving. - [ ] [Auth.resetAuthSession()](lite/nostr-login-lite.js:1) - Cancel current session safely. - [ ] [Auth.onAuth(type: "login" | "signup" | "logout", info?: Info)](lite/nostr-login-lite.js:1) - Update state, persist, dispatch nlAuth; fetch profile asynchronously and update name/picture when fetched; preserve nip05 semantics. - [ ] [Auth.switchAccount(info: Info, signup = false)](lite/nostr-login-lite.js:1) - Branch to [Auth.setReadOnly()](lite/nostr-login-lite.js:1)/[Auth.setOTP()](lite/nostr-login-lite.js:1)/[Auth.setLocal()](lite/nostr-login-lite.js:1)/[Auth.trySetExtensionForPubkey()](lite/nostr-login-lite.js:1)/[Auth.setConnect()](lite/nostr-login-lite.js:1). - [ ] [Auth.setReadOnly(pubkey: string)](lite/nostr-login-lite.js:1) - [ ] [Auth.setOTP(pubkey: string, data: string)](lite/nostr-login-lite.js:1) - [ ] [Auth.localSignup(name: string, sk?: string)](lite/nostr-login-lite.js:1) - Generate sk via [NostrTools.generateSecretKey()](lite/nostr-login-lite.js:1)/[generatePrivateKey()](lite/nostr-login-lite.js:1) if missing; [Auth.setLocal(signup=true)](lite/nostr-login-lite.js:1). - [ ] [Auth.setLocal(info: Info, signup?: boolean)](lite/nostr-login-lite.js:1) - Instantiate [LocalSigner](lite/nostr-login-lite.js:1); call [Auth.onAuth()](lite/nostr-login-lite.js:1). - [ ] [Auth.trySetExtensionForPubkey(pubkey: string)](lite/nostr-login-lite.js:1) - Use [ExtensionBridge.setExtensionReadPubkey()](lite/nostr-login-lite.js:1) and reconcile. - [ ] [Auth.setConnect(info: Info)](lite/nostr-login-lite.js:1) - Initialize NIP46Client with existing token/relays via [NIP46Client.init()](lite/nostr-login-lite.js:1) and [NIP46Client.connect()](lite/nostr-login-lite.js:1); call [Auth.onAuth()](lite/nostr-login-lite.js:1); [Auth.endAuthSession()](lite/nostr-login-lite.js:1). - [ ] [Auth.authNip46(type, { name, bunkerUrl, sk, domain, iframeUrl })](lite/nostr-login-lite.js:1) - Parse bunkerUrl/nip05; [NIP46Client.init()](lite/nostr-login-lite.js:1) + [NIP46Client.connect()](lite/nostr-login-lite.js:1); [Auth.onAuth()](lite/nostr-login-lite.js:1). - [ ] [Auth.nostrConnect(relay?: string, opts?: { domain?: string; link?: string; importConnect?: boolean; iframeUrl?: string })](lite/nostr-login-lite.js:1) - Create local keypair and secret; open provider link or show iframe starter; [NIP46Client.listen()](lite/nostr-login-lite.js:1) to learn signerPubkey, then [NIP46Client.initUserPubkey()](lite/nostr-login-lite.js:1); set bunkerUrl; [Auth.onAuth()](lite/nostr-login-lite.js:1) unless importConnect. - [ ] [Auth.createNostrConnect(relay?: string)](lite/nostr-login-lite.js:1) - Build nostrconnect:// URL with meta (icon/url/name/perms). - [ ] [Auth.getNostrConnectServices(): Promise<[string, ConnectionString[]]>](lite/nostr-login-lite.js:1) - Return nostrconnect URL and preconfigured providers; query .well-known/nostr.json for relays and iframe_url. - [ ] [Auth.importAndConnect(cs: ConnectionString)](lite/nostr-login-lite.js:1) - nostrConnect(..., importConnect=true) → logout keep signer → onAuth(login, connect info). - [ ] [Auth.createAccount(nip05: string)](lite/nostr-login-lite.js:1) (optional parity) - Use bunker provider to create account; return bunkerUrl and sk. NIP46Client (transport over NostrTools.SimplePool) - [ ] [NIP46Client.init(localSk: string, remotePubkey?: string, relays: string[], iframeOrigin?: string)](lite/nostr-login-lite.js:1) - Create [this.pool = new NostrTools.SimplePool()](lite/nostr-login-lite.js:1). - Cache relays; initialize local signer state (seckey, pubkey via [NostrTools.getPublicKey()](lite/nostr-login-lite.js:1)). - If remotePubkey provided, set it; cache iframeOrigin. - [ ] [NIP46Client.setUseNip44(use: boolean)](lite/nostr-login-lite.js:1) - [ ] [NIP46Client.subscribeReplies()](lite/nostr-login-lite.js:1) - sub = pool.sub(relays, [{ kinds:[24133], '#p':[localPubkey] }]) - sub.on('event', (ev) => [NIP46Client.onEvent(ev)](lite/nostr-login-lite.js:1)); sub.on('eose', () => {/* keep alive */}) - [ ] [NIP46Client.onEvent(ev: NostrEvent)](lite/nostr-login-lite.js:1) - Parse/decrypt via [NIP46Client.parseEvent()](lite/nostr-login-lite.js:1); route to response handlers. - [ ] [NIP46Client.listen(nostrConnectSecret: string): Promise](lite/nostr-login-lite.js:1) - Await unsolicited reply to local pubkey; accept "ack" or exact secret; return signer’s pubkey; unsubscribe this one-shot subscription. - [ ] [NIP46Client.connect(token?: string, perms?: string)](lite/nostr-login-lite.js:1) - [NIP46Client.sendRequest()](lite/nostr-login-lite.js:1) with method "connect" and params [userPubkey, token||'', perms||'']; resolve on ack. - [ ] [NIP46Client.initUserPubkey(hint?: string): Promise](lite/nostr-login-lite.js:1) - If hint present, set and return; else call "get_public_key" RPC and store result. - [ ] [NIP46Client.sendRequest(remotePubkey: string, method: string, params: string[], kind = 24133, cb?: (res) => void)](lite/nostr-login-lite.js:1) - id = random string - ev = [NIP46Client.createRequestEvent(id, remotePubkey, method, params, kind)](lite/nostr-login-lite.js:1) - pool.publish(relays, ev) - [NIP46Client.setResponseHandler(id, cb)](lite/nostr-login-lite.js:1) - [ ] [NIP46Client.createRequestEvent(id, remotePubkey, method, params, kind)](lite/nostr-login-lite.js:1) - content = JSON.stringify({ id, method, params }) - encrypt content using nip44 unless method==='create_account', otherwise nip04 fallback - tags: [ ['p', remotePubkey] ] - sign via [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1) - return event object - [ ] [NIP46Client.parseEvent(event)](lite/nostr-login-lite.js:1) - Detect nip04 vs nip44 by ciphertext shape; decrypt using local seckey and remote pubkey (nip04 or [Nip44](lite/nostr-login-lite.js:1)/window.NostrTools.nip44). - Return { id, method, params, event } or { id, result, error, event }. - [ ] [NIP46Client.setResponseHandler(id, cb)](lite/nostr-login-lite.js:1) - Deduplicate 'auth_url' emissions; ensure only one resolution per id; log elapsed; cleanup map entries. - [ ] [NIP46Client.setWorkerIframePort(port: MessagePort)](lite/nostr-login-lite.js:1) - When iframe is present, forward signed events via postMessage and map request id↔event.id; send keepalive pings. - [ ] [NIP46Client.teardown()](lite/nostr-login-lite.js:1) - Unsubscribe; clear timers/ports; pool.close(relays). Iframe handshake - [ ] [IframeReadyListener.start(messages: string[], origin: string)](lite/nostr-login-lite.js:1) - Listen for ["workerReady","workerError"] and ["starterDone","starterError"]; origin-check by host or subdomain. - [ ] [IframeReadyListener.wait(): Promise](lite/nostr-login-lite.js:1) ExtensionBridge - [ ] [ExtensionBridge.startChecking(nostrLite: NostrLite)](lite/nostr-login-lite.js:1) - Poll window.nostr until found; then call [ExtensionBridge.initExtension()](lite/nostr-login-lite.js:1) once; retry after a delay to capture last extension. - [ ] [ExtensionBridge.initExtension(nostrLite: NostrLite, lastTry?: boolean)](lite/nostr-login-lite.js:1) - Cache extension, reassign window.nostr to nostrLite; if currently authed as extension, reconcile; schedule a final late check. - [ ] [ExtensionBridge.setExtensionReadPubkey(expectedPubkey?: string)](lite/nostr-login-lite.js:1) - Temporarily set window.nostr = extension; read pubkey; compare to expected; emit extensionLogin/extensionLogout. - [ ] [ExtensionBridge.trySetForPubkey(expectedPubkey: string)](lite/nostr-login-lite.js:1) - [ ] [ExtensionBridge.setExtension()](lite/nostr-login-lite.js:1) - [ ] [ExtensionBridge.unset(nostrLite: NostrLite)](lite/nostr-login-lite.js:1) - [ ] [ExtensionBridge.hasExtension(): boolean](lite/nostr-login-lite.js:1) Local signer (wrapping window.NostrTools) - [ ] [LocalSigner.constructor(sk: string)](lite/nostr-login-lite.js:1) - Cache public key on construct via [NostrTools.getPublicKey()](lite/nostr-login-lite.js:1). - [ ] [LocalSigner.pubkey(): string](lite/nostr-login-lite.js:1) - [ ] [LocalSigner.sign(event: NostrEvent): Promise](lite/nostr-login-lite.js:1) - Use [NostrTools.finalizeEvent()](lite/nostr-login-lite.js:1), set id/sig/pubkey. - [ ] [LocalSigner.encrypt04(pubkey: string, plaintext: string): Promise](lite/nostr-login-lite.js:1) - Use [NostrTools.nip04.encrypt()](lite/nostr-login-lite.js:1). - [ ] [LocalSigner.decrypt04(pubkey: string, ciphertext: string): Promise](lite/nostr-login-lite.js:1) - Use [NostrTools.nip04.decrypt()](lite/nostr-login-lite.js:1). - [ ] [LocalSigner.encrypt44(pubkey: string, plaintext: string): Promise](lite/nostr-login-lite.js:1) - Use window.NostrTools.nip44 if available, else [Nip44.encrypt()](lite/nostr-login-lite.js:1). - [ ] [LocalSigner.decrypt44(pubkey: string, ciphertext: string): Promise](lite/nostr-login-lite.js:1) - Use window.NostrTools.nip44 if available, else [Nip44.decrypt()](lite/nostr-login-lite.js:1). Store (localStorage helpers) - [ ] [Store.addAccount(info: Info)](lite/nostr-login-lite.js:1) - [ ] [Store.removeCurrentAccount()](lite/nostr-login-lite.js:1) - [ ] [Store.getCurrent(): Info | null](lite/nostr-login-lite.js:1) - [ ] [Store.getAccounts(): Info[]](lite/nostr-login-lite.js:1) - [ ] [Store.getRecents(): Info[]](lite/nostr-login-lite.js:1) - [ ] [Store.setItem(key: string, value: string)](lite/nostr-login-lite.js:1) - [ ] [Store.getIcon(): Promise](lite/nostr-login-lite.js:1) UI (single modal) - [ ] [Modal.init(options)](lite/nostr-login-lite.js:1) - Create modal container; inject minimal CSS; set theme and RTL if required. - [ ] [Modal.open(opts: { startScreen?: string })](lite/nostr-login-lite.js:1) - Render options: Connect (with provider list/QR), Extension, Local (Create/Import), Read-only, OTP (if server URLs configured), Switch Account if prior accounts. - [ ] [Modal.close()](lite/nostr-login-lite.js:1) - [ ] [Modal.showAuthUrl(url: string)](lite/nostr-login-lite.js:1) - For connect OAuth-like prompt; present link/QR; if iframeUrl is used, show embedded iframe. - [ ] [Modal.showIframeUrl(url: string)](lite/nostr-login-lite.js:1) - [ ] [Modal.onSwitchAccount(info: Info)](lite/nostr-login-lite.js:1) - [ ] [Modal.onLogoutConfirm()](lite/nostr-login-lite.js:1) - [ ] [Modal.onImportConnectionString(cs: ConnectionString)](lite/nostr-login-lite.js:1) - Import-and-connect flow: calls [Auth.importAndConnect()](lite/nostr-login-lite.js:1). Event bus - [ ] [Bus.on(event: string, handler: Function)](lite/nostr-login-lite.js:1) - [ ] [Bus.off(event: string, handler: Function)](lite/nostr-login-lite.js:1) - [ ] [Bus.emit(event: string, payload?: any)](lite/nostr-login-lite.js:1) Relay configuration helpers - [ ] [Relays.getDefaultRelays(options): string[]](lite/nostr-login-lite.js:1) - From options or sensible defaults. - [ ] [Relays.normalize(relays: string[]): string[]](lite/nostr-login-lite.js:1) - Ensure proper wss:// and deduping. Parity acceptance criteria - Same external API names and events exported as current initializer: [init](packages/auth/src/index.ts:333), [launch](packages/auth/src/index.ts:333), [logout](packages/auth/src/index.ts:333), [setDarkMode](packages/auth/src/index.ts:333), [setAuth](packages/auth/src/index.ts:333), [cancelNeedAuth](packages/auth/src/index.ts:333). - Same nlAuth payload shape as [onAuth()](packages/auth/src/modules/AuthNostrService.ts:347-418). - window.nostr behavior matches [class Nostr](packages/auth/src/modules/Nostr.ts:23). Notes and implementation tips - Load order: [Deps.ensureNostrToolsLoaded()](lite/nostr-login-lite.js:1) must guard all usages of window.NostrTools. - Request de-duplication: mirror [setResponseHandler()](packages/auth/src/modules/Nip46.ts:150-173) behavior to avoid auth_url flooding. - NIP-44 selection: use nip44 for all methods except "create_account". - Iframe origin checks: follow [ReadyListener](packages/auth/src/modules/Nip46.ts:291-338) host/subdomain verification. - Secret handling in nostrconnect: accept 'ack' or exact secret; see [listen](packages/auth/src/modules/Nip46.ts:71-107). - Profile fetch is optional; keep it async and non-blocking, update UI/state when complete. Out of scope for initial lite version - Complex banners/popups and multi-window flows. - Full NDK feature parity beyond the minimum for NIP-46 RPC. - Advanced error telemetry; keep console logs minimal. Deliverables - Single distributable [lite/nostr-login-lite.js](lite/nostr-login-lite.js) usable via: - with window.nostr present after [init()](lite/nostr-login-lite.js:1). - Optional ESM import with same API. - Minimal HTML example showing each auth method path is functional.