flotilla/src/app/policies.ts

220 lines
6.5 KiB
TypeScript
Raw Normal View History

import {get, writable} from "svelte/store"
2026-07-28 16:16:26 +00:00
import {on, call, dissoc, assoc, noop, uniq} from "@welshman/lib"
import {isDVMKind, isEphemeralKind, verifyEvent} from "@welshman/util"
2025-10-21 15:27:30 +00:00
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
import {
2026-07-28 16:16:26 +00:00
AuthStatus,
2025-10-21 15:27:30 +00:00
SocketEvent,
isRelayEvent,
isRelayOk,
isRelayClosed,
2025-11-04 23:36:20 +00:00
isRelayNegErr,
2025-10-21 15:27:30 +00:00
isClientReq,
isClientEvent,
isClientClose,
2025-11-04 23:36:20 +00:00
isClientNegOpen,
isClientNegClose,
2025-10-21 15:27:30 +00:00
} from "@welshman/net"
2026-07-28 16:16:26 +00:00
import {merged} from "@welshman/store"
import {
BlockedRelayLists,
MessagingRelayLists,
RelayLists,
RoomLists,
Thunks,
makeAppPolicyAuth,
} from "@welshman/app"
import type {AppPolicy, IApp} from "@welshman/app"
import {app, appPolicies} from "@app/core"
import {BLOCKED_RELAYS} from "@app/env"
import {userSettingsValues, getSetting, RelayAuthMode} from "@app/settings"
// Relays sending events with empty signatures that the user has to choose to trust
export const relaysPendingTrust = writable<string[]>([])
// Relays that mostly send restricted responses to requests and events
export const relaysMostlyRestricted = writable<Record<string, string>>({})
2025-10-21 15:27:30 +00:00
2026-07-28 16:16:26 +00:00
// Welshman's default ingest policy drops anything that fails signature verification, but relays
// the user has explicitly trusted are allowed to send events with an empty signature.
export const ingestPolicy: AppPolicy = app =>
app.pool.subscribe(socket => {
const onReceive = (message: RelayMessage) => {
if (isRelayEvent(message)) {
const event = message[2]
const trusted = getSetting("trusted_relays").includes(socket.url)
if (isDVMKind(event.kind) || isEphemeralKind(event.kind)) return
if (!trusted && !verifyEvent(event)) return
app.tracker.track(event.id, socket.url)
app.repository.publish(event)
}
}
socket.on(SocketEvent.Receive, onReceive)
return () => socket.off(SocketEvent.Receive, onReceive)
})
// Welshman's appPolicyAuthUnlessBlocked, plus the conservative mode: only identify to relays
// the user already has a relationship with.
export const authPolicy = makeAppPolicyAuth((socket, $app) => {
const $pubkey = app.get().user?.pubkey
if (!$pubkey) return false
if ($app.use(BlockedRelayLists).urls($pubkey).get().includes(socket.url)) return false
if (getSetting("relay_auth") === RelayAuthMode.Aggressive) return true
if ($app.use(RoomLists).urls($pubkey).get().includes(socket.url)) return true
if ($app.use(RelayLists).urls($pubkey).get().includes(socket.url)) return true
if (get($app.use(Thunks).history).some(t => t.options.relays.includes(socket.url))) return true
if ($app.use(MessagingRelayLists).urls($pubkey).get().includes(socket.url)) return true
return false
2026-01-16 21:49:35 +00:00
})
2025-10-21 15:27:30 +00:00
2026-07-28 16:16:26 +00:00
const makeBlockPolicy = ($app: IApp) => (socket: Socket) => {
2026-01-16 21:10:48 +00:00
const previousOpen = socket.open
socket.open = () => {
2026-07-28 16:16:26 +00:00
const $pubkey = $app.user?.pubkey
2026-01-16 21:10:48 +00:00
2026-01-20 18:40:33 +00:00
if (BLOCKED_RELAYS.includes(socket.url)) return
2026-07-28 16:16:26 +00:00
if ($pubkey && $app.use(BlockedRelayLists).urls($pubkey).get().includes(socket.url)) return
2026-01-20 18:40:33 +00:00
previousOpen()
2026-01-16 21:10:48 +00:00
}
return () => {
socket.open = previousOpen
}
}
2026-07-28 16:16:26 +00:00
const trustPolicy = (socket: Socket) => {
2025-10-21 15:27:30 +00:00
const buffer: RelayMessage[] = []
const unsubscribers = [
// When the socket goes from untrusted to trusted, receive all buffered messages
userSettingsValues.subscribe($settings => {
if ($settings.trusted_relays.includes(socket.url)) {
for (const message of buffer.splice(0)) {
socket._recvQueue.push(message)
}
}
}),
// When we get an event with no signature from an untrusted relay, remove it from
// the receive queue. If trust status is undefined, buffer it for later.
on(socket, SocketEvent.Receiving, (message: RelayMessage) => {
if (isRelayEvent(message) && !message[2]?.sig) {
2026-07-28 16:16:26 +00:00
const isTrusted = getSetting("trusted_relays").includes(socket.url)
2025-10-21 15:27:30 +00:00
if (!isTrusted) {
buffer.push(message)
socket._recvQueue.remove(message)
relaysPendingTrust.update($r => uniq([...$r, socket.url]))
}
}
}),
]
return () => {
unsubscribers.forEach(call)
}
}
2026-07-28 16:16:26 +00:00
const mostlyRestrictedPolicy = (socket: Socket) => {
2025-10-21 15:27:30 +00:00
let total = 0
let restricted = 0
const pending = new Set<string>()
const updateStatus = (error?: string) => {
2026-06-24 18:09:44 +00:00
if (total > 5 && restricted > total / 2) {
if (error) {
return relaysMostlyRestricted.update(assoc(socket.url, error))
}
} else {
relaysMostlyRestricted.update(dissoc(socket.url))
}
}
2025-10-21 15:27:30 +00:00
const unsubscribers = [
on(socket, SocketEvent.Receive, (message: RelayMessage) => {
if (isRelayOk(message)) {
const [_, id, ok, details = ""] = message
if (pending.has(id)) {
pending.delete(id)
2025-11-04 23:36:20 +00:00
if (!ok) {
if (details.startsWith("auth-required: ")) {
total--
updateStatus()
}
if (details.startsWith("restricted: ")) {
restricted++
updateStatus(details)
2025-11-04 23:36:20 +00:00
}
2025-10-21 15:27:30 +00:00
}
}
}
2025-11-04 23:36:20 +00:00
if (isRelayClosed(message) || isRelayNegErr(message)) {
2025-10-21 15:27:30 +00:00
const [_, id, details = ""] = message
if (pending.has(id)) {
pending.delete(id)
2025-11-04 23:36:20 +00:00
if (details.startsWith("auth-required: ")) {
total--
updateStatus()
}
2025-10-21 15:27:30 +00:00
if (details.startsWith("restricted: ")) {
restricted++
updateStatus(details)
2025-10-21 15:27:30 +00:00
}
}
}
}),
on(socket, SocketEvent.Send, (message: ClientMessage) => {
2025-11-04 23:36:20 +00:00
if (isClientReq(message) || isClientNegOpen(message)) {
if (!pending.has(message[1])) {
total++
pending.add(message[1])
updateStatus()
}
2025-10-21 15:27:30 +00:00
}
if (isClientEvent(message)) {
total++
pending.add(message[1].id)
updateStatus()
}
2025-11-04 23:36:20 +00:00
if (isClientClose(message) || isClientNegClose(message)) {
2025-10-21 15:27:30 +00:00
pending.delete(message[1])
}
}),
]
return () => {
unsubscribers.forEach(call)
}
}
2026-07-28 16:16:26 +00:00
// Socket policies are installed on the pool rather than the app, so wrap them in an app policy
// to get the same construction/cleanup lifecycle as everything else.
export const socketPolicy: AppPolicy = $app => {
const policies = [makeBlockPolicy($app), trustPolicy, mostlyRestrictedPolicy]
$app.pool.socketPolicies.push(...policies)
return () => {
$app.pool.socketPolicies = $app.pool.socketPolicies.filter(p => !policies.includes(p))
}
}
appPolicies.push(ingestPolicy, authPolicy, socketPolicy)