rework permissions and popup prompts, make each permission fine grained.
This commit is contained in:
@@ -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
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: </div>
|
<label>
|
||||||
<div style={{marginLeft: '10px'}}>
|
<div>private key: </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])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
68
yarn.lock
68
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user