diff --git a/.gitignore b/.gitignore index b8bf853..6b647e4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pnpm-lock.yaml !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +.vscode diff --git a/package-lock.json b/package-lock.json index b449d3e..839f3ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,15 +8,20 @@ "name": "cofabricate", "version": "0.0.1", "dependencies": { + "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "^2.8.2", "@nostr-dev-kit/ndk-svelte": "^2.2.15", - "@nostr-dev-kit/ndk-svelte-components": "^2.2.16" + "@nostr-dev-kit/ndk-svelte-components": "^2.2.16", + "markdown-it": "^14.1.0", + "sanitize-html": "^2.13.0" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "@types/eslint": "^8.56.7", + "@types/markdown-it": "^14.1.1", + "@types/sanitize-html": "^2.11.0", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", @@ -1212,11 +1217,42 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, "node_modules/@types/pug": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==" }, + "node_modules/@types/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", @@ -1626,8 +1662,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.3.0", @@ -3082,6 +3117,14 @@ "node": ">=10" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -3149,6 +3192,22 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/marked": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", @@ -3192,6 +3251,11 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3829,6 +3893,14 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4591,6 +4663,11 @@ "resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz", "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==" }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, "node_modules/ufo": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", diff --git a/package.json b/package.json index 99ae03a..152518b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "vite dev", + "dev": "vite dev --host 0.0.0.0 --port 8091", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", diff --git a/src/lib/components/Avatar.svelte b/src/lib/components/Avatar.svelte index 91f0ff0..b063bb8 100644 --- a/src/lib/components/Avatar.svelte +++ b/src/lib/components/Avatar.svelte @@ -1,23 +1,35 @@ -User Avatar +
+ {#if !_ndk.activeUser} + Generic Avatar + {:else} + User Avatar + {/if} +
diff --git a/src/lib/components/LoginModal/Modal.svelte b/src/lib/components/LoginModal/Modal.svelte index f74aa1f..7fca8e6 100644 --- a/src/lib/components/LoginModal/Modal.svelte +++ b/src/lib/components/LoginModal/Modal.svelte @@ -1,8 +1,13 @@ @@ -11,8 +16,10 @@ diff --git a/src/lib/components/LoginModal/Nip07Button.svelte b/src/lib/components/LoginModal/Nip07Button.svelte index b487563..c3fa47f 100644 --- a/src/lib/components/LoginModal/Nip07Button.svelte +++ b/src/lib/components/LoginModal/Nip07Button.svelte @@ -2,18 +2,27 @@ import { ndk } from '$lib/stores/nostr'; import { login } from '$lib/utils/login'; import { currentUser } from '$lib/store'; + import { browser } from '$app/environment'; + import { createEventDispatcher } from 'svelte'; let noNip07: boolean; - $: noNip07 = !window.nostr; + $: if (browser) { + noNip07 = !window.nostr; + } + + const dispatch = createEventDispatcher(); async function nip07Login() { const user = await login($ndk, undefined, 'nip07'); if (!user) alert('Nip07 Login Failed'); else { $currentUser = user; + $currentUser.fetchProfile(); localStorage.setItem('nostr-key-method', 'nip07'); localStorage.setItem('nostr-target-npub', $currentUser.npub); + dispatch('closeModal'); + console.debug($currentUser); } } @@ -26,7 +35,5 @@ {:else} - + {/if} diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 1c82a3b..7d30aa7 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -2,6 +2,7 @@ import {} from '@nostr-dev-kit/ndk-svelte-components'; import Avatar from '$lib/components/Avatar.svelte'; import { ndk } from '$lib/stores/nostr'; + import { currentUser } from '$lib/store'; import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); @@ -10,7 +11,7 @@ let avatarimage: string | undefined; $: { - avatarimage = $ndk.activeUser?.profile?.image; + avatarimage = $currentUser?.profile?.image; } function signIn() { @@ -44,11 +45,9 @@
Home About - {#if $ndk.activeUser} - - {:else} - - {/if} +
diff --git a/src/lib/stores/nostr.ts b/src/lib/stores/nostr.ts index af4fdcf..cca7dfc 100644 --- a/src/lib/stores/nostr.ts +++ b/src/lib/stores/nostr.ts @@ -20,13 +20,14 @@ if (!relayList || !Array.isArray(relayList) || relayList.length === 0) { relayList = defaultRelays; } -const _ndk: NDKSvelte = new NDKSvelte({ +const _ndk: NDK = new NDK({ devWriteRelayUrls: ['wss://relay.strfront.com'], explicitRelayUrls: relayList, enableOutboxModel: true, autoConnectUserRelays: true, - autoFetchUserMutelist: true -}) as NDKSvelte; + autoFetchUserMutelist: true, + clientName: 'cofabricate' +}) as NDK; _ndk.connect(); @@ -34,6 +35,7 @@ console.log(_ndk.activeUser?.profile); const ndkStore = writable(_ndk); + export const ndk = ndkStore; const _bunkerNDK = new NDK({ diff --git a/src/lib/stores/sesson.ts b/src/lib/stores/sesson.ts new file mode 100644 index 0000000..322994c --- /dev/null +++ b/src/lib/stores/sesson.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; + +export type LoginState = 'logging-in' | 'logged-in' | 'contacting-remote-signer' | 'logged-out'; +export const loginState = writable(null); \ No newline at end of file diff --git a/src/lib/utils/login.ts b/src/lib/utils/login.ts index e622b4f..c0dd86e 100644 --- a/src/lib/utils/login.ts +++ b/src/lib/utils/login.ts @@ -1,50 +1,47 @@ import type NDK from '@nostr-dev-kit/ndk'; -import { NDKNip07Signer, NDKNip46Signer, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; +import { + NDKNip07Signer, + NDKNip46Signer, + NDKPrivateKeySigner, + NDKUser, + type Hexpubkey, + type NDKSigner +} from '@nostr-dev-kit/ndk'; +import { bunkerNDK, ndk } from '$lib/stores/nostr'; +import { currentUser } from '$lib/store'; +import { loginState } from '$lib/stores/sesson'; +import { get } from 'svelte/store'; export type LoginMethod = 'none' | 'pk' | 'nip07' | 'nip46'; +const $ndk = get(ndk); +const $bunkerNDK = get(bunkerNDK); -export async function login( - ndk: NDK, - bunkerNDK?: NDK, - method?: LoginMethod -): Promise { - const nostrKeyMethod = method || localStorage.getItem('nostr-key-method'); +export async function login(method: LoginMethod, userPubkey?: string ): Promise { + console.debug(`logging in with ${method}`); + let u: NDKUser | null | undefined; - switch (nostrKeyMethod) { + switch (method) { case 'none': + loginState.set(null); return null; case 'pk': const key = localStorage.getItem('nostr-key'); if (!key) return null; - - const signer = new NDKPrivateKeySigner(key); - ndk.signer = signer; - const user = await signer.user(); - if (user) user.ndk = ndk; - return user; + else return await pkLogin(key); case 'nip07': - return nip07SignIn(ndk); + u = await nip07Login($ndk); + console.debug('Logged in as: ', u); + loginState.set('logged-in'); + return u; case 'nip46': - const promise = new Promise((resolve, reject) => { - const existingPrivateKey = localStorage.getItem('nostr-nsecbunker-key'); - - if (!bunkerNDK) bunkerNDK = ndk; - - if (existingPrivateKey) { - bunkerNDK.connect(2500); - bunkerNDK.pool.on('relay:connect', async () => { - const user = await nip46SignIn(ndk, bunkerNDK!, existingPrivateKey); - resolve(user); - }); - } - }); + return await nip46Login(userPubkey); default: { - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve) => { let loadAttempts = 0; - const loadNip07Interval = setInterval(() => { + const loadNip07Interval = setInterval(async () => { if (window.nostr) { clearInterval(loadNip07Interval); - const user = nip07SignIn(ndk); + const user = await nip07Login($ndk); resolve(user); } if (loadAttempts++ > 10) clearInterval(loadNip07Interval); @@ -55,51 +52,97 @@ export async function login( } } -async function nip07SignIn(ndk: NDK): Promise { - const storedNpub = localStorage.getItem('currentUserNpub'); +async function pkLogin(key: string): Promise { + const signer = new NDKPrivateKeySigner(key); + const u = await signer.user(); + if (u) loggedIn(signer, u!, 'pk'); + return u; +} + +async function nip07Login(ndk: NDK): Promise { + const storedNpub = localStorage.getItem('pubkey'); let user: NDKUser | null = null; if (storedNpub) { user = new NDKUser({ npub: storedNpub }); user.ndk = ndk; + console.debug('Nip07 - logging in with stored npub', storedNpub); } if (window.nostr) { try { ndk.signer = new NDKNip07Signer(); - user = await ndk.signer.user(); + user = await ndk.signer?.blockUntilReady(); + ndk.activeUser = user; user.ndk = ndk; - localStorage.setItem('currentUserNpub', user.npub); - ndk = ndk; + console.debug('Nip07 Login user:', user); + console.debug('NDK: ', ndk); + if (user) localStorage.setItem('nostr-key-method', 'nip07'); + localStorage.setItem('pubkey', user.pubkey); } catch (e) {} } - + if (user) await user.fetchProfile(); return user; } -async function nip46SignIn( - ndk: NDK, - bunkerNDK: NDK, - existingPrivateKey: string -): Promise { - const npub = localStorage.getItem('nostr-target-npub')!; - const remoteUser = new NDKUser({ npub }); - let user: NDKUser | null = null; - remoteUser.ndk = bunkerNDK; +async function nip46Login(remotePubkey?: Hexpubkey): Promise { + const existingPrivateKey = localStorage.getItem('nostr-nsecbunker-key')!; + let remoteUser: NDKUser | undefined; - // check if there is a private key stored in localStorage - let localSigner: NDKPrivateKeySigner | undefined = undefined; + console.debug({ existingPrivateKey, remotePubkey }); - if (existingPrivateKey) { - localSigner = new NDKPrivateKeySigner(existingPrivateKey); + if (!existingPrivateKey) return null; + + if (remotePubkey) remoteUser = $ndk.getUser({ pubkey: remotePubkey }); + + if (!remoteUser) return null; + + currentUser.set(remoteUser); + + console.debug('NIP46 setting user: ', remoteUser); + + $bunkerNDK.pool.on('relay:ready', async () => { + console.debug('bunker relay ready'); + loginState.set('contacting-remote-signer'); + await nip46SignIn(existingPrivateKey, remoteUser!); + }); + + console.debug('connecting to nsecbunker relay'); + $bunkerNDK.connect(2500); + return remoteUser; +} + +async function nip46SignIn(existingPrivateKey: string, remoteUser: NDKUser) { + remoteUser.ndk = $bunkerNDK; + let localSigner: NDKPrivateKeySigner | null = null; + + if (existingPrivateKey) localSigner = new NDKPrivateKeySigner(existingPrivateKey); + else { + alert('Local signer not available'); + return null; } - const remoteSigner = new NDKNip46Signer(bunkerNDK, remoteUser.pubkey, localSigner); + const remoteSigner = new NDKNip46Signer($bunkerNDK, remoteUser.pubkey, localSigner!); - await remoteSigner.blockUntilReady(); - ndk.signer = remoteSigner; - user = remoteUser; - user.ndk = ndk; + console.debug('contacting remote signer'); + remoteSigner.blockUntilReady(); + console.debug('Remote signer connected'); - return user; + localStorage.setItem('nostr-nsecbunker-key', localSigner.privateKey!); + + loggedIn(remoteSigner, remoteUser, 'nip46'); +} + +export async function loggedIn(signer: NDKSigner, u: NDKUser, method: LoginMethod) { + const _ndk = get(ndk); + _ndk.signer = signer; + u.ndk = _ndk; + await u.fetchProfile(); + currentUser.set(u); + _ndk.activeUser = u; + console.log('DEBUG setting user (loggedIn)', u); + loginState.set('logged-in'); + + localStorage.setItem('pubkey', u.pubkey); + localStorage.setItem('nostr-key-method', method); } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 27792fd..81e1e2f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,8 @@ - + - + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 707eeb0..d5e982d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,5 @@ diff --git a/src/routes/browser-setup.ts b/src/routes/browser-setup.ts new file mode 100644 index 0000000..8a90c9b --- /dev/null +++ b/src/routes/browser-setup.ts @@ -0,0 +1,47 @@ +import { loginState } from "$lib/stores/sesson"; +import { loggedIn, login, type LoginMethod } from "$lib/utils/login"; +import { ndk } from "$lib/stores/nostr"; +import { get } from "svelte/store"; +import { currentUser } from "$lib/store"; +import { NDKNip07Signer, NDKUser } from "@nostr-dev-kit/ndk"; + +const _ndk = get(ndk); + +export async function browserSetup() { + const pubkey = localStorage.getItem('pubkey'); + console.debug(pubkey); + + if (pubkey){ + const u = _ndk.getUser({pubkey}); + u.fetchProfile(); + loginState.set('logging-in') + currentUser.set(u); + } + + const method = localStorage.getItem('nostr-key-method') as LoginMethod; + + if (!pubkey && method !== 'none') return newSessionTryNip07(); + + if (method && pubkey) { + return await login(method, pubkey); + } +} + +export async function newSessionTryNip07() { + let signer: NDKNip07Signer | undefined; + let u: NDKUser | null | undefined; + + try { + console.debug('trying nip07 signer'); + signer = new NDKNip07Signer(); + u = await signer.blockUntilReady(); + u.fetchProfile(); + } catch (e) { + console.debug('nip07Signer failed', e); + } + + if (u && signer) { + console.debug('nip07Signer succeeded'); + await loggedIn(signer, u, 'nip07'); + } +} \ No newline at end of file