This commit is contained in:
Your Name
2025-09-09 09:32:09 -04:00
commit 37fb89c0a9
135 changed files with 36437 additions and 0 deletions

318
NOSTR_LOGIN_LITE.md Normal file
View File

@@ -0,0 +1,318 @@
# 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.