import {bytesToHex, hexToBytes} from '@noble/hashes/utils'
import {getPublicKey} from 'nostr-tools'
import * as nip19 from 'nostr-tools/nip19'
import {decrypt, encrypt} from 'nostr-tools/nip49'
import {generateSecretKey} from 'nostr-tools/pure'
import React, {useEffect, useState} from 'react'
import {createRoot} from 'react-dom/client'
import QRCode from 'react-qr-code'
import browser from 'webextension-polyfill'
import {removePermissions} from './common'
function Options() {
let [unsavedChanges, setUnsavedChanges] = useState([])
let [privKey, setPrivKey] = useState(null)
let [privKeyInput, setPrivKeyInput] = useState('')
let [askPassword, setAskPassword] = useState(null)
let [password, setPassword] = useState('')
let [policies, setPermissions] = useState([])
let [protocolHandler, setProtocolHandler] = useState('https://njump.me/{raw}')
let [hidingPrivateKey, hidePrivateKey] = useState(true)
let [showNotifications, setNotifications] = useState(false)
let [messages, setMessages] = useState([])
let [handleNostrLinks, setHandleNostrLinks] = useState(false)
let [showProtocolHandlerHelp, setShowProtocolHandlerHelp] = useState(false)
let [selectedItems, setSelectedItems] = useState([])
const showMessage = msg => {
setMessages(oldMessages => [...oldMessages, msg])
}
useEffect(() => {
if (messages.length === 0) {
return
}
const timeout = setTimeout(() => setMessages([]), 3000)
return () => clearTimeout(timeout)
}, [messages, setMessages])
useEffect(() => {
browser.storage.local
.get(['private_key', 'protocol_handler', 'notifications'])
.then(results => {
if (results.private_key) {
let prvKey = results.private_key
let nsec = nip19.nsecEncode(hexToBytes(prvKey))
setPrivKeyInput(nsec)
setPrivKey(nsec)
}
if (results.protocol_handler) {
setProtocolHandler(results.protocol_handler)
setHandleNostrLinks(true)
setShowProtocolHandlerHelp(false)
}
if (results.notifications) {
setNotifications(true)
}
})
}, [])
useEffect(() => {
loadPermissions()
}, [])
async function loadPermissions() {
let {policies = {}} = await browser.storage.local.get('policies')
let list = []
Object.entries(policies).forEach(([host, accepts]) => {
Object.entries(accepts).forEach(([accept, types]) => {
Object.entries(types).forEach(([type, {conditions, created_at}]) => {
list.push({
host,
type,
accept,
conditions,
created_at
})
})
})
})
setPermissions(list)
}
return (
<>
nos2x
nostr signer extension
options
private key:
{privKeyInput === '' && (
generate
)}
{privKeyInput && hidingPrivateKey && (
<>
{askPassword !== 'encrypt/display' && (
hidePrivateKey(false)}>
show key
)}
setAskPassword('encrypt/display')}>
show key encrypted
>
)}
{privKeyInput && !hidingPrivateKey && (
hide key
)}
{privKeyInput &&
!privKeyInput.startsWith('ncryptsec1') &&
!isKeyValid() && (
private key is invalid!
)}
{!hidingPrivateKey &&
privKeyInput !== '' &&
(privKeyInput.startsWith('ncryptsec1') || isKeyValid()) && (
)}
{askPassword && (
)}
nosta.me:
{
let {data} = nip19.decode(privKey)
let pub = getPublicKey(data)
let npub = nip19.npubEncode(pub)
window.open('https://nosta.me/' + npub)
}}
style={{cursor: 'pointer'}}
>
browse your profile
window.open('https://nosta.me/login/options')}
style={{cursor: 'pointer'}}
>
edit your profile
handle{' '}
nostr: {' '}
links:
{handleNostrLinks && (
{!showProtocolHandlerHelp && (
?
)}
{showProtocolHandlerHelp && (
{`
{raw} = anything after the colon, i.e. the full nip19 bech32 string
{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
{u_or_n} = "u" for npub or nprofile, "n" for note or nevent
{relay0} = first relay in a nprofile or nevent
{relay1} = second relay in a nprofile or nevent
{relay2} = third relay in a nprofile or nevent
{hrp} = human-readable prefix of the nip19 string
examples:
- https://njump.me/{raw}
- https://snort.social/{raw}
- https://nostr.band/{raw}
`}
)}
)}
show notifications when permissions are used:
save
{messages.map((message, i) => (
{message}
))}
permissions
{!!policies.length && (
{selectedItems.length > 0 ? (
revoke
) : null}
)}
{!policies.length && (
no permissions have been granted yet
)}
>
)
async function hideAndResetKeyInput() {
setPrivKeyInput(privKey)
hidePrivateKey(true)
}
async function handleKeyChange(e) {
let key = e.target.value.toLowerCase().trim()
setPrivKeyInput(key)
try {
let bytes = hexToBytes(key)
if (bytes.length === 32) {
key = nip19.nsecEncode(bytes)
setPrivKeyInput(key)
}
} catch (err) {
/***/
}
if (key.startsWith('ncryptsec1')) {
// we won't save an encrypted key, will wait for the password
setAskPassword('decrypt/save')
return
}
try {
// we will only save a key that is a valid nsec
if (nip19.decode(key).type === 'nsec') {
addUnsavedChanges('private_key')
}
} catch (err) {
/***/
}
}
async function generate() {
setPrivKeyInput(nip19.nsecEncode(generateSecretKey()))
addUnsavedChanges('private_key')
}
function encryptPrivateKeyAndDisplay(ev) {
ev.preventDefault()
try {
let {data} = nip19.decode(privKeyInput)
let encrypted = encrypt(data, password, 16, 0x00)
setPrivKeyInput(encrypted)
hidePrivateKey(false)
setAskPassword(null)
} catch (e) {
showMessage(e.message)
}
}
function decryptPrivateKeyAndSave(ev) {
ev.preventDefault()
try {
let decrypted = decrypt(privKeyInput, password)
setPrivKeyInput(nip19.nsecEncode(decrypted))
browser.storage.local.set({
private_key: bytesToHex(decrypted)
})
setTimeout(() => {
setAskPassword(null)
}, 2000)
} catch (e) {
showMessage(e.message)
}
}
async function saveKey() {
if (!isKeyValid()) {
showMessage('PRIVATE KEY IS INVALID! did not save private key.')
return
}
let hexOrEmptyKey = privKeyInput
try {
let {type, data} = nip19.decode(privKeyInput)
if (type === 'nsec') hexOrEmptyKey = bytesToHex(data)
} catch (_) {}
await browser.storage.local.set({
private_key: hexOrEmptyKey
})
if (hexOrEmptyKey !== '') {
setPrivKeyInput(nip19.nsecEncode(hexToBytes(hexOrEmptyKey)))
}
showMessage('saved private key!')
}
function isKeyValid() {
if (privKeyInput === '') return true
try {
if (nip19.decode(privKeyInput).type === 'nsec') return true
} catch (_) {}
return false
}
async function handleSelect(index) {
if (selectedItems.includes(index)) {
setSelectedItems(selectedItems.filter(i => i !== index))
} else {
setSelectedItems([...selectedItems, index])
}
}
function handleNotifications() {
setNotifications(!showNotifications)
addUnsavedChanges('notifications')
if (!showNotifications) requestBrowserNotificationPermissions()
}
async function handleMultiRevoke() {
for (let index of selectedItems) {
let {host, accept, type} = policies[index]
await removePermissions(host, accept, type)
}
showMessage('removed selected policies')
loadPermissions()
setSelectedItems([])
}
async function requestBrowserNotificationPermissions() {
let granted = await browser.permissions.request({
permissions: ['notifications']
})
if (!granted) setNotifications(false)
}
async function saveNotifications() {
await browser.storage.local.set({notifications: showNotifications})
showMessage('saved notifications!')
}
function changeShowProtocolHandlerHelp() {
setShowProtocolHandlerHelp(true)
}
function changeHandleNostrLinks() {
if (handleNostrLinks) {
setProtocolHandler('')
addUnsavedChanges('protocol_handler')
} else setShowProtocolHandlerHelp(true)
setHandleNostrLinks(!handleNostrLinks)
}
function handleChangeProtocolHandler(e) {
setProtocolHandler(e.target.value)
addUnsavedChanges('protocol_handler')
}
async function saveNostrProtocolHandlerSettings() {
await browser.storage.local.set({protocol_handler: protocolHandler})
showMessage('saved protocol handler!')
}
function addUnsavedChanges(section) {
setUnsavedChanges(currentUnsavedChanges =>
currentUnsavedChanges.includes(section)
? currentUnsavedChanges
: [...currentUnsavedChanges, section]
)
}
async function saveChanges() {
for (let section of unsavedChanges) {
switch (section) {
case 'private_key':
await saveKey()
break
case 'protocol_handler':
await saveNostrProtocolHandlerSettings()
break
case 'notifications':
await saveNotifications()
break
}
}
setUnsavedChanges([])
}
}
createRoot(document.getElementById('main')).render( )