Merge pull request #122 from nostrband/feature/perm-sync

Feature/perm sync
This commit is contained in:
Nostr.Band 2024-02-23 09:32:49 +03:00 committed by GitHub
commit 3c2d2f9f84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 171 additions and 22 deletions

View File

@ -10,7 +10,7 @@ import { isEmptyString } from '@/utils/helpers/helpers'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
import { selectApps } from '@/store' import { selectApps } from '@/store'
import { dbi } from '@/modules/db' import { DbApp, dbi } from '@/modules/db'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { setApps } from '@/store/reducers/content.slice' import { setApps } from '@/store/reducers/content.slice'
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner' import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
@ -20,7 +20,7 @@ export const ModalAppDetails = () => {
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS)
const handleCloseModal = createHandleCloseReplace(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 apps = useAppSelector(selectApps)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -33,8 +33,8 @@ export const ModalAppDetails = () => {
}) })
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const currentApp = apps.find((app) => app.appNpub === appNpub && app.npub === npub)
useEffect(() => { useEffect(() => {
const currentApp = apps.find((app) => app.appNpub === appNpub)
if (!currentApp) return if (!currentApp) return
setDetails({ setDetails({
@ -94,14 +94,15 @@ export const ModalAppDetails = () => {
const submitHandler = async (e: FormEvent) => { const submitHandler = async (e: FormEvent) => {
e.preventDefault() e.preventDefault()
if (isLoading) return undefined if (isLoading || !currentApp) return undefined
try { try {
setIsLoading(true) setIsLoading(true)
const updatedApp = { const updatedApp: DbApp = {
...currentApp,
url, url,
name, name,
icon, icon,
appNpub, updateTimestamp: Date.now()
} }
await dbi.updateApp(updatedApp) await dbi.updateApp(updatedApp)
const apps = await dbi.listApps() const apps = await dbi.listApps()

View File

@ -5,12 +5,26 @@ import NDK, {
NDKEvent, NDKEvent,
NDKNip46Backend, NDKNip46Backend,
NDKPrivateKeySigner, NDKPrivateKeySigner,
NDKRelaySet,
NDKSigner, NDKSigner,
NDKSubscription, NDKSubscription,
NDKSubscriptionCacheUsage, NDKSubscriptionCacheUsage,
NDKUser, NDKUser,
} from '@nostr-dev-kit/ndk' } 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 { Nip04 } from './nip04'
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers' import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
import { NostrPowEvent, minePow } from './pow' import { NostrPowEvent, minePow } from './pow'
@ -234,8 +248,9 @@ export class NoauthBackend {
private accessBuffer: DbPending[] = [] private accessBuffer: DbPending[] = []
private notifCallback: (() => void) | null = null private notifCallback: (() => void) | null = null
private pendingNpubEvents = new Map<string, NDKEvent[]>() private pendingNpubEvents = new Map<string, NDKEvent[]>()
private permSub?: NDKSubscription
private ndk = new NDK({ private ndk = new NDK({
explicitRelayUrls: NIP46_RELAYS, explicitRelayUrls: [...NIP46_RELAYS, ...OUTBOX_RELAYS, BROADCAST_RELAY],
enableOutboxModel: false, enableOutboxModel: false,
}) })
@ -313,8 +328,7 @@ export class NoauthBackend {
// drop old pending reqs // drop old pending reqs
const pending = await dbi.listPending() const pending = await dbi.listPending()
for (const p of pending) { for (const p of pending) {
if (p.timestamp < Date.now() - REQ_TTL) if (p.timestamp < Date.now() - REQ_TTL) await dbi.removePending(p.id)
await dbi.removePending(p.id)
} }
const sub = await this.swg.registration.pushManager.getSubscription() const sub = await this.swg.registration.pushManager.getSubscription()
@ -325,6 +339,82 @@ export class NoauthBackend {
// ensure we're subscribed on the server // ensure we're subscribed on the server
if (sub) await this.sendSubscriptionToServer(k.npub, sub) 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) { public setNotifCallback(cb: () => void) {
@ -712,6 +802,10 @@ export class NoauthBackend {
if (perm) { if (perm) {
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) 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 return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
} }
@ -724,6 +818,42 @@ export class NoauthBackend {
return DECISION.ASK 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({ private async connectApp({
npub, npub,
appNpub, appNpub,
@ -746,6 +876,7 @@ export class NoauthBackend {
name: appName, name: appName,
icon: appIcon, icon: appIcon,
url: appUrl, url: appUrl,
updateTimestamp: Date.now()
}) })
// reload // reload
@ -765,6 +896,9 @@ export class NoauthBackend {
// reload // reload
this.perms = await dbi.listPerms() this.perms = await dbi.listPerms()
// async
this.publishAppPerms({ npub, appNpub })
} }
private async allowPermitCallback({ private async allowPermitCallback({
@ -830,6 +964,7 @@ export class NoauthBackend {
name: '', name: '',
icon: '', icon: '',
url: options.appUrl || '', url: options.appUrl || '',
updateTimestamp: Date.now()
}) })
// reload // reload
@ -874,6 +1009,9 @@ export class NoauthBackend {
// to this req // to this req
ok(decision) ok(decision)
// async
this.publishAppPerms({ npub: req.npub, appNpub: req.appNpub })
// notify UI that it was confirmed // notify UI that it was confirmed
// if (!PERF_TEST) // if (!PERF_TEST)
this.updateUI() this.updateUI()
@ -933,7 +1071,7 @@ export class NoauthBackend {
// looping for 10 seconds (our request age threshold) // looping for 10 seconds (our request age threshold)
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl) backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
} else { } else {
console.log("skip sending auth_url") console.log('skip sending auth_url')
} }
}, 500) }, 500)
@ -1033,11 +1171,15 @@ export class NoauthBackend {
const { data: pubkey } = nip19.decode(npub) const { data: pubkey } = nip19.decode(npub)
const { data: appPubkey } = nip19.decode(appNpub) const { data: appPubkey } = nip19.decode(appNpub)
const events = await this.ndk.fetchEvents({ const events = await this.ndk.fetchEvents(
kinds: [KIND_RPC], {
'#p': [pubkey as string], kinds: [KIND_RPC],
authors: [appPubkey as string], '#p': [pubkey as string],
}) authors: [appPubkey as string],
},
undefined,
NDKRelaySet.fromRelayUrls(NIP46_RELAYS, this.ndk)
)
console.log('fetched pending for', npub, events.size) console.log('fetched pending for', npub, events.size)
this.pendingNpubEvents.set(npub, [...events.values()]) this.pendingNpubEvents.set(npub, [...events.values()])
} }

View File

@ -18,6 +18,7 @@ export interface DbApp {
icon: string icon: string
url: string url: string
timestamp: number timestamp: number
updateTimestamp: number
} }
export interface DbPerm { export interface DbPerm {
@ -63,9 +64,9 @@ export interface DbSchema extends Dexie {
export const db = new Dexie('noauthdb') as DbSchema export const db = new Dexie('noauthdb') as DbSchema
db.version(8).stores({ db.version(9).stores({
keys: 'npub', keys: 'npub',
apps: 'appNpub,npub,name,timestamp', apps: 'appNpub,npub,name,timestamp,updateTimestamp',
perms: 'id,npub,appNpub,perm,value,timestamp', perms: 'id,npub,appNpub,perm,value,timestamp',
pending: 'id,npub,appNpub,timestamp,method', pending: 'id,npub,appNpub,timestamp,method',
history: 'id,npub,appNpub,timestamp,method,allowed', history: 'id,npub,appNpub,timestamp,method,allowed',
@ -113,12 +114,13 @@ export const dbi = {
console.log(`db addApp error: ${error}`) console.log(`db addApp error: ${error}`)
} }
}, },
updateApp: async (app: Omit<DbApp, 'npub' | 'timestamp'>) => { updateApp: async (app: DbApp) => {
try { try {
await db.apps.where({ appNpub: app.appNpub }).modify({ await db.apps.where({ appNpub: app.appNpub }).modify({
name: app.name, name: app.name,
icon: app.icon, icon: app.icon,
url: app.url, url: app.url,
updateTimestamp: app.updateTimestamp
}) })
} catch (error) { } catch (error) {
console.log(`db updateApp error: ${error}`) console.log(`db updateApp error: ${error}`)

View File

@ -97,6 +97,4 @@ const darkTheme: Theme = createTheme({
}, },
}) })
console.log(darkTheme)
export { lightTheme, darkTheme } export { lightTheme, darkTheme }

View File

@ -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 DOMAIN = process.env.REACT_APP_DOMAIN
export const RELAY = process.env.REACT_APP_RELAY || 'wss://relay.nsec.app' export const RELAY = process.env.REACT_APP_RELAY || 'wss://relay.nsec.app'
export const NIP46_RELAYS = [RELAY] 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 MIN_POW = 14
export const MAX_POW = 19 export const MAX_POW = 19
export const KIND_RPC = 24133 export const KIND_RPC = 24133
export const KIND_DATA = 30078
export const RELOAD_STORAGE_KEY = 'reload' export const RELOAD_STORAGE_KEY = 'reload'

View File

@ -126,7 +126,8 @@ export const getReferrerAppUrl = () => {
if (!window.document.referrer) return '' if (!window.document.referrer) return ''
try { try {
const u = new URL(window.document.referrer.toLocaleLowerCase()) 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 {} } catch {}
return '' return ''
} }