Use salt instead of device id for email settings

This commit is contained in:
mplorentz 2026-09-03 12:47:36 -04:00
parent 2b2515b1be
commit 42561c8c02
2 changed files with 38 additions and 7 deletions

View file

@ -4,9 +4,8 @@ import {Address, DELETE, makeEvent, makeHttpAuth, makeHttpAuthHeader} from "@wel
import {Relays, User} from "@welshman/app"
import {buildUrl} from "@lib/util"
import {app, thunks} from "@app/core"
import {device} from "@app/device"
import {EMAIL_NOTIFICATION_SERVER, PUSH_BRIDGE} from "@app/env"
import {userSettingsValues} from "@app/settings"
import {ensureEmailSalt, userSettingsValues} from "@app/settings"
import {emailNotificationState, syncRelaySubscriptions} from "@app/push/adapters/common"
export class MailNotifications {
@ -138,8 +137,11 @@ export class MailNotifications {
)
}
_getSubscriptionIdentifier = (relay: string, key: string) =>
String(hash(relay + key + device.get() + "email"))
_getSubscriptionIdentifier = async (relay: string, key: string) => {
const salt = await ensureEmailSalt()
return String(hash(relay + key + salt + "email"))
}
_getPushUrl = async (url: string) => {
for (const candidate of [url, PUSH_BRIDGE]) {
@ -161,7 +163,7 @@ export class MailNotifications {
return
}
const identifier = this._getSubscriptionIdentifier(relay, key)
const identifier = await this._getSubscriptionIdentifier(relay, key)
const thunk = thunks.get().publish({
relays: [url],
@ -188,7 +190,7 @@ export class MailNotifications {
if (!url) return
const $pubkey = User.require(app.get()).pubkey
const identifier = this._getSubscriptionIdentifier(relay, key)
const identifier = await this._getSubscriptionIdentifier(relay, key)
const address = new Address(30390, $pubkey, identifier).toString()
const event = makeEvent(DELETE, {tags: [["a", address]]})
const error = await thunks

View file

@ -1,5 +1,5 @@
import {derived, writable} from "svelte/store"
import {append, remove, spec} from "@welshman/lib"
import {append, call, randomId, remove, spec} from "@welshman/lib"
import {APP_DATA} from "@welshman/util"
import {withGetter} from "@welshman/store"
import {AppData} from "@welshman/domain"
@ -10,6 +10,8 @@ import {app, fromApp, usePlugin} from "@app/core"
export const SETTINGS = "flotilla/settings"
const emailSaltsByPubkey = new Map<string, Promise<string>>()
export enum RelayAuthMode {
Aggressive = "aggressive",
Conservative = "conservative",
@ -34,6 +36,7 @@ export type SettingsValues = {
email: boolean
emailAddress: string
emailFrequency: string
emailSalt: string
}
export const defaultSettings: SettingsValues = {
@ -49,6 +52,7 @@ export const defaultSettings: SettingsValues = {
email: false,
emailAddress: "",
emailFrequency: "daily",
emailSalt: "",
}
export class Settings extends DerivedPlugin<AppDataReader> {
@ -121,6 +125,31 @@ export const publishSettings = async (params: Partial<SettingsValues>) => {
return command.publish()
}
// A per-account salt for building deterministic identifiers (e.g. kind 30390 email
// subscription ids) that reproduce on any device of the user, stored with the settings.
export const ensureEmailSalt = () => {
const $pubkey = User.require(app.get()).pubkey
const existing = emailSaltsByPubkey.get($pubkey)
if (existing) return existing
// Force-load from the network so a salt synced by another device wins over a fresh one,
// and memoize the in-flight promise so parallel syncs (one per space relay) share a salt.
const promise = call(async () => {
await settings.get().forceLoad($pubkey)
const salt = userSettingsValues.get().emailSalt
if (salt) return salt
const emailSalt = randomId()
await publishSettings({emailSalt})
return emailSalt
})
emailSaltsByPubkey.set($pubkey, promise)
return promise
}
export const addTrustedRelay = (url: string) =>
publishSettings({trusted_relays: append(url, getSetting("trusted_relays"))})