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

View file

@ -1,5 +1,5 @@
import {derived, writable} from "svelte/store" 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 {APP_DATA} from "@welshman/util"
import {withGetter} from "@welshman/store" import {withGetter} from "@welshman/store"
import {AppData} from "@welshman/domain" import {AppData} from "@welshman/domain"
@ -10,6 +10,8 @@ import {app, fromApp, usePlugin} from "@app/core"
export const SETTINGS = "flotilla/settings" export const SETTINGS = "flotilla/settings"
const emailSaltsByPubkey = new Map<string, Promise<string>>()
export enum RelayAuthMode { export enum RelayAuthMode {
Aggressive = "aggressive", Aggressive = "aggressive",
Conservative = "conservative", Conservative = "conservative",
@ -34,6 +36,7 @@ export type SettingsValues = {
email: boolean email: boolean
emailAddress: string emailAddress: string
emailFrequency: string emailFrequency: string
emailSalt: string
} }
export const defaultSettings: SettingsValues = { export const defaultSettings: SettingsValues = {
@ -49,6 +52,7 @@ export const defaultSettings: SettingsValues = {
email: false, email: false,
emailAddress: "", emailAddress: "",
emailFrequency: "daily", emailFrequency: "daily",
emailSalt: "",
} }
export class Settings extends DerivedPlugin<AppDataReader> { export class Settings extends DerivedPlugin<AppDataReader> {
@ -121,6 +125,31 @@ export const publishSettings = async (params: Partial<SettingsValues>) => {
return command.publish() 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) => export const addTrustedRelay = (url: string) =>
publishSettings({trusted_relays: append(url, getSetting("trusted_relays"))}) publishSettings({trusted_relays: append(url, getSetting("trusted_relays"))})