diff --git a/src/lib/auth.svelte.ts b/src/lib/auth.svelte.ts index 38e0f59..e792844 100644 --- a/src/lib/auth.svelte.ts +++ b/src/lib/auth.svelte.ts @@ -1,5 +1,8 @@ import type { NostrUser } from "@nostr/gadgets/metadata"; import type { WindowNostr } from "@nostr/tools/nip07"; +import type { EventTemplate, VerifiedEvent } from "@nostr/tools/core"; +import * as nip19 from "@nostr/tools/nip19"; +import { finalizeEvent, getPublicKey } from "@nostr/tools/pure"; declare global { interface Window { @@ -7,8 +10,14 @@ declare global { } } +export type Signer = { + getPublicKey(): Promise; + signEvent(template: EventTemplate): Promise; +}; + let user = $state(null); -let signer = $state(null); +let signer = $state(null); +let loginModalOpen = $state(false); export const auth = { get user() { @@ -17,34 +26,107 @@ export const auth = { get signer() { return signer; }, + get loginModalOpen() { + return loginModalOpen; + }, }; const PUBKEY_KEY = "nostr_pubkey"; +const METHOD_KEY = "nostr_login_method"; +const NSEC_KEY = "nostr_nsec"; -export async function login() { +export function openLogin() { + loginModalOpen = true; +} + +export function closeLogin() { + loginModalOpen = false; +} + +function makeNsecSigner(secretKey: Uint8Array): Signer { + const pubkey = getPublicKey(secretKey); + return { + async getPublicKey() { + return pubkey; + }, + async signEvent(template) { + return finalizeEvent(template, secretKey); + }, + }; +} + +async function setUser(pubkey: string) { + const { loadNostrUser } = await import("@nostr/gadgets/metadata"); + user = await loadNostrUser(pubkey); +} + +export async function loginWithExtension() { if (!window.nostr) { - alert( - "No Nostr extension found. Please install a Nostr extension like nos2x.", - ); - return; + throw new Error("No Nostr extension found"); } const pubkey = await window.nostr.getPublicKey(); signer = window.nostr; localStorage.setItem(PUBKEY_KEY, pubkey); - const { loadNostrUser } = await import("@nostr/gadgets/metadata"); - user = await loadNostrUser(pubkey); + localStorage.setItem(METHOD_KEY, "extension"); + localStorage.removeItem(NSEC_KEY); + await setUser(pubkey); +} + +function parseSecretKey(input: string): Uint8Array { + const trimmed = input.trim(); + if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { + const sk = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + sk[i] = parseInt(trimmed.slice(i * 2, i * 2 + 2), 16); + } + return sk; + } + let decoded; + try { + decoded = nip19.decode(trimmed); + } catch { + throw new Error("Invalid nsec"); + } + if (decoded.type !== "nsec") throw new Error("Invalid nsec"); + return decoded.data; +} + +export async function loginWithNsec(input: string) { + const sk = parseSecretKey(input); + const nsec = nip19.nsecEncode(sk); + const pubkey = getPublicKey(sk); + signer = makeNsecSigner(sk); + localStorage.setItem(PUBKEY_KEY, pubkey); + localStorage.setItem(METHOD_KEY, "nsec"); + localStorage.setItem(NSEC_KEY, nsec); + await setUser(pubkey); } export function logout() { user = null; signer = null; localStorage.removeItem(PUBKEY_KEY); + localStorage.removeItem(METHOD_KEY); + localStorage.removeItem(NSEC_KEY); } export async function restoreSession() { const pubkey = localStorage.getItem(PUBKEY_KEY); if (!pubkey) return; - signer = window.nostr ?? null; - const { loadNostrUser } = await import("@nostr/gadgets/metadata"); - user = await loadNostrUser(pubkey); + const method = localStorage.getItem(METHOD_KEY) ?? "extension"; + + if (method === "nsec") { + const nsec = localStorage.getItem(NSEC_KEY); + if (!nsec) return; + try { + const decoded = nip19.decode(nsec); + if (decoded.type !== "nsec") return; + signer = makeNsecSigner(decoded.data); + } catch { + return; + } + } else { + signer = window.nostr ?? null; + } + await setUser(pubkey); } diff --git a/src/lib/components/LeftSidebar.svelte b/src/lib/components/LeftSidebar.svelte index 7c24983..591455f 100644 --- a/src/lib/components/LeftSidebar.svelte +++ b/src/lib/components/LeftSidebar.svelte @@ -1,6 +1,6 @@ + + + +{#if auth.loginModalOpen} +
+ + +
+{/if} diff --git a/src/lib/thread.svelte.ts b/src/lib/thread.svelte.ts index 0576e5b..1dc40f4 100644 --- a/src/lib/thread.svelte.ts +++ b/src/lib/thread.svelte.ts @@ -2,6 +2,7 @@ import { SimplePool } from "@nostr/tools"; import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; import { RELAY_URL, GROUP_ID } from "$lib/config"; import { threads as mockThreads } from "$lib/mock"; +import { auth } from "$lib/auth.svelte"; const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id); @@ -98,7 +99,7 @@ export async function loadThread(id: string) { export async function sendReply(content: string, ownPubkey: string) { if (!detail) throw new Error("No thread loaded"); - if (!window.nostr) throw new Error("No Nostr extension found"); + if (!auth.signer) throw new Error("Not logged in"); // Use last 3 events not authored by us as previous refs (NIP-29) const previousRefs = [...detail.replies, detail.op] @@ -114,7 +115,7 @@ export async function sendReply(content: string, ownPubkey: string) { if (previousRefs.length > 0) tags.push(["previous", ...previousRefs]); console.log("[reply] signing event…"); - const signed = await window.nostr.signEvent({ + const signed = await auth.signer.signEvent({ kind: 1111, created_at: Math.floor(Date.now() / 1000), tags, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c2223e6..09c0421 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,7 @@ import Navbar from "$lib/components/Navbar.svelte"; import LeftSidebar from "$lib/components/LeftSidebar.svelte"; import ChatSidebar from "$lib/components/ChatSidebar.svelte"; + import LoginModal from "$lib/components/LoginModal.svelte"; import { page } from "$app/state"; import { onMount } from "svelte"; import { restoreSession } from "$lib/auth.svelte"; @@ -45,3 +46,5 @@ {/if} + + diff --git a/src/routes/thread/[id]/+page.svelte b/src/routes/thread/[id]/+page.svelte index b90e0c6..12eca35 100644 --- a/src/routes/thread/[id]/+page.svelte +++ b/src/routes/thread/[id]/+page.svelte @@ -7,7 +7,7 @@ sendReply, type PostData, } from "$lib/thread.svelte"; - import { auth, login } from "$lib/auth.svelte"; + import { auth, openLogin } from "$lib/auth.svelte"; import Reactions from "$lib/components/Reactions.svelte"; import ThreadScrubber from "$lib/components/ThreadScrubber.svelte"; import type { NostrUser } from "@nostr/gadgets/metadata"; @@ -200,7 +200,7 @@ {:else}

To participate and reply, please -