import { Buffer } from "buffer"; import { browser } from "$app/environment"; import { decrypted, eventToSign, password as pw, passwordPrompt, signer as $signer, } from "$lib/store"; import { post, wait } from "$lib/utils"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; import type { EventTemplate } from "nostr-tools"; import { get } from "svelte/store"; let nip04Promise: Promise | undefined; let nip19Promise: Promise | undefined; let purePromise: Promise | undefined; let nip44Promise: Promise | undefined; let relayPromise: Promise | undefined; let nip49Promise: Promise | undefined; const loadNip04 = () => (nip04Promise ||= import("nostr-tools/nip04")); const loadNip19 = () => (nip19Promise ||= import("nostr-tools/nip19")); const loadPure = () => (purePromise ||= import("nostr-tools/pure")); const loadNip44 = () => (nip44Promise ||= import("nostr-tools/nip44")); const loadRelay = () => (relayPromise ||= import("nostr-tools/relay")); const loadNip49 = () => (nip49Promise ||= import("nostr-tools/nip49")); type User = { [key: string]: any; }; type EncryptParams = { message: string; recipient: string; user: User; }; export const encrypt = async ({ message, recipient, user }: EncryptParams) => { const sk = await getPrivateKey(user); const nip04 = await loadNip04(); return nip04.encrypt(sk, recipient, message); }; export const decrypt = async ({ event, user }: { event: any; user: any }) => { const cache = get(decrypted); try { let { content, pubkey, id } = event; if (cache[id]) return cache[id]; if (pubkey === user.pubkey) pubkey = event.tags[0][1]; const nip04 = await loadNip04(); const message = await nip04.decrypt(await getPrivateKey(user), pubkey, content); cache[id] = Buffer.from(message).toString("utf8"); decrypted.set(cache); return cache[id]; } catch { // console.log(e); } }; export const getPrivateKey = async (user: User): Promise => { let k; if (browser) { k = localStorage.getItem("nsec"); if (k) { const nip19 = await loadNip19(); return nip19.decode(k).data as unknown as Uint8Array; } } const { nsec } = user; if (nsec) { const { decrypt: nip49decrypt } = await loadNip49(); k = nip49decrypt(nsec, await getPassword()); } else { throw new Error("nsec not available"); } const nip19 = await loadNip19(); localStorage.setItem("nsec", nip19.nsecEncode(k)); return k; }; const decodeNsec = async (nsec: string): Promise => { const nip19 = await loadNip19(); const { type, data } = nip19.decode(nsec); if (type === "nsec" && data instanceof Uint8Array) return data; throw new Error("invalid nsec"); }; export const getNsec = async (user: User) => { const nip19 = await loadNip19(); return nip19.nsecEncode(await getPrivateKey(user)); }; export const setNsec = async (user: User, nsec: string) => { const { getPublicKey } = await loadPure(); user.pubkey = getPublicKey(await decodeNsec(nsec)); user.nsec = await encryptNsec(nsec); }; export const encryptNsec = async (nsec: string) => { const { encrypt: nip49encrypt } = await loadNip49(); const d = await decodeNsec(nsec); return nip49encrypt(d, await getPassword()); }; export const sign = async (event: any, user?: any) => { if (user?.nsec && !get($signer)?.ready) { const sk = await getPrivateKey(user); const { getPublicKey } = await loadPure(); $signer.set({ method: "nsec", ready: true, params: { sk: bytesToHex(sk), pk: getPublicKey(sk) }, }); } eventToSign.set(event); let unsubscribe: (() => void) | undefined; const { method, params } = await new Promise<{ method: string; params: any }>((r, j) => { unsubscribe = $signer.subscribe((v) => { if (v === "cancel") { j("cancelled"); $signer.set(undefined); } v?.ready && r(v); }); }); await unsubscribe?.(); const signedEvent = await signingMethods[method](event, params); eventToSign.set(signedEvent); setTimeout(() => eventToSign.set(null), 1000); return signedEvent; }; export const send = (event: EventTemplate) => { return post("/event", { event }); }; const getPassword = async (): Promise => { if (!get(pw)) passwordPrompt.set(true); await wait(() => !!get(pw)); return get(pw) || ""; }; export const nostrConnectRelay = "wss://relay.nsec.app"; const signingMethods: Record Promise> = { async connect(event: any, { sk, pk, pubkey }: any) { const id = crypto.randomUUID(); const signEvent = { id, method: "sign_event", params: [JSON.stringify(event)], }; const [nip04, { finalizeEvent }] = await Promise.all([loadNip04(), loadPure()]); const skBytes = hexToBytes(sk); const content = await nip04.encrypt(skBytes, pk, JSON.stringify(signEvent)); const signedSignEvent = finalizeEvent( { created_at: Math.round(Date.now() / 1000), kind: 24133, content, tags: [["p", pk]], } as EventTemplate, skBytes, ); const { Relay } = await loadRelay(); const relay = await Relay.connect(nostrConnectRelay); relay.publish(signedSignEvent); return new Promise((resolve) => { relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], { async onevent(event) { pk = event.pubkey; try { let response; try { const nip04 = await loadNip04(); response = JSON.parse(await nip04.decrypt(hexToBytes(sk), pk, event.content)); } catch { const { getConversationKey, decrypt: nip44decrypt } = await loadNip44(); const ck = await getConversationKey(hexToBytes(sk), pk); response = JSON.parse(await nip44decrypt(event.content, ck)); } if (response.id === id) resolve(JSON.parse(response.result)); } catch (e) { console.log("failed to parse nostr connect event", e); } }, }); }); }, async nsec(event: any, { sk }: any) { const { finalizeEvent } = await loadPure(); return finalizeEvent(event, hexToBytes(sk)); }, async extension(event: any) { return window.nostr!.signEvent(event); }, async signer(event: any, { pubkey, sig }: any) { event.pubkey = pubkey; const { getEventHash } = await loadPure(); const signedEvent = { ...event, id: getEventHash(event), sig, }; return signedEvent; }, }; // Returns all the keys mentioned in the provided event's 'p' tags. export const pTagKeys = (event: any): string[] => { let keys: string[] = []; for (const tag of event.tags) { if (tag[0] === "p") { keys.push(tag[1]); } } return keys; };