nostr: link replacing.
this commit also includes (unrelated) naïvely hiding the private key and displaying on blur/focus.
This commit is contained in:
@@ -1,10 +1,17 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
import {validateEvent, signEvent, getEventHash, getPublicKey} from 'nostr-tools'
|
import {
|
||||||
|
validateEvent,
|
||||||
|
signEvent,
|
||||||
|
getEventHash,
|
||||||
|
getPublicKey,
|
||||||
|
nip19
|
||||||
|
} from 'nostr-tools'
|
||||||
import {encrypt, decrypt} from 'nostr-tools/nip04'
|
import {encrypt, decrypt} from 'nostr-tools/nip04'
|
||||||
import {Mutex} from 'async-mutex'
|
import {Mutex} from 'async-mutex'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PERMISSIONS_REQUIRED,
|
PERMISSIONS_REQUIRED,
|
||||||
|
NO_PERMISSIONS_REQUIRED,
|
||||||
readPermissionLevel,
|
readPermissionLevel,
|
||||||
updatePermission
|
updatePermission
|
||||||
} from './common'
|
} from './common'
|
||||||
@@ -41,19 +48,60 @@ browser.windows.onRemoved.addListener(windowId => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function handleContentScriptMessage({type, params, host}) {
|
async function handleContentScriptMessage({type, params, host}) {
|
||||||
let level = await readPermissionLevel(host)
|
if (NO_PERMISSIONS_REQUIRED[type]) {
|
||||||
|
// authorized, and we won't do anything with private key here, so do a separate handler
|
||||||
|
switch (type) {
|
||||||
|
case 'replaceURL': {
|
||||||
|
let {protocol_handler: ph} = await browser.storage.local.get([
|
||||||
|
'protocol_handler'
|
||||||
|
])
|
||||||
|
if (!ph) return false
|
||||||
|
|
||||||
if (level >= PERMISSIONS_REQUIRED[type]) {
|
let {url} = params
|
||||||
// authorized, proceed
|
let raw = url.split('nostr:')[1]
|
||||||
|
let {type, data} = nip19.decode(raw)
|
||||||
|
let replacements = {
|
||||||
|
raw,
|
||||||
|
hrp: type,
|
||||||
|
hex:
|
||||||
|
type === 'npub' || type === 'note'
|
||||||
|
? data
|
||||||
|
: type === 'nprofile'
|
||||||
|
? data.pubkey
|
||||||
|
: type === 'nevent'
|
||||||
|
? data.id
|
||||||
|
: null,
|
||||||
|
p_or_e: {npub: 'p', note: 'e', nprofile: 'p', nevent: 'e'}[type],
|
||||||
|
u_or_n: {npub: 'u', note: 'n', nprofile: 'u', nevent: 'n'}[type],
|
||||||
|
relay0: type === 'nprofile' ? data.relays[0] : null,
|
||||||
|
relay1: type === 'nprofile' ? data.relays[1] : null,
|
||||||
|
relay2: type === 'nprofile' ? data.relays[2] : null
|
||||||
|
}
|
||||||
|
let result = ph
|
||||||
|
Object.entries(replacements).forEach(([pattern, value]) => {
|
||||||
|
result = result.replace(new RegExp(`{ *${pattern} *}`, 'g'), value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
// ask for authorization
|
let level = await readPermissionLevel(host)
|
||||||
try {
|
|
||||||
await promptPermission(host, PERMISSIONS_REQUIRED[type], params)
|
if (level >= PERMISSIONS_REQUIRED[type]) {
|
||||||
// authorized, proceed
|
// authorized, proceed
|
||||||
} catch (_) {
|
} else {
|
||||||
// not authorized, stop here
|
// ask for authorization
|
||||||
return {
|
try {
|
||||||
error: `insufficient permissions, required ${PERMISSIONS_REQUIRED[type]}`
|
await promptPermission(host, PERMISSIONS_REQUIRED[type], params)
|
||||||
|
// authorized, proceed
|
||||||
|
} catch (_) {
|
||||||
|
// not authorized, stop here
|
||||||
|
return {
|
||||||
|
error: `insufficient permissions, required ${PERMISSIONS_REQUIRED[type]}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
|
|
||||||
|
export const NO_PERMISSIONS_REQUIRED = {
|
||||||
|
replaceURL: true
|
||||||
|
}
|
||||||
|
|
||||||
export const PERMISSIONS_REQUIRED = {
|
export const PERMISSIONS_REQUIRED = {
|
||||||
getPublicKey: 1,
|
getPublicKey: 1,
|
||||||
getRelays: 5,
|
getRelays: 5,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "nos2x",
|
"name": "nos2x",
|
||||||
"description": "Nostr Signer Extension",
|
"description": "Nostr Signer Extension",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"homepage_url": "https://github.com/fiatjaf/nos2x",
|
"homepage_url": "https://github.com/fiatjaf/nos2x",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"icons": {
|
"icons": {
|
||||||
@@ -18,17 +18,17 @@
|
|||||||
"default_title": "nos2x",
|
"default_title": "nos2x",
|
||||||
"default_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
"content_scripts": [{
|
"content_scripts": [
|
||||||
"matches": ["<all_urls>"],
|
{
|
||||||
"js": [
|
"matches": ["<all_urls>"],
|
||||||
"content-script.build.js"
|
"js": ["content-script.build.js"]
|
||||||
]
|
}
|
||||||
}],
|
|
||||||
"permissions": [
|
|
||||||
"storage"
|
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [{
|
"permissions": ["storage"],
|
||||||
"resources": ["nostr-provider.js"],
|
"web_accessible_resources": [
|
||||||
"matches": ["https://*/*", "http://localhost:*/*"]
|
{
|
||||||
}]
|
"resources": ["nostr-provider.js"],
|
||||||
|
"matches": ["https://*/*", "http://localhost:*/*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ window.nostr = {
|
|||||||
window.addEventListener('message', message => {
|
window.addEventListener('message', message => {
|
||||||
if (
|
if (
|
||||||
!message.data ||
|
!message.data ||
|
||||||
!message.data.response ||
|
message.data.response === null ||
|
||||||
|
message.data.response === undefined ||
|
||||||
message.data.ext !== 'nos2x' ||
|
message.data.ext !== 'nos2x' ||
|
||||||
!window.nostr._requests[message.data.id]
|
!window.nostr._requests[message.data.id]
|
||||||
)
|
)
|
||||||
@@ -60,5 +61,23 @@ window.addEventListener('message', message => {
|
|||||||
window.nostr._requests[message.data.id].resolve(message.data.response)
|
window.nostr._requests[message.data.id].resolve(message.data.response)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete window.nostr._requests[message.data.id];
|
delete window.nostr._requests[message.data.id]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// hack to replace nostr:nprofile.../etc links with something else
|
||||||
|
let replacing = null
|
||||||
|
let links = document.querySelectorAll('a[href^="nostr:"]')
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
links[i].addEventListener('mouseenter', replaceNostrSchemeLink)
|
||||||
|
}
|
||||||
|
async function replaceNostrSchemeLink(e) {
|
||||||
|
if (replacing === false) return
|
||||||
|
|
||||||
|
let response = await window.nostr._call('replaceURL', {url: e.target.href})
|
||||||
|
if (response === false) {
|
||||||
|
replacing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.target.href = response
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ function Options() {
|
|||||||
let [relays, setRelays] = useState([])
|
let [relays, setRelays] = useState([])
|
||||||
let [newRelayURL, setNewRelayURL] = useState('')
|
let [newRelayURL, setNewRelayURL] = useState('')
|
||||||
let [permissions, setPermissions] = useState()
|
let [permissions, setPermissions] = useState()
|
||||||
|
let [protocolHandler, setProtocolHandler] = useState(null)
|
||||||
|
let [hidingPrivateKey, hidePrivateKey] = useState(true)
|
||||||
let [message, setMessage] = useState('')
|
let [message, setMessage] = useState('')
|
||||||
|
|
||||||
const showMessage = useCallback(msg => {
|
const showMessage = useCallback(msg => {
|
||||||
@@ -22,25 +24,45 @@ function Options() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
browser.storage.local.get(['private_key', 'relays']).then(results => {
|
browser.storage.local
|
||||||
if (results.private_key) setKey(nip19.nsecEncode(results.private_key))
|
.get(['private_key', 'relays', 'protocol_handler'])
|
||||||
if (results.relays) {
|
.then(results => {
|
||||||
let relaysList = []
|
if (results.private_key) setKey(nip19.nsecEncode(results.private_key))
|
||||||
for (let url in results.relays) {
|
if (results.relays) {
|
||||||
relaysList.push({
|
let relaysList = []
|
||||||
url,
|
for (let url in results.relays) {
|
||||||
policy: results.relays[url]
|
relaysList.push({
|
||||||
})
|
url,
|
||||||
|
policy: results.relays[url]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setRelays(relaysList)
|
||||||
}
|
}
|
||||||
setRelays(relaysList)
|
if (results.protocol_handler) {
|
||||||
}
|
setProtocolHandler(results.protocol_handler)
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPermissions()
|
loadPermissions()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
function loadPermissions() {
|
||||||
|
readPermissions().then(permissions => {
|
||||||
|
setPermissions(
|
||||||
|
Object.entries(permissions).map(
|
||||||
|
([host, {level, condition, created_at}]) => ({
|
||||||
|
host,
|
||||||
|
level,
|
||||||
|
condition,
|
||||||
|
created_at
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>nos2x</h1>
|
<h1>nos2x</h1>
|
||||||
@@ -89,15 +111,18 @@ function Options() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{marginBottom: '10px'}}>
|
||||||
<label>
|
<label>
|
||||||
<div>private key: </div>
|
<div>private key: </div>
|
||||||
<div style={{marginLeft: '10px'}}>
|
<div style={{marginLeft: '10px'}}>
|
||||||
<div style={{display: 'flex'}}>
|
<div style={{display: 'flex'}}>
|
||||||
<input
|
<input
|
||||||
style={{width: '500px'}}
|
type={hidingPrivateKey ? 'password' : 'text'}
|
||||||
|
style={{width: '600px'}}
|
||||||
value={key}
|
value={key}
|
||||||
onChange={handleKeyChange}
|
onChange={handleKeyChange}
|
||||||
|
onFocus={() => hidePrivateKey(false)}
|
||||||
|
onBlur={() => hidePrivateKey(true)}
|
||||||
/>
|
/>
|
||||||
{key === '' && <button onClick={generate}>generate</button>}
|
{key === '' && <button onClick={generate}>generate</button>}
|
||||||
</div>
|
</div>
|
||||||
@@ -144,6 +169,70 @@ function Options() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
|
<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
|
||||||
|
{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
|
||||||
|
{raw} = anything after the colon, i.e. the full nip19 bech32 string
|
||||||
|
{hrp} = human-readable prefix of the nip19 string
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- https://nostr.guru/{p_or_e}/{hex}
|
||||||
|
- https://brb.io/{u_or_n}/{hex}
|
||||||
|
- https://notes.blockcore.net/{p_or_e}/{hex}
|
||||||
|
`}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
style={{marginTop: '10px'}}
|
||||||
|
onClick={saveNostrProtocolHandlerSettings}
|
||||||
|
>
|
||||||
|
save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{marginTop: '12px', fontSize: '120%'}}>{message}</div>
|
<div style={{marginTop: '12px', fontSize: '120%'}}>{message}</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -216,7 +305,6 @@ function Options() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRevoke(e) {
|
async function handleRevoke(e) {
|
||||||
e.preventDefault()
|
|
||||||
let host = e.target.dataset.domain
|
let host = e.target.dataset.domain
|
||||||
if (window.confirm(`revoke all permissions from ${host}?`)) {
|
if (window.confirm(`revoke all permissions from ${host}?`)) {
|
||||||
await removePermissions(host)
|
await removePermissions(host)
|
||||||
@@ -225,21 +313,6 @@ function Options() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPermissions() {
|
|
||||||
readPermissions().then(permissions => {
|
|
||||||
setPermissions(
|
|
||||||
Object.entries(permissions).map(
|
|
||||||
([host, {level, condition, created_at}]) => ({
|
|
||||||
host,
|
|
||||||
level,
|
|
||||||
condition,
|
|
||||||
created_at
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveRelays() {
|
async function saveRelays() {
|
||||||
await browser.storage.local.set({
|
await browser.storage.local.set({
|
||||||
relays: Object.fromEntries(
|
relays: Object.fromEntries(
|
||||||
@@ -250,6 +323,24 @@ function Options() {
|
|||||||
})
|
})
|
||||||
showMessage('saved relays!')
|
showMessage('saved relays!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleChangeProtocolHandler(e) {
|
||||||
|
if (e.target.type === 'text') setProtocolHandler(e.target.value)
|
||||||
|
else
|
||||||
|
switch (e.target.value) {
|
||||||
|
case 'no':
|
||||||
|
setProtocolHandler(null)
|
||||||
|
break
|
||||||
|
case 'yes':
|
||||||
|
setProtocolHandler('')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNostrProtocolHandlerSettings() {
|
||||||
|
await browser.storage.local.set({protocol_handler: protocolHandler})
|
||||||
|
showMessage('saved protocol handler!')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<Options />, document.getElementById('main'))
|
render(<Options />, document.getElementById('main'))
|
||||||
|
|||||||
Reference in New Issue
Block a user