rework permissions and popup prompts, make each permission fine grained.

This commit is contained in:
fiatjaf
2023-06-10 22:26:49 -03:00
parent 4759ce6d36
commit 0b1d849f19
6 changed files with 447 additions and 386 deletions

View File

@@ -10,9 +10,8 @@ import {nip04} from 'nostr-tools'
import {Mutex} from 'async-mutex' import {Mutex} from 'async-mutex'
import { import {
PERMISSIONS_REQUIRED,
NO_PERMISSIONS_REQUIRED, NO_PERMISSIONS_REQUIRED,
readPermissionLevel, getPermissionStatus,
updatePermission updatePermission
} from './common' } from './common'
@@ -90,24 +89,60 @@ async function handleContentScriptMessage({type, params, host}) {
return return
} else { } else {
let level = await readPermissionLevel(host) // acquire mutex here before reading policies
releasePromptMutex = await promptMutex.acquire()
if (level >= PERMISSIONS_REQUIRED[type]) { let allowed = await getPermissionStatus(
host,
type,
type === 'signEvent' ? params.event : undefined
)
if (allowed === true) {
// authorized, proceed // authorized, proceed
releasePromptMutex()
} else if (allowed === false) {
// denied, just refuse immediately
releasePromptMutex()
return {
error: 'denied'
}
} else { } else {
// ask for authorization // ask for authorization
try { try {
await promptPermission(host, PERMISSIONS_REQUIRED[type], params) let id = Math.random().toString().slice(4)
// authorized, proceed let qs = new URLSearchParams({
} catch (_) { host,
// not authorized, stop here id,
params: JSON.stringify(params),
type
})
// prompt will be resolved with true or false
let accept = await new Promise((resolve, reject) => {
openPrompt = {resolve, reject}
browser.windows.create({
url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`,
type: 'popup',
width: 340,
height: 360
})
})
// denied, stop here
if (!accept) return {error: 'denied'}
} catch (err) {
// errored, stop here
releasePromptMutex()
return { return {
error: `insufficient permissions, required ${PERMISSIONS_REQUIRED[type]}` error: `error: ${err}`
} }
} }
} }
} }
// if we're here this means it was accepted
let results = await browser.storage.local.get('private_key') let results = await browser.storage.local.get('private_key')
if (!results || !results.private_key) { if (!results || !results.private_key) {
return {error: 'no private key found'} return {error: 'no private key found'}
@@ -148,51 +183,23 @@ async function handleContentScriptMessage({type, params, host}) {
} }
} }
function handlePromptMessage({id, condition, host, level}, sender) { function handlePromptMessage({id, host, type, accept, conditions}, sender) {
switch (condition) { // return response
case 'forever': openPrompt?.resolve?.(accept)
case 'expirable':
openPrompt?.resolve?.() // update policies
updatePermission(host, { if (conditions) {
level, updatePermission(host, type, accept, conditions)
condition
})
break
case 'single':
openPrompt?.resolve?.()
break
case 'no':
openPrompt?.reject?.()
break
} }
// cleanup this
openPrompt = null openPrompt = null
// release mutex here after updating policies
releasePromptMutex() releasePromptMutex()
// close prompt
if (sender) { if (sender) {
browser.windows.remove(sender.tab.windowId) browser.windows.remove(sender.tab.windowId)
} }
} }
async function promptPermission(host, level, params) {
releasePromptMutex = await promptMutex.acquire()
let id = Math.random().toString().slice(4)
let qs = new URLSearchParams({
host,
level,
id,
params: JSON.stringify(params)
})
return new Promise((resolve, reject) => {
openPrompt = {resolve, reject}
browser.windows.create({
url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`,
type: 'popup',
width: 340,
height: 330
})
})
}

View File

@@ -4,90 +4,90 @@ export const NO_PERMISSIONS_REQUIRED = {
replaceURL: true replaceURL: true
} }
export const PERMISSIONS_REQUIRED = { export const PERMISSION_NAMES = Object.fromEntries([
getPublicKey: 1, ['getPublicKey', 'read your public key'],
getRelays: 5, ['getRelays', 'read your list of preferred relays'],
signEvent: 10, ['signEvent', 'sign events using your private key'],
'nip04.encrypt': 20, ['nip04.encrypt', 'encrypt messages to peers'],
'nip04.decrypt': 20, ['nip04.decrypt', 'decrypt messages from peers']
} ])
const ORDERED_PERMISSIONS = [ function matchConditions(conditions, event) {
[1, ['getPublicKey']], if (conditions?.kinds) {
[5, ['getRelays']], if (event.kind in conditions.kinds) return true
[10, ['signEvent']], else return false
[20, ['nip04.encrypt']],
[20, ['nip04.decrypt']]
]
const PERMISSION_NAMES = {
getPublicKey: 'read your public key',
getRelays: 'read your list of preferred relays',
signEvent: 'sign events using your private key',
'nip04.encrypt': 'encrypt messages to peers',
'nip04.decrypt': 'decrypt messages from peers',
}
export function getAllowedCapabilities(permission) {
let requestedMethods = []
for (let i = 0; i < ORDERED_PERMISSIONS.length; i++) {
let [perm, methods] = ORDERED_PERMISSIONS[i]
if (perm > permission) break
requestedMethods = requestedMethods.concat(methods)
} }
if (requestedMethods.length === 0) return 'nothing' return true
return requestedMethods.map(method => PERMISSION_NAMES[method])
} }
export function getPermissionsString(permission) { export async function getPermissionStatus(host, type, event) {
let capabilities = getAllowedCapabilities(permission) let {policies} = await browser.storage.local.get('policies')
if (capabilities.length === 0) return 'none' let answers = [true, false]
if (capabilities.length === 1) return capabilities[0] for (let i = 0; i < answers.length; i++) {
let accept = answers[i]
let {conditions} = policies?.[host]?.[accept]?.[type] || {}
return ( if (conditions) {
capabilities.slice(0, -1).join(', ') + if (type === 'signEvent') {
' and ' + if (matchConditions(conditions, event)) {
capabilities[capabilities.length - 1] return accept // may be true or false
) } else {
} // if this doesn't match we just continue so it will either match for the opposite answer (reject)
// or it will end up returning undefined at the end
export async function readPermissions() { continue
let {permissions = {}} = await browser.storage.local.get('permissions') }
} else {
// delete expired return accept // may be true or false
var needsUpdate = false }
for (let host in permissions) {
if (
permissions[host].condition === 'expirable' &&
permissions[host].created_at < Date.now() / 1000 - 5 * 60
) {
delete permissions[host]
needsUpdate = true
} }
} }
if (needsUpdate) browser.storage.local.set({permissions})
return permissions return undefined
} }
export async function readPermissionLevel(host) { export async function updatePermission(host, type, accept, conditions) {
return (await readPermissions())[host]?.level || 0 let {policies = {}} = await browser.storage.local.get('policies')
}
export async function updatePermission(host, permission) { // if the new conditions is "match everything", override the previous
let {permissions = {}} = await browser.storage.local.get('permissions') if (Object.keys(conditions).length === 0) {
permissions[host] = { conditions = {}
...permission, } else {
// if we already had a policy for this, merge the conditions
let existingConditions = policies[host]?.[accept]?.[type]?.conditions
if (existingConditions) {
if (existingConditions.kinds && conditions.kinds) {
Object.keys(existingConditions.kinds).forEach(kind => {
conditions.kinds[kind] = true
})
}
}
}
// if we have a reverse policy (accept / reject) that is exactly equal to this, remove it
let other = !accept
let reverse = policies?.[host]?.[other]?.[type]
if (
reverse &&
JSON.stringify(reverse.conditions) === JSON.stringify(conditions)
) {
delete policies[host][other][type]
}
// insert our new policy
policies[host] = policies[host] || {}
policies[host][accept] = policies[host][accept] || {}
policies[host][accept][type] = {
conditions, // filter that must match the event (in case of signEvent)
created_at: Math.round(Date.now() / 1000) created_at: Math.round(Date.now() / 1000)
} }
browser.storage.local.set({permissions})
browser.storage.local.set({policies})
} }
export async function removePermissions(host) { export async function removePermissions(host, accept, type) {
let {permissions = {}} = await browser.storage.local.get('permissions') let {policies = {}} = await browser.storage.local.get('policies')
delete permissions[host] delete policies[host]
browser.storage.local.set({permissions}) browser.storage.local.set({policies})
} }

View File

@@ -4,18 +4,14 @@ import {render} from 'react-dom'
import {generatePrivateKey, getPublicKey, nip19} from 'nostr-tools' import {generatePrivateKey, getPublicKey, nip19} from 'nostr-tools'
import QRCode from 'react-qr-code' import QRCode from 'react-qr-code'
import { import {removePermissions, PERMISSION_NAMES} from './common'
getPermissionsString,
readPermissions,
removePermissions
} from './common'
function Options() { function Options() {
let [pubKey, setPubKey] = useState('') let [pubKey, setPubKey] = useState('')
let [privKey, setPrivKey] = useState('') let [privKey, setPrivKey] = useState('')
let [relays, setRelays] = useState([]) let [relays, setRelays] = useState([])
let [newRelayURL, setNewRelayURL] = useState('') let [newRelayURL, setNewRelayURL] = useState('')
let [permissions, setPermissions] = useState() let [policies, setPermissions] = useState()
let [protocolHandler, setProtocolHandler] = useState(null) let [protocolHandler, setProtocolHandler] = useState(null)
let [hidingPrivateKey, hidePrivateKey] = useState(true) let [hidingPrivateKey, hidePrivateKey] = useState(true)
let [message, setMessage] = useState('') let [message, setMessage] = useState('')
@@ -28,217 +24,241 @@ function Options() {
useEffect(() => { useEffect(() => {
browser.storage.local browser.storage.local
.get(['private_key', 'relays', 'protocol_handler']) .get(['private_key', 'relays', 'protocol_handler'])
.then(results => { .then(results => {
if (results.private_key) { if (results.private_key) {
setPrivKey(nip19.nsecEncode(results.private_key)) setPrivKey(nip19.nsecEncode(results.private_key))
let hexKey = getPublicKey(results.private_key) let hexKey = getPublicKey(results.private_key)
let npubKey = nip19.npubEncode(hexKey) let npubKey = nip19.npubEncode(hexKey)
setPubKey(npubKey) setPubKey(npubKey)
}
if (results.relays) {
let relaysList = []
for (let url in results.relays) {
relaysList.push({
url,
policy: results.relays[url]
})
} }
setRelays(relaysList) if (results.relays) {
} let relaysList = []
if (results.protocol_handler) { for (let url in results.relays) {
setProtocolHandler(results.protocol_handler) relaysList.push({
} url,
}) policy: results.relays[url]
})
}
setRelays(relaysList)
}
if (results.protocol_handler) {
setProtocolHandler(results.protocol_handler)
}
})
}, []) }, [])
useEffect(() => { useEffect(() => {
loadPermissions() loadPermissions()
}, []) }, [])
function loadPermissions() { async function loadPermissions() {
readPermissions().then(permissions => { let {policies = {}} = await browser.storage.local.get('policies')
setPermissions( let list = []
Object.entries(permissions).map(
([host, {level, condition, created_at}]) => ({ Object.entries(policies).forEach(([host, accepts]) => {
host, Object.entries(accepts).forEach(([accept, types]) => {
level, Object.entries(types).forEach(([type, {conditions, created_at}]) => {
condition, list.push({
created_at host,
}) type,
) accept: {true: 'allow', false: 'deny'}[accept],
) conditions,
created_at
})
})
})
}) })
setPermissions(list)
} }
return ( return (
<> <>
<h1>nos2x</h1> <h1>nos2x</h1>
<p>nostr signer extension</p> <p>nostr signer extension</p>
<h2>options</h2> <h2>options</h2>
<div style={{marginBottom: '10px'}}> <div style={{marginBottom: '10px'}}>
<div style={{display: 'flex', alignItems: 'center'}}> <div style={{display: 'flex', alignItems: 'center'}}>
<span>preferred relays:</span> <span>preferred relays:</span>
<button style={{marginLeft: '20px'}} onClick={saveRelays}> <button style={{marginLeft: '20px'}} onClick={saveRelays}>
save save
</button> </button>
</div> </div>
<div style={{marginLeft: '10px'}}> <div style={{marginLeft: '10px'}}>
{relays.map(({url, policy}, i) => ( {relays.map(({url, policy}, i) => (
<div key={i} style={{display: 'flex'}}> <div key={i} style={{display: 'flex'}}>
<input
style={{marginRight: '10px', width: '400px'}}
value={url}
onChange={changeRelayURL.bind(null, i)}
/>
<label>
read
<input
type="checkbox"
checked={policy.read}
onChange={toggleRelayPolicy.bind(null, i, 'read')}
/>
</label>
<label>
write
<input
type="checkbox"
checked={policy.write}
onChange={toggleRelayPolicy.bind(null, i, 'write')}
/>
</label>
</div>
))}
<div style={{display: 'flex'}}>
<input <input
style={{width: '400px'}} style={{marginRight: '10px', width: '400px'}}
value={newRelayURL} value={url}
onChange={e => setNewRelayURL(e.target.value)} onChange={changeRelayURL.bind(null, i)}
onBlur={addNewRelay}
/> />
<label>
read
<input
type="checkbox"
checked={policy.read}
onChange={toggleRelayPolicy.bind(null, i, 'read')}
/>
</label>
<label>
write
<input
type="checkbox"
checked={policy.write}
onChange={toggleRelayPolicy.bind(null, i, 'write')}
/>
</label>
</div> </div>
))}
<div style={{display: 'flex'}}>
<input
style={{width: '400px'}}
value={newRelayURL}
onChange={e => setNewRelayURL(e.target.value)}
onBlur={addNewRelay}
/>
</div> </div>
</div> </div>
<div style={{marginBottom: '10px'}}> </div>
<label> <div style={{marginBottom: '10px'}}>
<div>private key:&nbsp;</div> <label>
<div style={{marginLeft: '10px'}}> <div>private key:&nbsp;</div>
<div style={{display: 'flex'}}> <div style={{marginLeft: '10px'}}>
<input <div style={{display: 'flex'}}>
type={hidingPrivateKey ? 'password' : 'text'} <input
style={{width: '600px'}} type={hidingPrivateKey ? 'password' : 'text'}
value={privKey} style={{width: '600px'}}
onChange={handleKeyChange} value={privKey}
onFocus={() => hidePrivateKey(false)} onChange={handleKeyChange}
onBlur={() => hidePrivateKey(true)} onFocus={() => hidePrivateKey(false)}
/> onBlur={() => hidePrivateKey(true)}
{privKey === '' && <button onClick={generate}>generate</button>} />
</div> {privKey === '' && <button onClick={generate}>generate</button>}
<button disabled={!isKeyValid()} onClick={saveKey}>
save
</button>
<button disabled={!isKeyValid()} onClick={() => setShowQR('priv')}>
Show QR for private key
</button>
<button disabled={!isKeyValid()} onClick={() => setShowQR('pub')}>
Show QR for public key
</button>
{ showQR && (
<div id={'qrCodeDiv'} style={{ height: 'auto', maxWidth: 256, width: '100%', marginTop: '20px', marginBottom: '30px' }}>
<QRCode
size={256}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
value={showQR === 'priv' ? privKey : pubKey}
viewBox={`0 0 256 256`}
/>
</div>
)}
</div> </div>
</label>
{permissions?.length > 0 && ( <button disabled={!isKeyValid()} onClick={saveKey}>
<> save
<h2>permissions</h2> </button>
<table>
<thead> <button disabled={!isKeyValid()} onClick={() => setShowQR('priv')}>
<tr> Show QR for private key
<th>domain</th> </button>
<th>permissions</th>
<th>condition</th> <button disabled={!isKeyValid()} onClick={() => setShowQR('pub')}>
<th>since</th> Show QR for public key
<th></th> </button>
</tr>
</thead> {showQR && (
<tbody> <div
{permissions.map(({host, level, condition, created_at}) => ( id={'qrCodeDiv'}
<tr key={host}> style={{
<td>{host}</td> height: 'auto',
<td>{getPermissionsString(level)}</td> maxWidth: 256,
<td>{condition}</td> width: '100%',
<td> marginTop: '20px',
{new Date(created_at * 1000) marginBottom: '30px'
}}
>
<QRCode
size={256}
style={{height: 'auto', maxWidth: '100%', width: '100%'}}
value={showQR === 'priv' ? privKey : pubKey}
viewBox={`0 0 256 256`}
/>
</div>
)}
</div>
</label>
{policies?.length > 0 && (
<>
<h2>policies</h2>
<table>
<thead>
<tr>
<th>domain</th>
<th>permission</th>
<th>answer</th>
<th>conditions</th>
<th>since</th>
<th></th>
</tr>
</thead>
<tbody>
{policies.map(
({host, type, accept, conditions, created_at}) => (
<tr key={host}>
<td>{host}</td>
<td>{PERMISSION_NAMES[type]}</td>
<td>{accept}</td>
<td>{JSON.stringify(conditions).slice(1, -1)}</td>
<td>
{new Date(created_at * 1000)
.toISOString() .toISOString()
.split('.')[0] .split('.')[0]
.split('T') .split('T')
.join(' ')} .join(' ')}
</td> </td>
<td> <td>
<button onClick={handleRevoke} data-domain={host}> <button
revoke onClick={handleRevoke}
</button> data-host={host}
</td> data-accept={accept}
</tr> data-type={type}
))} >
</tbody> revoke
</table> </button>
</> </td>
)} </tr>
</div> )
<div> )}
<h2> </tbody>
handle{' '} </table>
<span style={{padding: '2px', background: 'silver'}}>nostr:</span>{' '} </>
links: )}
</h2> </div>
<div style={{marginLeft: '10px'}}> <div>
<h2>
handle{' '}
<span style={{padding: '2px', background: 'silver'}}>nostr:</span>{' '}
links:
</h2>
<div style={{marginLeft: '10px'}}>
<div>
<label>
<input
type="radio"
name="ph"
value="no"
checked={protocolHandler === null}
onChange={handleChangeProtocolHandler}
/>{' '}
no
</label>
</div>
<div>
<label>
<input
type="radio"
name="ph"
value="yes"
checked={protocolHandler !== null}
onChange={handleChangeProtocolHandler}
/>
yes
</label>
</div>
{protocolHandler !== null && (
<div> <div>
<label> <input
<input placeholder="url template"
type="radio" value={protocolHandler}
name="ph" onChange={handleChangeProtocolHandler}
value="no" style={{width: '680px', maxWidth: '90%'}}
checked={protocolHandler === null} />
onChange={handleChangeProtocolHandler} <pre>{`
/>{' '}
no
</label>
</div>
<div>
<label>
<input
type="radio"
name="ph"
value="yes"
checked={protocolHandler !== null}
onChange={handleChangeProtocolHandler}
/>
yes
</label>
</div>
{protocolHandler !== null && (
<div>
<input
placeholder="url template"
value={protocolHandler}
onChange={handleChangeProtocolHandler}
style={{width: '680px', maxWidth: '90%'}}
/>
<pre>{`
{hex} = hex pubkey for npub or nprofile, hex event id for note or nevent {hex} = hex pubkey for npub or nprofile, hex event id for note or nevent
{p_or_e} = "p" for npub or nprofile, "e" for note or nevent {p_or_e} = "p" for npub or nprofile, "e" for note or nevent
{u_or_n} = "u" for npub or nprofile, "n" for note or nevent {u_or_n} = "u" for npub or nprofile, "n" for note or nevent
@@ -253,18 +273,18 @@ function Options() {
- https://brb.io/{u_or_n}/{hex} - https://brb.io/{u_or_n}/{hex}
- https://notes.blockcore.net/{p_or_e}/{hex} - https://notes.blockcore.net/{p_or_e}/{hex}
`}</pre> `}</pre>
</div> </div>
)} )}
<button <button
style={{marginTop: '10px'}} style={{marginTop: '10px'}}
onClick={saveNostrProtocolHandlerSettings} onClick={saveNostrProtocolHandlerSettings}
> >
save save
</button> </button>
</div>
</div> </div>
<div style={{marginTop: '12px', fontSize: '120%'}}>{message}</div> </div>
</> <div style={{marginTop: '12px', fontSize: '120%'}}>{message}</div>
</>
) )
async function handleKeyChange(e) { async function handleKeyChange(e) {
@@ -335,10 +355,16 @@ function Options() {
} }
async function handleRevoke(e) { async function handleRevoke(e) {
let host = e.target.dataset.domain let {host, accept, type} = e.target.dataset
if (window.confirm(`revoke all permissions from ${host}?`)) { if (
await removePermissions(host) window.confirm(
showMessage(`removed permissions from ${host}`) `revoke all ${
accept ? 'accept' : 'deny'
} ${type} policies from ${host}?`
)
) {
await removePermissions(host, accept, type)
showMessage('removed policies')
loadPermissions() loadPermissions()
} }
} }
@@ -346,7 +372,7 @@ function Options() {
async function saveRelays() { async function saveRelays() {
await browser.storage.local.set({ await browser.storage.local.set({
relays: Object.fromEntries( relays: Object.fromEntries(
relays relays
.filter(({url}) => url.trim() !== '') .filter(({url}) => url.trim() !== '')
.map(({url, policy}) => [url.trim(), policy]) .map(({url, policy}) => [url.trim(), policy])
) )

View File

@@ -2,17 +2,18 @@ import browser from 'webextension-polyfill'
import {render} from 'react-dom' import {render} from 'react-dom'
import React from 'react' import React from 'react'
import {getAllowedCapabilities} from './common' import {PERMISSION_NAMES} from './common'
function Prompt() { function Prompt() {
let qs = new URLSearchParams(location.search) let qs = new URLSearchParams(location.search)
let id = qs.get('id') let id = qs.get('id')
let host = qs.get('host') let host = qs.get('host')
let level = parseInt(qs.get('level')) let type = qs.get('type')
let params let params, event
try { try {
params = JSON.parse(qs.get('params')) params = JSON.parse(qs.get('params'))
if (Object.keys(params).length === 0) params = null if (Object.keys(params).length === 0) params = null
else if (params.event) event = params.event
} catch (err) { } catch (err) {
params = null params = null
} }
@@ -23,20 +24,15 @@ function Prompt() {
<b style={{display: 'block', textAlign: 'center', fontSize: '200%'}}> <b style={{display: 'block', textAlign: 'center', fontSize: '200%'}}>
{host} {host}
</b>{' '} </b>{' '}
<p>is requesting your permission to </p> <p>
<ul> is requesting your permission to <b>{PERMISSION_NAMES[type]}:</b>
{getAllowedCapabilities(level).map(cap => ( </p>
<li key={cap}>
<i style={{fontSize: '140%'}}>{cap}</i>
</li>
))}
</ul>
</div> </div>
{params && ( {params && (
<> <>
<p>now acting on</p> <p>now acting on</p>
<pre style={{overflow: 'auto', maxHeight: '100px'}}> <pre style={{overflow: 'auto', maxHeight: '120px'}}>
<code>{JSON.stringify(params, null, 2)}</code> <code>{JSON.stringify(event || params, null, 2)}</code>
</pre> </pre>
</> </>
)} )}
@@ -49,35 +45,65 @@ function Prompt() {
> >
<button <button
style={{marginTop: '5px'}} style={{marginTop: '5px'}}
onClick={authorizeHandler('forever')} onClick={authorizeHandler(
true,
{} // store this and answer true forever
)}
> >
authorize forever authorize forever
</button> </button>
<button {event?.kind !== undefined && (
style={{marginTop: '5px'}} <button
onClick={authorizeHandler('expirable')} style={{marginTop: '5px'}}
> onClick={authorizeHandler(
authorize for 5 minutes true,
</button> {kinds: {[event.kind]: true}} // store and always answer true for all events that match this condition
<button style={{marginTop: '5px'}} onClick={authorizeHandler('single')}> )}
>
authorize kind {event.kind} forever
</button>
)}
<button style={{marginTop: '5px'}} onClick={authorizeHandler(true)}>
authorize just this authorize just this
</button> </button>
<button style={{marginTop: '5px'}} onClick={authorizeHandler('no')}> {event?.kind !== undefined ? (
cancel <button
style={{marginTop: '5px'}}
onClick={authorizeHandler(
false,
{kinds: {[event.kind]: true}} // idem
)}
>
reject kind {event.kind} forever
</button>
) : (
<button
style={{marginTop: '5px'}}
onClick={authorizeHandler(
false,
{} // idem
)}
>
reject forever
</button>
)}
<button style={{marginTop: '5px'}} onClick={authorizeHandler(false)}>
reject
</button> </button>
</div> </div>
</> </>
) )
function authorizeHandler(condition) { function authorizeHandler(accept, conditions) {
return function (ev) { return function (ev) {
ev.preventDefault() ev.preventDefault()
browser.runtime.sendMessage({ browser.runtime.sendMessage({
prompt: true, prompt: true,
id, id,
host, host,
level, type,
condition accept,
conditions
}) })
} }
} }

View File

@@ -7,7 +7,7 @@
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"events": "^3.3.0", "events": "^3.3.0",
"nostr-tools": "^1.1.0", "nostr-tools": "^1.12.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View File

@@ -31,41 +31,43 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@noble/hashes@^0.5.7": "@noble/curves@1.0.0", "@noble/curves@~1.0.0":
version "0.5.9" version "1.0.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-0.5.9.tgz#9f3051a4cc6f7c168022b3b7fbbe9fe2a35cccf0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932"
integrity sha512-7lN1Qh6d8DUGmfN36XRsbN/WcGIPNtTGhkw26vWId/DlCIGsYJJootTtPGghTLcn/AaXPx2Q0b3cacrwXa7OVw== integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==
dependencies:
"@noble/hashes" "1.3.0"
"@noble/hashes@~1.1.1", "@noble/hashes@~1.1.3": "@noble/hashes@1.3.0":
version "1.1.5" version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ== integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
"@noble/secp256k1@^1.7.0", "@noble/secp256k1@~1.7.0": "@noble/hashes@~1.3.0":
version "1.7.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw== integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@scure/base@^1.1.1", "@scure/base@~1.1.0": "@scure/base@1.1.1", "@scure/base@~1.1.0":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@scure/bip32@^1.1.1": "@scure/bip32@1.3.0":
version "1.1.1" version "1.3.0"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.1.tgz#f62e4a2f13cc3e5e720ad81b7582b8631ae6835a" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87"
integrity sha512-UmI+liY7np2XakaW+6lMB6HZnpczWk1yXZTxvg8TM8MdOcKHCGL1YkraGj8eAjPfMwFNiAyek2hXmS/XFbab8g== integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==
dependencies: dependencies:
"@noble/hashes" "~1.1.3" "@noble/curves" "~1.0.0"
"@noble/secp256k1" "~1.7.0" "@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0" "@scure/base" "~1.1.0"
"@scure/bip39@^1.1.0": "@scure/bip39@1.2.0":
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b"
integrity sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w== integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==
dependencies: dependencies:
"@noble/hashes" "~1.1.1" "@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0" "@scure/base" "~1.1.0"
acorn-jsx@^5.3.1: acorn-jsx@^5.3.1:
@@ -932,16 +934,16 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
nostr-tools@^1.1.0: nostr-tools@^1.12.0:
version "1.1.0" version "1.12.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.1.0.tgz#f7c06a1d1a1a71b7b1feb7b0e687cef6a4e24286" resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.12.0.tgz#ec3618fc2298e029941b7db3bbe95187777c488f"
integrity sha512-T+Fj29ff6dn1YMMDrG03OctxrWVKeei/DZatVjgoad0tYUCiBBERk37qkpCqFAoKYVreIPl/Mxrh2DVfMzLA7g== integrity sha512-fsIXaNJPKaSrO9MxsCEWbhI4tG4pToQK4D4sgLRD0fRDfZ6ocCg8CLlh9lcNx0o8pVErCMLVASxbJ+w4WNK0MA==
dependencies: dependencies:
"@noble/hashes" "^0.5.7" "@noble/curves" "1.0.0"
"@noble/secp256k1" "^1.7.0" "@noble/hashes" "1.3.0"
"@scure/base" "^1.1.1" "@scure/base" "1.1.1"
"@scure/bip32" "^1.1.1" "@scure/bip32" "1.3.0"
"@scure/bip39" "^1.1.0" "@scure/bip39" "1.2.0"
nth-check@^2.0.1: nth-check@^2.0.1:
version "2.1.1" version "2.1.1"