Things are basically working, profile update done

This commit is contained in:
Jonathan Staab 2022-11-23 19:54:22 -08:00
parent 2b48a59c85
commit c2eee552b4
23 changed files with 576 additions and 219 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -24,10 +24,14 @@
"@noble/secp256k1": "^1.7.0",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"compressorjs": "^1.1.1",
"dexie": "^3.2.2",
"fuse.js": "^6.6.2",
"hurdak": "github:ConsignCloud/hurdak",
"nostr-tools": "^0.24.1",
"ramda": "^0.28.0",
"svelte-routing": "^1.6.0"
"svelte-routing": "^1.6.0",
"throttle-debounce": "^5.0.0",
"vite-plugin-node-polyfills": "^0.5.0"
}
}

View File

@ -5,13 +5,14 @@
import {onMount} from "svelte"
import {writable} from "svelte/store"
import {fly} from "svelte/transition"
import {Router, Route, links} from "svelte-routing"
import {Router, Route, links, navigate} from "svelte-routing"
import {store as toast} from "src/state/toast"
import {user} from 'src/state/user'
import Feed from "src/routes/Feed.svelte"
import Login from "src/routes/Login.svelte"
import Profile from "src/routes/Profile.svelte"
import RelayList from "src/routes/RelayList.svelte"
import RelayDetail from "src/routes/RelayDetail.svelte"
import UserDetail from "src/routes/UserDetail.svelte"
import Explore from "src/routes/Explore.svelte"
import Messages from "src/routes/Messages.svelte"
@ -26,12 +27,20 @@
export let url = ""
onMount(() => {
document.querySelector("body").addEventListener("click", e => {
document.querySelector("html").addEventListener("click", e => {
if (e.target !== menuIcon) {
menuIsOpen.set(false)
}
})
})
// Give the animation a moment to finish
const logout = () => {
setTimeout(() => {
user.set(null)
navigate("/login")
}, 200)
}
</script>
<Router {url}>
@ -39,28 +48,49 @@
<div class="py-20 p-4 text-white">
<Route path="/" component={Feed} />
<Route path="/login" component={Login} />
<Route path="/profile" component={Profile} />
<Route path="/relays" component={RelayList} />
<Route path="/relays/:id" component={RelayDetail} />
<Route path="/user/:pubkey" component={UserDetail} />
<Route path="/explore" component={Explore} />
<Route path="/messages" component={Messages} />
<Route path="/settings/profile" component={Profile} />
</div>
<ul
class="py-20 p-4 w-48 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl
class="py-20 w-48 bg-dark fixed top-0 bottom-0 left-0 transition-all shadow-xl
border-r border-light text-white"
class:-ml-48={!$menuIsOpen}
>
<li class="cursor-pointer py-2">
<a href="/profile">
{#if $user}
<li class="flex gap-2 px-4 py-2 pb-8 items-center">
<div
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0"
style="background-image: url({$user.picture})" />
<span class="text-lg font-bold">{$user.name}</span>
</li>
<li class="cursor-pointer">
<a class="block pl-6 px-4 py-2 hover:bg-accent transition-all" href="/user/{$user.pubkey}">
<i class="fa-solid fa-user-astronaut mr-2" /> Profile
</a>
</li>
<li class="cursor-pointer py-2">
<a href="/relays">
{:else}
<li class="cursor-pointer">
<a class="block pl-6 px-4 py-2 hover:bg-accent transition-all" href="/login">
<i class="fa-solid fa-right-to-bracket mr-2" /> Login
</a>
</li>
{/if}
<li class="cursor-pointer">
<a class="block pl-6 px-4 py-2 hover:bg-accent transition-all" href="/relays">
<i class="fa-solid fa-server mr-2" /> Relays
</a>
</li>
{#if $user}
<li class="cursor-pointer">
<a class="block pl-6 px-4 py-2 hover:bg-accent transition-all" on:click={logout}>
<i class="fa-solid fa-right-from-bracket mr-2" /> Logout
</a>
</li>
{/if}
</ul>
<div
@ -68,7 +98,7 @@
border-b border-light"
>
<i class="fa-solid fa-bars fa-2xl cursor-pointer" bind:this={menuIcon} on:click={toggleMenu} />
<h1 class="staatliches text-3xl">Blazepoint</h1>
<h1 class="staatliches text-3xl">Coracle</h1>
</div>
<div

View File

@ -1,35 +0,0 @@
import {identity} from 'ramda'
import {defmulti} from "hurdak/lib/hurdak"
import keys from "src/util/keys"
import {db} from "src/adapters/mock/db"
import {user} from "src/adapters/mock/user"
import {broadcastNewEvent} from "src/adapters/mock/events"
// Commands are processed in two layers:
// - App-oriented commands are created via dispatch
// - processEvent, which can be used to populate the database
// whether commands are coming from our instance or a remote instance.
export const dispatch = defmulti("commands/dispatch", identity)
dispatch.addMethod("account/init", (topic, privkey) => {
const pubkey = keys.getPublicKey(privkey)
user.set({
pubkey,
privkey,
name: pubkey.slice(0, 8),
picture: null,
about: null,
})
})
dispatch.addMethod("server/join", (topic, s) => {
db.relays.put(s)
})
dispatch.addMethod("server/leave", (topic, s) => {
db.relays.where("url").equals(s.url).delete()
})
dispatch.addMethod("post/create", broadcastNewEvent)

View File

@ -1,17 +0,0 @@
import {Dexie} from "dexie"
export const db = new Dexie("coracle/db", {})
db.version(2).stores({
relays: "url",
events: "id, pubkey",
})
db
.open()
.then(async db => {
console.log("Database ready")
})
.catch(err => {
console.error(err)
})

View File

@ -1,117 +0,0 @@
import {get} from "svelte/store"
import {Buffer} from "buffer"
import {pickValues, switcherFn} from "hurdak/lib/hurdak"
import keys from "src/util/keys"
import {db} from "src/adapters/mock/db"
import {user} from "src/adapters/mock/user"
export const broadcastNewEvent = async (topic, payload) => {
const event = await initEvent(topic, payload)
try {
await broadcast(event)
} catch (e) {
console.error("Failed to broadcast new event", e)
}
}
const initEvent = async (topic, payload) => {
const {pubKey, instance} = get(user)
const ord = await db.events.where({pubKey}).count()
return {
topic,
pubKey,
instance,
path: [],
ordinal: ord,
payload: JSON.stringify(payload),
synchronized: [],
gid: [pubKey, instance, ord].join("."),
}
}
export const broadcast = async event => {
const signedEvent = await signEvent(event)
const servers = await db.servers.toArray()
if (event.pubKey === get(user).pubKey) {
await db.events.put(signedEvent)
}
await processEvent(signedEvent)
await Promise.all(servers.map(({url}) => req("POST", url, "/publish", {events: [signedEvent]})))
}
export const signEvent = async event => {
const {privKey, pubKey} = get(user)
return {
...event,
path: event.path.concat({
pubKey,
timestamp: new Date().toISOString(),
signature: await keys.sign(await hashEvent(event), privKey),
}),
}
}
export const hashEvent = async event => {
let payload = pickValues(["topic", "payload", "instance", "ordinal"], event).join("")
for (let part of event["path"]) {
payload += pickValues(["pubKey", "timestamp", "signature"], part).join("")
}
const hash = await crypto.subtle.digest("SHA-256", _encoder.encode(payload))
return Buffer.from(hash).toString("hex")
}
export const processEvent = async event => {
const payload = JSON.parse(event.payload)
console.log("processing event", {...event, payload})
event.path.forEach(async ({signature, pubKey}, i) => {
const hash = await hashEvent({...event, path: event.path.slice(0, i)})
if (!(await keys.verify(signature, hash, pubKey))) {
console.warn(`Event failed to validate: ${event.gid} at signature #${i}`)
}
})
if (event.pubKey === get(user).pubKey) {
await switcherFn(event.topic, {
"post/create": () => db.posts.put({...payload, gid: event.gid}),
default: () => console.warn(`Unrecognized event topic: ${event.topic}`),
})
}
}
export const req = async (method, url, path, {json, body} = {}) => {
const $user = get(user)
const headers = {}
if (json) {
headers["Content-Type"] = "application/json"
body = JSON.stringify(json)
}
if ($user) {
const {privKey, pubKey} = $user
const prep = x => btoa(JSON.stringify(x))
const header = prep({typ: "JWT", alg: "secp256k1"})
const meta = prep({iss: pubKey, exp: new Date().valueOf() + 1000, aud: url})
const payload = Buffer.from(_encoder.encode([header, meta].join(".")))
const signature = btoa(await keys.sign(payload.toString("hex"), privKey))
const jwt = [header, meta, signature].join(".")
headers["Authorization"] = `Bearer ${jwt}`
}
return fetch(`${url}${path}`, {method, body, headers})
}
const _encoder = new TextEncoder()

View File

@ -1,3 +0,0 @@
export {dispatch} from "src/adapters/mock/commands"
export {user} from "src/adapters/mock/user"
export {db} from "src/adapters/mock/db"

View File

@ -1,6 +0,0 @@
import {writable} from "svelte/store"
import {getLocalJson, setLocalJson} from "src/util/misc"
export const user = writable(getLocalJson("coracle/user"))
user.subscribe($user => setLocalJson("coracle/user", $user))

View File

@ -2,7 +2,7 @@
@tailwind components;
@tailwind utilities;
font-face {
@font-face {
font-family: "Montserrat";
font-style: normal;
font-weight: 400;

View File

@ -7,9 +7,10 @@
const className = cx(
$$props.class,
"cursor-pointer",
switcher(type, {
anchor: "underline",
button: "py-2 px-4 rounded bg-white text-accent cursor-pointer",
button: "py-2 px-4 rounded bg-white text-accent",
}),
)
</script>

View File

@ -2,7 +2,8 @@
import {liveQuery} from "dexie"
import {navigate} from "svelte-routing"
import Anchor from "src/partials/Anchor.svelte"
import {db, user} from "src/adapters/mock"
import {user} from "src/state/user"
import {db} from "src/state/db"
const relays = liveQuery(() => db.relays.toArray())
@ -28,7 +29,7 @@
</div>
{:else if $relays}
<div class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-sm">
<div class="text-center max-w-2xl">
You aren't yet connected to any relays. Please click <Anchor href="/relays"
>here</Anchor
> to get started.

View File

@ -1 +1,61 @@
Login
<script>
import {navigate} from 'svelte-routing'
import {generatePrivateKey} from 'nostr-tools'
import {copyToClipboard} from "src/util/html"
import Anchor from "src/partials/Anchor.svelte"
import Input from "src/partials/Input.svelte"
import toast from "src/state/toast"
import {user} from "src/state/user"
import {dispatch} from "src/state/dispatch"
let privKey = ''
const copyKey = () => {
copyToClipboard(privKey)
toast.show("info", "Your private key has been copied to the clipboard.")
}
const generateKey = () => {
privKey = generatePrivateKey()
toast.show("info", "Your private key has been re-generated.")
}
const logIn = async () => {
if (!privKey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid private key.")
} else {
const {found} = await dispatch("account/init", privKey)
await navigate(found ? `/` : '/settings/profile')
}
}
</script>
<div class="flex justify-center pt-12">
<div class="flex flex-col gap-4 max-w-2xl">
<div class="flex justify-center items-center flex-col mb-4">
<h1 class="staatliches text-6xl">Welcome!</h1>
<i>To the Dogwood Social Network</i>
</div>
<div class="flex flex-col gap-4">
<small>
To log in to existing account, simply enter your private key. To create a new account, just
let us generate one for you.
</small>
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input type="password" bind:value={privKey} placeholder="Enter your private key">
<i slot="after" class="fa-solid fa-copy" on:click={copyKey} />
</Input>
<Anchor on:click={generateKey} class="text-right">
<small>Generate new key</small>
</Anchor>
</div>
<small>
Your private key is a string of random letters and numbers that allow you to prove
you own your account. Write it down and keep it secret!
</small>
</div>
<Anchor class="text-center" type="button" on:click={logIn}>Log In</Anchor>
</div>
</div>

View File

@ -1 +1,96 @@
Profile
<script>
import {onMount} from "svelte"
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import pick from "ramda/src/pick"
import {stripExifData} from "src/util/html"
import Input from "src/partials/Input.svelte"
import Select from "src/partials/Select.svelte"
import Textarea from "src/partials/Textarea.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Button from "src/partials/Button.svelte"
import {user} from "src/state/user"
import {dispatch} from "src/state/dispatch"
import toast from "src/state/toast"
let values = {picture: null, about: null, name: null}
onMount(async () => {
if (!$user) {
return navigate("/login")
}
values = pick(Object.keys(values), $user)
document.querySelector('[name=picture]').addEventListener('change', async e => {
const [file] = e.target.files
if (file) {
const reader = new FileReader()
reader.onload = () => values.picture = reader.result
reader.onerror = e => console.error(e)
reader.readAsDataURL(await stripExifData(file))
} else {
values.picture = null
}
})
})
const submit = async event => {
event.preventDefault()
if (!values.name.match(/^\w[\w\-]+\w$/)) {
toast.show("error", "Names must be comprised of letters, numbers, and dashes only.")
} else {
await dispatch("account/update", values)
toast.show("info", "Your profile has been updated!")
}
}
</script>
<form on:submit={submit} class="flex justify-center py-12" in:fly={{y: 20}}>
<div class="flex flex-col gap-4 max-w-2xl">
<div class="flex justify-center items-center flex-col mb-4">
<h1 class="staatliches text-6xl">About You</h1>
<p>
Give people a friendly way to recognize you. We recommend you do not use your real name or
share your personal information. The future of the internet is
<Anchor
target="_blank"
rel="noopener"
href="https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
>
pseudonymous
</Anchor>.
</p>
</div>
<div class="flex flex-col gap-8 w-full">
<div class="flex flex-col gap-1">
<strong>Username</strong>
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={values.name}>
<i slot="before" class="fa-solid fa-user-astronaut" />
</Input>
<p class="text-sm text-light">
Your username can be changed at any time. To prevent spoofing, a few characters of your
public key will also be displayed next to your posts.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>About you</strong>
<Textarea name="about" bind:value={values.about} />
<p class="text-sm text-light">
Tell the world about yourself. This will be shown on your profile page.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Profile Image</strong>
<input type="file" name="picture" />
<p class="text-sm text-light">
Your profile image will have all metadata removed before being published.
</p>
</div>
<Button type="submit" class="text-center">Done</Button>
</div>
</div>
</form>

View File

@ -1 +0,0 @@
RelayDetail

View File

@ -1 +1,53 @@
Relays
<script>
import {liveQuery} from "dexie"
import {propEq} from "ramda"
import {fly} from 'svelte/transition'
import {fuzzy, hash} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {db} from "src/state/db"
import {dispatch} from "src/state/dispatch"
import {nostr, relays} from "src/state/nostr"
let q = ""
let search = () => []
const knownRelays = liveQuery(() => db.relays.toArray())
$: search = fuzzy($knownRelays || [], {keys: ["name", "description", "url"]})
const toggle = (url, value) => {
dispatch(value ? "relay/join" : "relay/leave", url)
}
</script>
<div class="flex justify-center py-12" in:fly={{y: 20}}>
<div class="flex flex-col gap-8 max-w-2xl w-full">
<div class="flex justify-center items-center flex-col mb-4">
<h1 class="staatliches text-6xl">Get Connected</h1>
<p>
Relays are hubs for your content and connections. At least one is required to
interact with the network, but you can join as many as you like.
</p>
</div>
<div class="flex gap-4">
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
<i slot="before" class="fa-solid fa-search" />
</Input>
<Anchor type="button" href="/">Done</Anchor>
</div>
<div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6">
{#each search(q) as relay}
<div class="flex gap-2 justify-between cursor-pointer">
<div>
<strong>{relay.name || relay.url}</strong>
<p class="text-light">{relay.description || ''}</p>
</div>
<a class="underline" on:click={() => toggle(relay.url, !$relays.includes(relay.url))}>
{$relays.includes(relay.url) ? "Leave" : "Join"}
</a>
</div>
{/each}
</div>
</div>
</div>

View File

@ -0,0 +1,74 @@
<script>
import {onMount} from 'svelte'
import {reverse} from 'ramda'
import {fly} from 'svelte/transition'
import {uniqBy, prop} from 'ramda'
import {switcherFn} from 'hurdak/src/core'
import {nostr} from 'src/state/nostr'
import {user} from 'src/state/user'
export let pubkey
let userData
let notes = []
onMount(() => {
const sub = nostr.sub({
filter: {authors: [pubkey]},
cb: e => {
switcherFn(e.kind, {
[0]: () => {
userData = JSON.parse(e.content)
},
[1]: () => {
notes = uniqBy(prop('id'), notes.concat(e))
},
default: () => null,
})
},
})
return () => sub.unsub()
})
const formatTimestamp = ts => {
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
})
return formatter.format(new Date(ts * 1000))
}
</script>
{#if userData}
<div class="max-w-2xl m-auto flex flex-col gap-4 py-4">
<div class="flex flex-col gap-4" in:fly={{y: 20}}>
<div class="flex gap-4">
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0"
style="background-image: url({userData.picture})" />
<div class="flex-grow">
<div class="flex justify-between items-center">
<h1 class="text-2xl">{userData.name}</h1>
{#if $user?.pubkey === pubkey}
<a href="/settings/profile" class="cursor-pointer text-sm">
<i class="fa-solid fa-edit" /> Edit
</a>
{/if}
</div>
<p>{userData.about}</p>
</div>
</div>
</div>
<div class="h-px bg-light" in:fly={{y: 20, delay: 200}} />
<div class="flex flex-col gap-4" in:fly={{y: 20, delay: 400}}>
{#each reverse(notes) as note}
<div>
<small class="text-light">{formatTimestamp(note.created_at)}</small>
<p>{note.content}</p>
</div>
{/each}
</div>
</div>
{/if}

38
src/state/db.js Normal file
View File

@ -0,0 +1,38 @@
import {Dexie} from "dexie"
export const db = new Dexie("coracle/db", {})
db.version(2).stores({
relays: "url",
events: "id, pubkey",
})
db
.open()
.then(async db => {
console.log("Database ready")
})
.catch(err => {
console.error(err)
})
export const registerRelay = async url => {
let json
try {
const res = await fetch(url.replace(/^ws/, 'http'), {
headers: {
Accept: 'application/nostr_json',
},
})
json = await res.json()
} catch (e) {
json = {}
}
db.relays.put({...json, url})
}
registerRelay('wss://nostr-pub.wellorder.net')
registerRelay('wss://nostr-relay.wlvs.space')
registerRelay('ws://localhost:7000')

44
src/state/dispatch.js Normal file
View File

@ -0,0 +1,44 @@
import {identity, without} from 'ramda'
import {getPublicKey} from 'nostr-tools'
import {get} from 'svelte/store'
import {defmulti} from "hurdak/lib/hurdak"
import {db} from "src/state/db"
import {user} from "src/state/user"
import {nostr, relays} from 'src/state/nostr'
// Commands are processed in two layers:
// - App-oriented commands are created via dispatch
// - processEvent, which can be used to populate the database
// whether commands are coming from our instance or a remote instance.
export const dispatch = defmulti("dispatch", identity)
dispatch.addMethod("account/init", async (topic, privkey) => {
// Generate a public key
const pubkey = getPublicKey(privkey)
// Set what we know about the user to our store
user.set({name: pubkey.slice(0, 8), privkey, pubkey})
// Attempt to refresh user data from the network
const found = Boolean(await user.refresh())
// Tell the caller whether this user was found
return {found}
})
dispatch.addMethod("account/update", async (topic, updates) => {
// Update our local copy
user.set({...get(user), ...updates})
// Tell the network
await nostr.publish(nostr.event(0, JSON.stringify(updates)))
})
dispatch.addMethod("relay/join", (topic, url) => {
relays.update(r => r.concat(url))
})
dispatch.addMethod("relay/leave", (topic, url) => {
relays.update(r => without([url], r))
})

77
src/state/nostr.js Normal file
View File

@ -0,0 +1,77 @@
import {writable} from 'svelte/store'
import {relayPool, getPublicKey} from 'nostr-tools'
import {getLocalJson, setLocalJson} from "src/util/misc"
export const nostr = relayPool()
// Augment nostr with some extra methods
nostr.login = privkey => {
nostr.setPrivateKey(privkey)
nostr._privkey = privkey
}
nostr.event = (kind, content = '', tags = []) => {
const pubkey = getPublicKey(nostr._privkey)
const createdAt = Math.round(new Date().valueOf() / 1000)
return {kind, content, tags, pubkey, created_at: createdAt}
}
nostr.find = (filter, timeout = 300) => {
return new Promise(resolve => {
const sub = nostr.sub({
filter,
cb: e => {
resolve(e)
sub.unsub()
},
})
setTimeout(() => {
resolve(null)
sub.unsub()
}, timeout)
})
}
nostr.findLast = (filter, timeout = 300) => {
return new Promise(resolve => {
let result = null
const sub = nostr.sub({
filter,
cb: e => {
result = e
},
})
setTimeout(() => {
resolve(result)
sub.unsub()
}, timeout)
})
}
// Create writable store for relays so we can observe changes in the app
export const relays = writable(getLocalJson("coracle/relays") || [])
relays.subscribe($relays => {
Object.keys(nostr.relays).forEach(url => {
if (!$relays.includes(url)) {
nostr.removeRelay(url)
}
})
$relays.forEach(url => {
if (!nostr.relays[url]) {
nostr.addRelay(url)
}
})
setLocalJson("coracle/relays", $relays)
})

31
src/state/user.js Normal file
View File

@ -0,0 +1,31 @@
import {writable, get} from "svelte/store"
import {getLocalJson, setLocalJson} from "src/util/misc"
import {nostr} from 'src/state/nostr'
export const user = writable(getLocalJson("coracle/user"))
user.subscribe($user => {
setLocalJson("coracle/user", $user)
// Keep nostr in sync
nostr.login($user?.privkey)
})
user.refresh = async () => {
const $user = get(user)
if ($user) {
const data = await nostr.findLast({authors: [$user.pubkey], kinds: [0]})
if (data) {
user.update($user => ({...$user, ...JSON.parse(data.content)}))
}
return Boolean(data)
}
return false
}
// Check for new data every so often
setTimeout(() => user.refresh(), 60 * 1000)

View File

@ -13,3 +13,34 @@ export const copyToClipboard = text => {
return result
}
export const stripExifData = async file => {
if (window.DataTransferItem && file instanceof DataTransferItem) {
file = file.getAsFile()
}
if (!file) {
return file
}
const {default: Compressor} = await import("compressorjs")
/* eslint no-new: 0 */
return new Promise((resolve, _reject) => {
new Compressor(file, {
maxWidth: 50,
maxHeight: 50,
convertSize: 1024,
success: resolve,
error: e => {
// Non-images break compressor
if (e.toString().includes("File or Blob")) {
return resolve(file)
}
_reject(e)
},
})
})
}

View File

@ -1,11 +0,0 @@
import {Buffer} from "buffer"
import * as secp from "@noble/secp256k1"
const toHex = buffer => Buffer.from(buffer).toString("hex")
export default {
generatePrivateKey: () => toHex(secp.utils.randomPrivateKey()),
getPublicKey: privKey => toHex(secp.schnorr.getPublicKey(privKey)),
sign: async (hex, privKey) => toHex(await secp.schnorr.sign(hex, privKey)),
verify: (sig, payload, pubKey) => secp.schnorr.verify(sig, payload, pubKey),
}

View File

@ -1,18 +1,27 @@
import * as path from 'path'
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: [
{find: 'src', replacement: path.resolve(__dirname, 'src')},
],
define: {
global: {},
},
plugins: [svelte({
onwarn: (warning, handler) => {
if (warning.code.startsWith("a11y-")) return
handler(warning)
},
})],
resolve: {
alias: {
src: path.resolve(__dirname, 'src'),
}
},
plugins: [
nodePolyfills({
protocolImports: true,
}),
svelte({
onwarn: (warning, handler) => {
if (warning.code.startsWith("a11y-")) return
handler(warning)
},
}),
],
})