diff --git a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx index 5465b7f..04355bb 100644 --- a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx +++ b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx @@ -10,7 +10,7 @@ import { isEmptyString } from '@/utils/helpers/helpers' import { useParams } from 'react-router-dom' import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' import { selectApps } from '@/store' -import { dbi } from '@/modules/db' +import { DbApp, dbi } from '@/modules/db' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { setApps } from '@/store/reducers/content.slice' import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner' @@ -20,7 +20,7 @@ export const ModalAppDetails = () => { const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS) - const { appNpub = '' } = useParams() + const { npub = '', appNpub = '' } = useParams() const apps = useAppSelector(selectApps) const dispatch = useAppDispatch() @@ -33,8 +33,8 @@ export const ModalAppDetails = () => { }) const [isLoading, setIsLoading] = useState(false) + const currentApp = apps.find((app) => app.appNpub === appNpub && app.npub === npub) useEffect(() => { - const currentApp = apps.find((app) => app.appNpub === appNpub) if (!currentApp) return setDetails({ @@ -94,14 +94,15 @@ export const ModalAppDetails = () => { const submitHandler = async (e: FormEvent) => { e.preventDefault() - if (isLoading) return undefined + if (isLoading || !currentApp) return undefined try { setIsLoading(true) - const updatedApp = { + const updatedApp: DbApp = { + ...currentApp, url, name, icon, - appNpub, + updateTimestamp: Date.now() } await dbi.updateApp(updatedApp) const apps = await dbi.listApps() diff --git a/src/modules/backend.ts b/src/modules/backend.ts index ed98d1d..572d9c5 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -5,12 +5,26 @@ import NDK, { NDKEvent, NDKNip46Backend, NDKPrivateKeySigner, + NDKRelaySet, NDKSigner, NDKSubscription, NDKSubscriptionCacheUsage, NDKUser, } from '@nostr-dev-kit/ndk' -import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN, REQ_TTL } from '../utils/consts' +import { + NOAUTHD_URL, + WEB_PUSH_PUBKEY, + NIP46_RELAYS, + MIN_POW, + MAX_POW, + KIND_RPC, + DOMAIN, + REQ_TTL, + KIND_DATA, + OUTBOX_RELAYS, + BROADCAST_RELAY, + APP_TAG, +} from '../utils/consts' // import { Nip04 } from './nip04' import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers' import { NostrPowEvent, minePow } from './pow' @@ -234,8 +248,9 @@ export class NoauthBackend { private accessBuffer: DbPending[] = [] private notifCallback: (() => void) | null = null private pendingNpubEvents = new Map() + private permSub?: NDKSubscription private ndk = new NDK({ - explicitRelayUrls: NIP46_RELAYS, + explicitRelayUrls: [...NIP46_RELAYS, ...OUTBOX_RELAYS, BROADCAST_RELAY], enableOutboxModel: false, }) @@ -313,8 +328,7 @@ export class NoauthBackend { // drop old pending reqs const pending = await dbi.listPending() for (const p of pending) { - if (p.timestamp < Date.now() - REQ_TTL) - await dbi.removePending(p.id) + if (p.timestamp < Date.now() - REQ_TTL) await dbi.removePending(p.id) } const sub = await this.swg.registration.pushManager.getSubscription() @@ -325,6 +339,82 @@ export class NoauthBackend { // ensure we're subscribed on the server if (sub) await this.sendSubscriptionToServer(k.npub, sub) } + +// this.subscribeToAppPerms() + } + + private async subscribeToAppPerms() { + if (this.permSub) { + this.permSub.stop() + this.permSub.removeAllListeners() + this.permSub = undefined + } + + const authors = this.keys.map((k) => nip19.decode(k.npub).data as string) + this.permSub = this.ndk.subscribe( + { + authors, + kinds: [KIND_DATA], + '#t': [APP_TAG], + limit: 100, + }, + { + closeOnEose: false, + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + }, + NDKRelaySet.fromRelayUrls(OUTBOX_RELAYS, this.ndk), + true // auto-start + ) + this.permSub.on('event', async (e) => { + const npub = nip19.npubEncode(e.pubkey) + const key = this.keys.find((k) => k.npub === npub) + if (!key) return + + // parse + try { + const payload = await key.signer.decrypt(new NDKUser({ pubkey: e.pubkey }), e.content) + const data = JSON.parse(payload) + console.log('Got app perm event', { e, data }) + // FIXME validate first! + await this.mergeAppPerms(key, data) + } catch (err) { + console.log('Bad app perm event', e, err) + } + + // notify UI + this.updateUI() + }) + } + + private async mergeAppPerms(key: Key, data: any) { + let app = this.apps.find(a => a.appNpub === data.appNpub) + const appFromData = (): DbApp => { + return { + npub: data.npub, + appNpub: data.appNpub, + name: data.name, + icon: data.icon, + url: data.url, + // choose older creation timestamp + timestamp: app ? Math.min(app.timestamp, data.timestamp) : data.timestamp, + updateTimestamp: data.updateTimestamp + } + } + if (!app) { + // new app + app = appFromData() + console.log("New app from event", { data, app }) + await dbi.addApp(app) + } else if (app.updateTimestamp < data.updateTimestamp) { + // update existing app + app = appFromData() + await dbi.updateApp(app) + } else { + // old data + console.log("skip old app perms", { data, app }) + } + + // FIXME merge perms } public setNotifCallback(cb: () => void) { @@ -712,6 +802,10 @@ export class NoauthBackend { if (perm) { console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) + // connect reqs are always 'ignore' if were disallowed + if (perm.perm === 'connect' && perm.value === '0') return DECISION.IGNORE + + // all other reqs are not ignored return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW } @@ -724,6 +818,42 @@ export class NoauthBackend { return DECISION.ASK } + private async publishAppPerms({ npub, appNpub }: { npub: string; appNpub: string }) { + const key = this.keys.find((k) => k.npub === npub) + if (!key) throw new Error('Key not found') + const app = this.apps.find((a) => a.appNpub === appNpub && a.npub === npub) + if (!app) throw new Error('App not found') + const perms = this.perms.filter((p) => p.appNpub === appNpub && p.npub === npub) + const data = { + appNpub, + npub, + name: app.name, + icon: app.icon, + url: app.url, + timestamp: app.timestamp, + updateTimestamp: app.updateTimestamp, + perms, + } + const id = await this.sha256(`nsec.app_${npub}_${appNpub}`) + const { type, data: pubkey } = nip19.decode(npub) + if (type !== 'npub') throw new Error('Bad npub') + const content = await key.signer.encrypt(new NDKUser({ pubkey }), JSON.stringify(data)) + const event = new NDKEvent(this.ndk, { + pubkey, + kind: KIND_DATA, + content, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['d', id], + ['t', APP_TAG], + ], + }) + event.sig = await event.sign(key.signer) + console.log('app perms event', event.rawEvent(), 'payload', data) + const relays = await event.publish(NDKRelaySet.fromRelayUrls([...OUTBOX_RELAYS, BROADCAST_RELAY], this.ndk)) + console.log('app perm event published', event.id, 'to', relays) + } + private async connectApp({ npub, appNpub, @@ -746,6 +876,7 @@ export class NoauthBackend { name: appName, icon: appIcon, url: appUrl, + updateTimestamp: Date.now() }) // reload @@ -765,6 +896,9 @@ export class NoauthBackend { // reload this.perms = await dbi.listPerms() + + // async + this.publishAppPerms({ npub, appNpub }) } private async allowPermitCallback({ @@ -830,6 +964,7 @@ export class NoauthBackend { name: '', icon: '', url: options.appUrl || '', + updateTimestamp: Date.now() }) // reload @@ -874,6 +1009,9 @@ export class NoauthBackend { // to this req ok(decision) + // async + this.publishAppPerms({ npub: req.npub, appNpub: req.appNpub }) + // notify UI that it was confirmed // if (!PERF_TEST) this.updateUI() @@ -933,7 +1071,7 @@ export class NoauthBackend { // looping for 10 seconds (our request age threshold) backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl) } else { - console.log("skip sending auth_url") + console.log('skip sending auth_url') } }, 500) @@ -1033,11 +1171,15 @@ export class NoauthBackend { const { data: pubkey } = nip19.decode(npub) const { data: appPubkey } = nip19.decode(appNpub) - const events = await this.ndk.fetchEvents({ - kinds: [KIND_RPC], - '#p': [pubkey as string], - authors: [appPubkey as string], - }) + const events = await this.ndk.fetchEvents( + { + kinds: [KIND_RPC], + '#p': [pubkey as string], + authors: [appPubkey as string], + }, + undefined, + NDKRelaySet.fromRelayUrls(NIP46_RELAYS, this.ndk) + ) console.log('fetched pending for', npub, events.size) this.pendingNpubEvents.set(npub, [...events.values()]) } diff --git a/src/modules/db.ts b/src/modules/db.ts index 982ad24..c6991a8 100644 --- a/src/modules/db.ts +++ b/src/modules/db.ts @@ -18,6 +18,7 @@ export interface DbApp { icon: string url: string timestamp: number + updateTimestamp: number } export interface DbPerm { @@ -63,9 +64,9 @@ export interface DbSchema extends Dexie { export const db = new Dexie('noauthdb') as DbSchema -db.version(8).stores({ +db.version(9).stores({ keys: 'npub', - apps: 'appNpub,npub,name,timestamp', + apps: 'appNpub,npub,name,timestamp,updateTimestamp', perms: 'id,npub,appNpub,perm,value,timestamp', pending: 'id,npub,appNpub,timestamp,method', history: 'id,npub,appNpub,timestamp,method,allowed', @@ -113,12 +114,13 @@ export const dbi = { console.log(`db addApp error: ${error}`) } }, - updateApp: async (app: Omit) => { + updateApp: async (app: DbApp) => { try { await db.apps.where({ appNpub: app.appNpub }).modify({ name: app.name, icon: app.icon, url: app.url, + updateTimestamp: app.updateTimestamp }) } catch (error) { console.log(`db updateApp error: ${error}`) diff --git a/src/modules/theme/theme.ts b/src/modules/theme/theme.ts index beb0ff6..4eaeca7 100644 --- a/src/modules/theme/theme.ts +++ b/src/modules/theme/theme.ts @@ -97,6 +97,4 @@ const darkTheme: Theme = createTheme({ }, }) -console.log(darkTheme) - export { lightTheme, darkTheme } diff --git a/src/utils/consts.ts b/src/utils/consts.ts index 4466b9a..f6737e1 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -3,11 +3,16 @@ export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY export const DOMAIN = process.env.REACT_APP_DOMAIN export const RELAY = process.env.REACT_APP_RELAY || 'wss://relay.nsec.app' export const NIP46_RELAYS = [RELAY] +export const OUTBOX_RELAYS = ['wss://relay.nostr.band', 'wss://nos.lol', 'wss://purplepag.es'] +export const BROADCAST_RELAY = 'wss://nostr.mutinywallet.com' + +export const APP_TAG = 'nsec.app/perm' export const MIN_POW = 14 export const MAX_POW = 19 export const KIND_RPC = 24133 +export const KIND_DATA = 30078 export const RELOAD_STORAGE_KEY = 'reload' diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index 3977126..68b049b 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -126,7 +126,8 @@ export const getReferrerAppUrl = () => { if (!window.document.referrer) return '' try { const u = new URL(window.document.referrer.toLocaleLowerCase()) - if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN)) return u.origin + if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN) && u.origin != window.location.origin) + return u.origin } catch {} return '' }