Only protect events if the relay will authenticate with the user

This commit is contained in:
Jon Staab
2025-07-30 16:30:03 -07:00
parent 03b42c8276
commit 069904f07a
23 changed files with 159 additions and 87 deletions

View File

@@ -5,6 +5,7 @@
* Add `created_at` to event info dialog
* Add signer status to profile page
* Re-work bunker login flow
* Only protect events if relay authenticates
# 1.2.2
@@ -25,7 +26,7 @@
* Fix sort order of thread comments
* Fix link display when no title is available
* Fix making profiles non-protected
* Fix making profiles non-protect
* Replace bunker url with relay claims for notifier auth
* Add push notifications on all platforms
* Add "mark all as read" on desktop

View File

@@ -214,10 +214,19 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
// Relay access
export const attemptAuth = (url: string) =>
Pool.get()
.get(url)
.auth.attemptAuth(e => signer.get()?.sign(e))
export const attemptAuth = async (url: string) => {
const socket = Pool.get().get(url)
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
}
export const canEnforceNip70 = async (url: string) => {
const socket = Pool.get().get(url)
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
return socket.auth.status !== AuthStatus.None
}
export const checkRelayAccess = async (url: string, claim = "") => {
const socket = Pool.get().get(url)
@@ -305,26 +314,29 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
// Actions
export const makeDelete = ({event, tags = []}: {event: TrustedEvent; tags?: string[][]}) => {
export type DeleteParams = {
protect: boolean
event: TrustedEvent
tags?: string[][]
}
export const makeDelete = ({protect, event, tags = []}: DeleteParams) => {
const thisTags = [["k", String(event.kind)], ...tagEvent(event), ...tags]
const groupTag = getTag("h", event.tags)
if (groupTag) {
thisTags.push(PROTECTED, groupTag)
thisTags.push(groupTag)
}
if (protect) {
thisTags.push(PROTECTED)
}
return makeEvent(DELETE, {tags: thisTags})
}
export const publishDelete = ({
relays,
event,
tags = [],
}: {
relays: string[]
event: TrustedEvent
tags?: string[][]
}) => publishThunk({event: makeDelete({event, tags}), relays})
export const publishDelete = ({relays, ...params}: DeleteParams & {relays: string[]}) =>
publishThunk({event: makeDelete(params), relays})
export type ReportParams = {
event: TrustedEvent
@@ -350,21 +362,24 @@ export const publishReport = ({
publishThunk({event: makeReport({event, reason, content}), relays})
export type ReactionParams = {
protect: boolean
event: TrustedEvent
content: string
tags?: string[][]
}
export const makeReaction = ({content, event, tags: paramTags = []}: ReactionParams) => {
export const makeReaction = ({protect, content, event, tags: paramTags = []}: ReactionParams) => {
const tags = [...paramTags, ...tagEventForReaction(event)]
const groupTag = getTag("h", event.tags)
if (groupTag) {
tags.push(PROTECTED)
tags.push(groupTag)
}
if (protect) {
tags.push(PROTECTED)
}
return makeEvent(REACTION, {content, tags})
}

View File

@@ -12,7 +12,10 @@
const {alert}: Props = $props()
const confirm = () => {
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY], tags: [["p", NOTIFIER_PUBKEY]]})
const relays = [NOTIFIER_RELAY]
const tags = [["p", NOTIFIER_PUBKEY]]
publishDelete({event: alert.event, relays, tags, protect: false})
pushToast({message: "Your alert has been deleted!"})
history.back()
}

View File

@@ -8,7 +8,7 @@
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
import {publishDelete, publishReaction} from "@app/commands"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/commands"
import {makeCalendarPath} from "@app/routes"
import {pushModal} from "@app/modal"
@@ -26,10 +26,11 @@
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)})
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)})
</script>
<div class="flex flex-wrap items-center justify-between gap-2">

View File

@@ -16,6 +16,7 @@
import {PROTECTED} from "@app/state"
import {makeEditor} from "@app/editor"
import {pushToast} from "@app/toast"
import {canEnforceNip70} from "@app/commands"
type Props = {
url: string
@@ -63,9 +64,8 @@
}
const ed = await editor
const event = makeEvent(EVENT_TIME, {
content: ed.getText({blockSeparator: "\n"}).trim(),
tags: [
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = [
["d", initialValues?.d || randomId()],
["title", title],
["location", location || ""],
@@ -73,9 +73,13 @@
["end", end.toString()],
...daysBetween(start, end).map(D => ["D", D.toString()]),
...ed.storage.nostr.getEditorTags(),
PROTECTED,
],
})
]
if (await canEnforceNip70(url)) {
tags.push(PROTECTED)
}
const event = makeEvent(EVENT_TIME, {content, tags})
pushToast({message: "Your event has been saved!"})
publishThunk({event, relays: [url]})

View File

@@ -16,7 +16,7 @@
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
import {colors, ENABLE_ZAPS} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/commands"
import {pushModal} from "@app/modal"
interface Props {
@@ -41,10 +41,11 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)})
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)})
</script>
<TapTarget

View File

@@ -2,12 +2,17 @@
import type {NativeEmoji} from "emoji-picker-element/shared"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import Icon from "@lib/components/Icon.svelte"
import {publishReaction} from "@app/commands"
import {publishReaction, canEnforceNip70} from "@app/commands"
const {url, event} = $props()
const onEmoji = (emoji: NativeEmoji) =>
publishReaction({event, relays: [url], content: emoji.unicode})
const onEmoji = async (emoji: NativeEmoji) =>
publishReaction({
event,
relays: [url],
content: emoji.unicode,
protect: await canEnforceNip70(url),
})
</script>
<EmojiButton {onEmoji} class="btn join-item btn-xs">

View File

@@ -9,7 +9,7 @@
import EventInfo from "@app/components/EventInfo.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {ENABLE_ZAPS} from "@app/state"
import {publishReaction} from "@app/commands"
import {publishReaction, canEnforceNip70} from "@app/commands"
import {pushModal} from "@app/modal"
type Props = {
@@ -20,9 +20,14 @@
const {url, event, reply}: Props = $props()
const onEmoji = ((event: TrustedEvent, url: string, emoji: NativeEmoji) => {
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
history.back()
publishReaction({event, relays: [url], content: emoji.unicode})
publishReaction({
event,
relays: [url],
content: emoji.unicode,
protect: await canEnforceNip70(url),
})
}).bind(undefined, event, url)
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})

View File

@@ -37,10 +37,10 @@
const reply = () => replyTo(event)
const deleteReaction = (event: TrustedEvent) =>
sendWrapped({template: makeDelete({event}), pubkeys})
sendWrapped({template: makeDelete({event, protect: false}), pubkeys})
const createReaction = (template: EventContent) =>
sendWrapped({template: makeReaction({event, ...template}), pubkeys})
sendWrapped({template: makeReaction({event, protect: false, ...template}), pubkeys})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})

View File

@@ -14,7 +14,7 @@
const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) =>
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
sendWrapped({template: makeReaction({event, content: emoji.unicode, protect: false}), pubkeys})
</script>
<EmojiButton {onEmoji} class="btn join-item btn-xs">

View File

@@ -20,7 +20,7 @@
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back()
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
sendWrapped({template: makeReaction({event, content: emoji.unicode, protect: false}), pubkeys})
}).bind(undefined, event, pubkeys)
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})

View File

@@ -4,7 +4,7 @@
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import {publishDelete, publishReaction} from "@app/commands"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/commands"
import {makeThreadPath} from "@app/routes"
interface Props {
@@ -17,10 +17,11 @@
const path = makeThreadPath(url, event.id)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)})
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)})
</script>
<div class="flex flex-wrap items-center justify-between gap-2">

View File

@@ -10,7 +10,7 @@
import EmojiButton from "@lib/components/EmojiButton.svelte"
import EventMenu from "@app/components/EventMenu.svelte"
import {ENABLE_ZAPS} from "@app/state"
import {publishReaction} from "@app/commands"
import {publishReaction, canEnforceNip70} from "@app/commands"
type Props = {
url: string
@@ -26,8 +26,13 @@
const hidePopover = () => popover?.hide()
const onEmoji = (emoji: NativeEmoji) =>
publishReaction({event, content: emoji.unicode, relays: [url]})
const onEmoji = async (emoji: NativeEmoji) =>
publishReaction({
event,
content: emoji.unicode,
relays: [url],
protect: await canEnforceNip70(url),
})
let popover: Instance | undefined = $state()
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import Confirm from "@lib/components/Confirm.svelte"
import {publishDelete} from "@app/commands"
import {publishDelete, canEnforceNip70} from "@app/commands"
import {clearModals} from "@app/modal"
type Props = {
@@ -12,7 +12,7 @@
const {url, event}: Props = $props()
const confirm = async () => {
await publishDelete({event, relays: [url]})
await publishDelete({event, relays: [url], protect: await canEnforceNip70(url)})
clearModals()
}

View File

@@ -7,7 +7,7 @@
import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {publishComment} from "@app/commands"
import {publishComment, canEnforceNip70} from "@app/commands"
import {PROTECTED} from "@app/state"
import {makeEditor} from "@app/editor"
import {pushToast} from "@app/toast"
@@ -23,7 +23,11 @@
const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = [...ed.storage.nostr.getEditorTags(), PROTECTED]
const tags = ed.storage.nostr.getEditorTags()
if (await canEnforceNip70(url)) {
tags.push(PROTECTED)
}
if (!content) {
return pushToast({

View File

@@ -6,7 +6,7 @@
import ModalHeader from "@lib/components/ModalHeader.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import {publishDelete} from "@app/commands"
import {publishDelete, canEnforceNip70} from "@app/commands"
const {url, event} = $props()
@@ -16,8 +16,8 @@
const back = () => history.back()
const deleteReport = (report: TrustedEvent) => {
publishDelete({event: report, relays: [url]})
const deleteReport = async (report: TrustedEvent) => {
publishDelete({event: report, relays: [url], protect: await canEnforceNip70(url)})
if ($reports.length === 0) {
history.back()

View File

@@ -4,7 +4,7 @@
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import {publishDelete, publishReaction} from "@app/commands"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/commands"
import {makeGoalPath} from "@app/routes"
interface Props {
@@ -17,10 +17,11 @@
const path = makeGoalPath(url, event.id)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)})
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)})
</script>
<div class="flex flex-wrap items-center justify-between gap-2">

View File

@@ -13,6 +13,7 @@
import {pushToast} from "@app/toast"
import {PROTECTED} from "@app/state"
import {makeEditor} from "@app/editor"
import {canEnforceNip70} from "@app/commands"
const {url} = $props()
@@ -47,9 +48,12 @@
["summary", summary],
["amount", String(amount)],
["relays", url],
PROTECTED,
]
if (await canEnforceNip70(url)) {
tags.push(PROTECTED)
}
publishThunk({
relays: [url],
event: makeEvent(ZAP_GOAL, {content, tags}),

View File

@@ -6,17 +6,23 @@
import NoteContent from "@app/components/NoteContent.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import {publishDelete, publishReaction} from "@app/commands"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/commands"
const {url, event} = $props()
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)})
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)})
const onEmoji = (emoji: NativeEmoji) =>
publishReaction({event, content: emoji.unicode, relays: [url]})
const onEmoji = async (emoji: NativeEmoji) =>
publishReaction({
event,
content: emoji.unicode,
relays: [url],
protect: await canEnforceNip70(url),
})
</script>
<NoteCard {event} {url} class="card2 bg-alt">

View File

@@ -4,7 +4,7 @@
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import {publishDelete, publishReaction} from "@app/commands"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/commands"
import {makeThreadPath} from "@app/routes"
interface Props {
@@ -17,10 +17,11 @@
const path = makeThreadPath(url, event.id)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)})
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)})
</script>
<div class="flex flex-wrap items-center justify-between gap-2">

View File

@@ -12,6 +12,7 @@
import {pushToast} from "@app/toast"
import {PROTECTED} from "@app/state"
import {makeEditor} from "@app/editor"
import {canEnforceNip70} from "@app/commands"
const {url} = $props()
@@ -41,7 +42,11 @@
})
}
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title], PROTECTED]
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
if (await canEnforceNip70(url)) {
tags.push(PROTECTED)
}
publishThunk({
relays: [url],

View File

@@ -39,7 +39,12 @@
REACTION_KINDS,
} from "@app/state"
import {setChecked, checked} from "@app/notifications"
import {addRoomMembership, removeRoomMembership, prependParent} from "@app/commands"
import {
addRoomMembership,
canEnforceNip70,
removeRoomMembership,
prependParent,
} from "@app/commands"
import {PROTECTED} from "@app/state"
import {makeFeed} from "@app/requests"
import {popKey} from "@app/implicit"
@@ -101,9 +106,12 @@
share = undefined
}
const onSubmit = ({content, tags}: EventContent) => {
const onSubmit = async ({content, tags}: EventContent) => {
tags.push(["h", room])
if (await canEnforceNip70(url)) {
tags.push(PROTECTED)
}
let template = {content, tags}

View File

@@ -20,7 +20,7 @@
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {userSettingValues, decodeRelay, getEventsForUrl} from "@app/state"
import {setChecked, checked} from "@app/notifications"
import {prependParent} from "@app/commands"
import {prependParent, canEnforceNip70} from "@app/commands"
import {PROTECTED, REACTION_KINDS} from "@app/state"
import {makeFeed} from "@app/requests"
import {popKey} from "@app/implicit"
@@ -43,8 +43,10 @@
share = undefined
}
const onSubmit = ({content, tags}: EventContent) => {
const onSubmit = async ({content, tags}: EventContent) => {
if (await canEnforceNip70(url)) {
tags.push(PROTECTED)
}
let template = {content, tags}