diff --git a/extension/background.js b/extension/background.js index 9750a18..f23e9ba 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,10 +1,17 @@ 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 {Mutex} from 'async-mutex' import { PERMISSIONS_REQUIRED, + NO_PERMISSIONS_REQUIRED, readPermissionLevel, updatePermission } from './common' @@ -41,19 +48,60 @@ browser.windows.onRemoved.addListener(windowId => { }) 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]) { - // authorized, proceed + let {url} = params + 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 { - // ask for authorization - try { - await promptPermission(host, PERMISSIONS_REQUIRED[type], params) + let level = await readPermissionLevel(host) + + if (level >= PERMISSIONS_REQUIRED[type]) { // authorized, proceed - } catch (_) { - // not authorized, stop here - return { - error: `insufficient permissions, required ${PERMISSIONS_REQUIRED[type]}` + } else { + // ask for authorization + try { + await promptPermission(host, PERMISSIONS_REQUIRED[type], params) + // authorized, proceed + } catch (_) { + // not authorized, stop here + return { + error: `insufficient permissions, required ${PERMISSIONS_REQUIRED[type]}` + } } } } diff --git a/extension/common.js b/extension/common.js index 6452f7c..d1274e9 100644 --- a/extension/common.js +++ b/extension/common.js @@ -1,5 +1,9 @@ import browser from 'webextension-polyfill' +export const NO_PERMISSIONS_REQUIRED = { + replaceURL: true +} + export const PERMISSIONS_REQUIRED = { getPublicKey: 1, getRelays: 5, diff --git a/extension/manifest.json b/extension/manifest.json index 4a8fe99..23f422f 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "name": "nos2x", "description": "Nostr Signer Extension", - "version": "1.7.0", + "version": "1.8.0", "homepage_url": "https://github.com/fiatjaf/nos2x", "manifest_version": 3, "icons": { @@ -18,17 +18,17 @@ "default_title": "nos2x", "default_popup": "popup.html" }, - "content_scripts": [{ - "matches": [""], - "js": [ - "content-script.build.js" - ] - }], - "permissions": [ - "storage" + "content_scripts": [ + { + "matches": [""], + "js": ["content-script.build.js"] + } ], - "web_accessible_resources": [{ - "resources": ["nostr-provider.js"], - "matches": ["https://*/*", "http://localhost:*/*"] - }] + "permissions": ["storage"], + "web_accessible_resources": [ + { + "resources": ["nostr-provider.js"], + "matches": ["https://*/*", "http://localhost:*/*"] + } + ] } diff --git a/extension/nostr-provider.js b/extension/nostr-provider.js index 31bcc4a..bb6e1ee 100644 --- a/extension/nostr-provider.js +++ b/extension/nostr-provider.js @@ -46,7 +46,8 @@ window.nostr = { window.addEventListener('message', message => { if ( !message.data || - !message.data.response || + message.data.response === null || + message.data.response === undefined || message.data.ext !== 'nos2x' || !window.nostr._requests[message.data.id] ) @@ -60,5 +61,23 @@ window.addEventListener('message', message => { 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 +} diff --git a/extension/options.jsx b/extension/options.jsx index f571589..1907891 100644 --- a/extension/options.jsx +++ b/extension/options.jsx @@ -14,6 +14,8 @@ function Options() { let [relays, setRelays] = useState([]) let [newRelayURL, setNewRelayURL] = useState('') let [permissions, setPermissions] = useState() + let [protocolHandler, setProtocolHandler] = useState(null) + let [hidingPrivateKey, hidePrivateKey] = useState(true) let [message, setMessage] = useState('') const showMessage = useCallback(msg => { @@ -22,25 +24,45 @@ function Options() { }) useEffect(() => { - browser.storage.local.get(['private_key', 'relays']).then(results => { - if (results.private_key) setKey(nip19.nsecEncode(results.private_key)) - if (results.relays) { - let relaysList = [] - for (let url in results.relays) { - relaysList.push({ - url, - policy: results.relays[url] - }) + browser.storage.local + .get(['private_key', 'relays', 'protocol_handler']) + .then(results => { + if (results.private_key) setKey(nip19.nsecEncode(results.private_key)) + if (results.relays) { + let relaysList = [] + for (let url in results.relays) { + relaysList.push({ + url, + policy: results.relays[url] + }) + } + setRelays(relaysList) } - setRelays(relaysList) - } - }) + if (results.protocol_handler) { + setProtocolHandler(results.protocol_handler) + } + }) }, []) useEffect(() => { loadPermissions() }, []) + function loadPermissions() { + readPermissions().then(permissions => { + setPermissions( + Object.entries(permissions).map( + ([host, {level, condition, created_at}]) => ({ + host, + level, + condition, + created_at + }) + ) + ) + }) + } + return ( <>

nos2x

@@ -89,15 +111,18 @@ function Options() { -
+