Files
nostr_login_lite/NOSTR_LOGIN_LITE.md
Your Name 37fb89c0a9 first
2025-09-09 09:32:09 -04:00

318 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 projects 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:
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
- 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<string>](lite/nostr-login-lite.js:1)
- Await unsolicited reply to local pubkey; accept "ack" or exact secret; return signers 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<string>](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<any>](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<string>](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<string>](lite/nostr-login-lite.js:1)
- Use [NostrTools.nip04.encrypt()](lite/nostr-login-lite.js:1).
- [ ] [LocalSigner.decrypt04(pubkey: string, ciphertext: string): Promise<string>](lite/nostr-login-lite.js:1)
- Use [NostrTools.nip04.decrypt()](lite/nostr-login-lite.js:1).
- [ ] [LocalSigner.encrypt44(pubkey: string, plaintext: string): Promise<string>](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<string>](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<string>](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:
- <script src="..."></script> 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.