diff --git a/Nostore.xcodeproj/project.pbxproj b/Nostore.xcodeproj/project.pbxproj index 50fb97a..f069b5d 100644 --- a/Nostore.xcodeproj/project.pbxproj +++ b/Nostore.xcodeproj/project.pbxproj @@ -71,6 +71,12 @@ 941B04342978CDF900CA291E /* Icon-16.png in Resources */ = {isa = PBXBuildFile; fileRef = 941B042F2978CDF900CA291E /* Icon-16.png */; }; 941B04352978CDF900CA291E /* Icon-64.png in Resources */ = {isa = PBXBuildFile; fileRef = 941B04302978CDF900CA291E /* Icon-64.png */; }; 941B04362978CDF900CA291E /* Icon-64.png in Resources */ = {isa = PBXBuildFile; fileRef = 941B04302978CDF900CA291E /* Icon-64.png */; }; + 944A6DD32988BA200032C2E3 /* permission.html in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DD22988BA200032C2E3 /* permission.html */; }; + 944A6DD42988BA200032C2E3 /* permission.html in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DD22988BA200032C2E3 /* permission.html */; }; + 944A6DD62988BD230032C2E3 /* permission.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DD52988BD230032C2E3 /* permission.js */; }; + 944A6DD72988BD230032C2E3 /* permission.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DD52988BD230032C2E3 /* permission.js */; }; + 944A6DD92988D7900032C2E3 /* permission.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DD82988D7900032C2E3 /* permission.build.js */; }; + 944A6DDA2988D7900032C2E3 /* permission.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DD82988D7900032C2E3 /* permission.build.js */; }; 948C69D9297F887600FB3574 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69D8297F887600FB3574 /* options.html */; }; 948C69DA297F887600FB3574 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69D8297F887600FB3574 /* options.html */; }; 948C69DD297F88A200FB3574 /* options.css in Resources */ = {isa = PBXBuildFile; fileRef = 948C69DB297F88A200FB3574 /* options.css */; }; @@ -171,6 +177,9 @@ 941B042E2978CDF900CA291E /* Icon-32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-32.png"; sourceTree = ""; }; 941B042F2978CDF900CA291E /* Icon-16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-16.png"; sourceTree = ""; }; 941B04302978CDF900CA291E /* Icon-64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-64.png"; sourceTree = ""; }; + 944A6DD22988BA200032C2E3 /* permission.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = permission.html; sourceTree = ""; }; + 944A6DD52988BD230032C2E3 /* permission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = permission.js; sourceTree = ""; }; + 944A6DD82988D7900032C2E3 /* permission.build.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = permission.build.js; sourceTree = ""; }; 948C69D8297F887600FB3574 /* options.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = options.html; sourceTree = ""; }; 948C69DB297F88A200FB3574 /* options.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = options.css; sourceTree = ""; }; 948C69DC297F88A200FB3574 /* options.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = options.js; sourceTree = ""; }; @@ -258,6 +267,9 @@ 941B03A2296FA90400CA291E /* Resources */ = { isa = PBXGroup; children = ( + 944A6DD82988D7900032C2E3 /* permission.build.js */, + 944A6DD52988BD230032C2E3 /* permission.js */, + 944A6DD22988BA200032C2E3 /* permission.html */, 948C69E4297F8BA600FB3574 /* options.build.css */, 948C69E1297F891F00FB3574 /* options.build.js */, 948C69DB297F88A200FB3574 /* options.css */, @@ -501,7 +513,9 @@ 941B0413297110F100CA291E /* background.build.js in Resources */, 948C69E82982DFE900FB3574 /* background.html in Resources */, 948C69DF297F88A200FB3574 /* options.js in Resources */, + 944A6DD32988BA200032C2E3 /* permission.html in Resources */, 948C69DD297F88A200FB3574 /* options.css in Resources */, + 944A6DD92988D7900032C2E3 /* permission.build.js in Resources */, 941B03F2296FA90400CA291E /* background.js in Resources */, 948C69E2297F891F00FB3574 /* options.build.js in Resources */, 941B03F8296FA90400CA291E /* popup.css in Resources */, @@ -512,6 +526,7 @@ 941B040D296FAD6900CA291E /* nostr.js in Resources */, 941B03EE296FA90400CA291E /* images in Resources */, 941B03F0296FA90400CA291E /* manifest.json in Resources */, + 944A6DD62988BD230032C2E3 /* permission.js in Resources */, 941B04312978CDF900CA291E /* Icon-32.png in Resources */, 941B041A2971139000CA291E /* content.build.js in Resources */, 941B041C2971139000CA291E /* popup.build.js in Resources */, @@ -535,7 +550,9 @@ 941B0414297110F100CA291E /* background.build.js in Resources */, 948C69E92982DFE900FB3574 /* background.html in Resources */, 948C69E0297F88A200FB3574 /* options.js in Resources */, + 944A6DD42988BA200032C2E3 /* permission.html in Resources */, 948C69DE297F88A200FB3574 /* options.css in Resources */, + 944A6DDA2988D7900032C2E3 /* permission.build.js in Resources */, 941B03F3296FA90400CA291E /* background.js in Resources */, 948C69E3297F891F00FB3574 /* options.build.js in Resources */, 941B03F9296FA90400CA291E /* popup.css in Resources */, @@ -546,6 +563,7 @@ 941B040E296FAD6900CA291E /* nostr.js in Resources */, 941B03EF296FA90400CA291E /* images in Resources */, 941B03F1296FA90400CA291E /* manifest.json in Resources */, + 944A6DD72988BD230032C2E3 /* permission.js in Resources */, 941B04322978CDF900CA291E /* Icon-32.png in Resources */, 941B041B2971139000CA291E /* content.build.js in Resources */, 941B041D2971139000CA291E /* popup.build.js in Resources */, @@ -764,7 +782,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (Extension)/Info.plist"; @@ -795,7 +813,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (Extension)/Info.plist"; @@ -830,7 +848,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (App)/Info.plist"; @@ -872,7 +890,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS (App)/Info.plist"; @@ -913,7 +931,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/nostore.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -945,7 +963,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/nostore.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -980,7 +998,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "macOS (App)/nostore.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1016,7 +1034,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "macOS (App)/nostore.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 5SD834TD9W; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/Nostore.xcodeproj/xcuserdata/ryan.xcuserdatad/xcschemes/xcschememanagement.plist b/Nostore.xcodeproj/xcuserdata/ryan.xcuserdatad/xcschemes/xcschememanagement.plist index 2fa6e1a..1e154e6 100644 --- a/Nostore.xcodeproj/xcuserdata/ryan.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Nostore.xcodeproj/xcuserdata/ryan.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ Nostore (iOS).xcscheme_^#shared#^_ orderHint - 1 + 0 Nostore (macOS).xcscheme_^#shared#^_ orderHint - 0 + 1 nostore (iOS).xcscheme_^#shared#^_ diff --git a/Shared (Extension)/Resources/background.js b/Shared (Extension)/Resources/background.js index a55816a..68e37cf 100644 --- a/Shared (Extension)/Resources/background.js +++ b/Shared (Extension)/Resources/background.js @@ -5,81 +5,197 @@ import { nip04, nip19, } from 'nostr-tools'; - -import { getProfileIndex, get, getProfile } from './utils'; +import { Mutex } from 'async-mutex'; +import { + getProfileIndex, + get, + getProfile, + getPermission, + setPermission, +} from './utils'; const storage = browser.storage.local; const log = msg => console.log('Background: ', msg); +const validations = {}; +let prompt = { mutex: new Mutex(), release: null, tabId: null }; browser.runtime.onInstalled.addListener(async ({ reason }) => { // I would like to be able to skip this for development purposes - let ignoreHook = (await storage.get({ ignoreInstallHook: false })) - .ignoreInstallHook; - if (ignoreHook === true) { - return; - } - if (['install'].includes(reason)) { - browser.tabs.create({ - url: 'https://ursus.camp/nostore', - }); + // let ignoreHook = (await storage.get({ ignoreInstallHook: false })) + // .ignoreInstallHook; + // if (ignoreHook === true) { + // return; + // } + // if (['install'].includes(reason)) { + // browser.tabs.create({ + // url: 'https://ursus.camp/nostore', + // }); + // } +}); + +browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { + log(message); + let uuid = crypto.randomUUID(); + let sr; + + switch (message.kind) { + // General + case 'closePrompt': + prompt.release?.(); + return Promise.resolve(true); + case 'allowed': + complete(message); + return Promise.resolve(true); + case 'denied': + deny(message); + return Promise.resolve(true); + case 'generatePrivateKey': + return Promise.resolve(generatePrivateKey()); + case 'savePrivateKey': + return savePrivateKey(message.payload); + case 'getNpub': + return getNpub(message.payload); + case 'getNsec': + return getNsec(message.payload); + + // window.nostr + case 'getPubKey': + case 'signEvent': + case 'nip04.encrypt': + case 'nip04.decrypt': + case 'getRelays': + console.log('asking'); + validations[uuid] = sendResponse; + ask(uuid, message); + setTimeout(() => { + console.log('timeout release'); + prompt.release?.(); + }, 10_000); + return true; + default: + return Promise.resolve(); } }); -browser.runtime.onMessage.addListener( - async (message, _sender, sendResponse) => { - log(message); +async function forceRelease() { + if (prompt.tabId !== null) { + try { + // If the previous prompt is still open, then this won't do anything. + // If it's not open, it will throw an error and get caught. + await browser.tabs.get(prompt.tabId); + } catch (error) { + // If the tab is closed, but somehow escaped our event handling, we can clean it up here + // before attempting to open the next tab. + prompt.release?.(); + prompt.tabId = null; + } + } +} - switch (message.kind) { - // General - case 'log': - console.log( - message.payload.module ? `${module}: ` : '', - message.payload.msg - ); - break; - case 'generatePrivateKey': - sendResponse(generatePrivateKey()); - break; - case 'savePrivateKey': - await savePrivateKey(message.payload); - break; - case 'getNpub': - let npub = await getNpub(message.payload); - sendResponse(npub); - break; - case 'getNsec': - let nsec = await getNsec(message.payload); - sendResponse(nsec); - break; +async function ask(uuid, { kind, host, payload }) { + await forceRelease(); // Clean up previous tab if it closed without cleaning itself up + prompt.release = await prompt.mutex.acquire(); + + let mKind = kind === 'signEvent' ? `signEvent:${payload.kind}` : kind; + let permission = await getPermission(host, mKind); + console.log('existing permission: ', permission); + if (permission === 'allow') { + console.log('already allowed'); + complete({ + payload: uuid, + origKind: kind, + event: payload, + remember: false, + host, + }); + prompt.release(); + return; + } + + if (permission === 'deny') { + console.log('already denied'); + deny({ payload: uuid, origKind: kind, host }); + prompt.release(); + return; + } + + console.log('creating asking popup'); + let qs = new URLSearchParams({ + uuid, + kind, + host, + payload: JSON.stringify(payload || false), + }); + let tab = await browser.tabs.getCurrent(); + let p = await browser.tabs.create({ + url: `/permission.html?${qs.toString()}`, + openerTabId: tab.id, + }); + prompt.tabId = p.id; + return true; +} + +function complete({ payload, origKind, event, remember, host }) { + console.log('complete'); + sendResponse = validations[payload]; + + if (remember) { + console.log('saving permission'); + let mKind = + origKind === 'signEvent' ? `signEvent:${event.kind}` : origKind; + setPermission(host, mKind, 'allow'); + } + + if (sendResponse) { + console.log('sendResponse found'); + switch (origKind) { case 'getPubKey': - let pubKey = await getPubKey(); - sendResponse(pubKey); + getPubKey().then(pk => { + console.log(pk); + sendResponse(pk); + }); break; - - // window.nostr case 'signEvent': - let event = await signEvent_(message.payload); - sendResponse(event); + signEvent_(event).then(e => sendResponse(e)); break; case 'nip04.encrypt': - let cipherText = await nip04Encrypt(message.payload); - sendResponse(cipherText); + nip04Encrypt(event).then(e => sendResponse(e)); break; case 'nip04.decrypt': - let plainText = await nip04Decrypt(message.payload); - sendResponse(plainText); + nip04Decrypt(event).then(e => sendResponse(e)); break; case 'getRelays': - let relays = await getRelays(); - sendResponse(relays); - break; - - default: + getRelays().then(e => sendResponse(e)); break; } - return false; } -); +} + +function deny({ origKind, host, payload, remember, event }) { + console.log('denied'); + sendResponse = validations[payload]; + + if (remember) { + console.log('saving permission'); + let mKind = + origKind === 'signEvent' ? `signEvent:${event.kind}` : origKind; + setPermission(host, mKind, 'deny'); + } + + sendResponse?.(undefined); + return false; +} + +function keyDeleter(key) { + return new Promise(resolver => { + setTimeout(() => { + console.log('Validations: ', validations); + console.log('Deleting key validations: ', key); + resolver(); + delete validations[key]; + }, 1000); + }); +} // Options async function savePrivateKey([index, privKey]) { @@ -89,6 +205,7 @@ async function savePrivateKey([index, privKey]) { let profiles = await get('profiles'); profiles[index].privKey = privKey; await storage.set({ profiles }); + return true; } async function getNsec(index) { diff --git a/Shared (Extension)/Resources/content.js b/Shared (Extension)/Resources/content.js index 7dfd747..b81a3d5 100644 --- a/Shared (Extension)/Resources/content.js +++ b/Shared (Extension)/Resources/content.js @@ -13,7 +13,12 @@ window.addEventListener('message', async message => { let { kind, reqId, payload } = message.data; if (!validEvents.includes(kind)) return; - payload = await browser.runtime.sendMessage({ kind, payload }); + payload = await browser.runtime.sendMessage({ + kind, + payload, + host: window.location.host, + }); + console.log(payload); kind = `return_${kind}`; diff --git a/Shared (Extension)/Resources/nostr.js b/Shared (Extension)/Resources/nostr.js index 83371fd..92dd87a 100644 --- a/Shared (Extension)/Resources/nostr.js +++ b/Shared (Extension)/Resources/nostr.js @@ -56,6 +56,6 @@ window.addEventListener('message', message => { if (!validEvents.includes(kind)) return; - window.nostr.requests[reqId](payload); + window.nostr.requests[reqId]?.(payload); delete window.nostr.requests[reqId]; }); diff --git a/Shared (Extension)/Resources/options.css b/Shared (Extension)/Resources/options.css index bdc4b3d..66a7c3b 100644 --- a/Shared (Extension)/Resources/options.css +++ b/Shared (Extension)/Resources/options.css @@ -34,4 +34,12 @@ .section-header { @apply text-2xl lg:text-5xl font-bold; } + + .subsection-header { + @apply text-xl lg:text-4xl font-bold; + } + + a { + @apply border-2 border-dotted text-fuchsia-800 border-fuchsia-800 hover:border-transparent; + } } \ No newline at end of file diff --git a/Shared (Extension)/Resources/options.html b/Shared (Extension)/Resources/options.html index cd57dd4..b520413 100644 --- a/Shared (Extension)/Resources/options.html +++ b/Shared (Extension)/Resources/options.html @@ -22,9 +22,8 @@
- - - + +
@@ -53,7 +52,7 @@
- +
@@ -81,7 +80,7 @@ - + @@ -111,10 +110,52 @@
+ + +
+

App Permissions

+

+ Permissions granted to individual applications. + Everything defaults to Ask unless you have saved a different option. +

+ +
+ +
+ +
+ +

You have not remembered any app requests yet.

+ + + + + + + +
App RequestAction
+
+
- - - + +
diff --git a/Shared (Extension)/Resources/options.js b/Shared (Extension)/Resources/options.js index 7e7f5f7..8e34260 100644 --- a/Shared (Extension)/Resources/options.js +++ b/Shared (Extension)/Resources/options.js @@ -11,12 +11,16 @@ import { saveProfileName, saveRelays, RECOMMENDED_RELAYS, + getPermissions, + setPermission, + KINDS, + humanPermission, } from './utils'; const log = console.log; Alpine.data('options', () => ({ - profileNames: ['Poop'], + profileNames: ['---'], profileIndex: 0, profileName: '', pristineProfileName: '', @@ -27,8 +31,11 @@ Alpine.data('options', () => ({ newRelay: '', urlError: '', recommendedRelay: '', - confirmDelete: false, - confirmClear: false, + permissions: {}, + host: '', + permHosts: [], + hostPerms: [], + setPermission, async init(watch = true) { log('Initialize backend.'); @@ -37,6 +44,11 @@ Alpine.data('options', () => ({ if (watch) { this.$watch('profileIndex', async () => { await this.refreshProfile(); + this.host = ''; + }); + + this.$watch('host', () => { + this.calcHostPerms(); }); this.$watch('recommendedRelay', async () => { @@ -59,8 +71,7 @@ Alpine.data('options', () => ({ await this.getNsec(); await this.getNpub(); await this.getRelays(); - this.confirmClear = false; - this.confirmDelete = false; + await this.getPermissions(); }, // Profile functions @@ -87,8 +98,14 @@ Alpine.data('options', () => ({ }, async deleteProfile() { - await deleteProfile(this.profileIndex); - await this.init(false); + if ( + confirm( + 'This will delete this profile and all associated data. Are you sure you wish to continue?' + ) + ) { + await deleteProfile(this.profileIndex); + await this.init(false); + } }, // Key functions @@ -96,9 +113,13 @@ Alpine.data('options', () => ({ async saveProfile() { if (!this.needsSave) return; + console.log('saving private key'); await savePrivateKey(this.profileIndex, this.privKey); + console.log('saving profile name'); await saveProfileName(this.profileIndex, this.profileName); + console.log('getting profile name'); await this.getProfileNames(); + console.log('refreshing profile'); await this.refreshProfile(); }, @@ -161,11 +182,57 @@ Alpine.data('options', () => ({ }, 3000); }, + // Permissions + + async getPermissions() { + this.permissions = await getPermissions(this.profileIndex); + + // Set the convenience variables + this.calcPermHosts(); + this.calcHostPerms(); + }, + + calcPermHosts() { + let hosts = Object.keys(this.permissions); + hosts.sort(); + this.permHosts = hosts; + }, + + calcHostPerms() { + let hp = this.permissions[this.host] || {}; + let keys = Object.keys(hp); + keys.sort(); + this.hostPerms = keys.map(k => [k, humanPermission(k), hp[k]]); + console.log(this.hostPerms); + }, + + permTypes(hostPerms) { + let k = Object.keys(hostPerms); + k = Object.keys.sort(); + k = k.map(p => { + let e = [p, hostPerms[p]]; + if (p.startsWith('signEvent')) { + let n = parseInt(p.split(':')[1]); + let name = + KINDS.find(kind => kind[0] === n) || `Unknown (Kind ${n})`; + e = [name, hostPerms[p]]; + } + return e; + }); + return k; + }, + // General async clearData() { - await clearData(); - await this.init(false); + if ( + confirm( + 'This will remove your private keys and all associated data. Are you sure you wish to continue?' + ) + ) { + await clearData(); + await this.init(false); + } }, // Properties diff --git a/Shared (Extension)/Resources/permission.html b/Shared (Extension)/Resources/permission.html new file mode 100644 index 0000000..846f0ab --- /dev/null +++ b/Shared (Extension)/Resources/permission.html @@ -0,0 +1,43 @@ + + + + + + + + + + Permission Requested + + + +
+ +

App is requesting permission

+

+ The host + + is requesting the following permission: + . +

+

+ Event kind is . +

+ + + +
+ + + + +
+
+ + + \ No newline at end of file diff --git a/Shared (Extension)/Resources/permission.js b/Shared (Extension)/Resources/permission.js new file mode 100644 index 0000000..82dc77a --- /dev/null +++ b/Shared (Extension)/Resources/permission.js @@ -0,0 +1,103 @@ +import Alpine from 'alpinejs'; +import jsonFormatHighlight from 'json-format-highlight'; +import { KINDS } from './utils'; + +storage = browser.storage.local; + +window.addEventListener('beforeunload', () => { + browser.runtime.sendMessage({ kind: 'closePrompt' }); + return true; +}); + +Alpine.data('permission', () => ({ + host: '', + permission: '', + key: '', + event: '', + remember: false, + + async init() { + let qs = new URLSearchParams(location.search); + console.log(location.search); + this.host = qs.get('host'); + this.permission = qs.get('kind'); + this.key = qs.get('uuid'); + this.event = JSON.parse(qs.get('payload')); + }, + + async allow() { + console.log('allowing'); + await browser.runtime.sendMessage({ + kind: 'allowed', + payload: this.key, + origKind: this.permission, + event: this.event, + remember: this.remember, + host: this.host, + }); + console.log('closing'); + await this.close(); + }, + + async deny() { + await browser.runtime.sendMessage({ + kind: 'denied', + payload: this.key, + origKind: this.permission, + event: this.event, + remember: this.remember, + host: this.host, + }); + await this.close(); + }, + + async close() { + let tab = await browser.tabs.getCurrent(); + console.log('closing current tab: ', tab.id); + await browser.tabs.update(tab.openerTabId, { active: true }); + window.close(); + }, + + async openNip() { + await browser.tabs.create({ url: this.eventInfo.nip, active: true }); + }, + + get humanPermission() { + switch (this.permission) { + case 'getPubKey': + return 'Read public key'; + case 'signEvent': + return 'Sign event'; + case 'getRelays': + return 'Read relay list'; + case 'nip04.encrypt': + return 'Encrypt private message'; + case 'nip04.decrypt': + return 'Decrypt private message'; + default: + break; + } + }, + + get humanEvent() { + return jsonFormatHighlight(this.event); + }, + + get isSigningEvent() { + return this.permission === 'signEvent'; + }, + + get eventInfo() { + if (!this.isSigningEvent) { + return {}; + } + + let [kind, desc, nip] = KINDS.find(([kind, desc, nip]) => { + return kind === this.event.kind; + }) || ['Unknown', 'Unknown', 'https://github.com/nostr-protocol/nips']; + + return { kind, desc, nip }; + }, +})); + +Alpine.start(); diff --git a/Shared (Extension)/Resources/popup.js b/Shared (Extension)/Resources/popup.js index 0097618..8e45c0a 100644 --- a/Shared (Extension)/Resources/popup.js +++ b/Shared (Extension)/Resources/popup.js @@ -1,11 +1,11 @@ import { - bglog, getProfileNames, setProfileIndex, getProfileIndex, getRelays, RECOMMENDED_RELAYS, saveRelays, + initialize, } from './utils'; import Alpine from 'alpinejs'; window.Alpine = Alpine; @@ -19,7 +19,7 @@ Alpine.data('popup', () => ({ async init() { log('Initializing backend.'); - await browser.runtime.sendMessage({ kind: 'init' }); + await initialize(); this.$watch('profileIndex', async () => { await this.loadNames(); diff --git a/Shared (Extension)/Resources/utils.js b/Shared (Extension)/Resources/utils.js index 494aa79..2a1a458 100644 --- a/Shared (Extension)/Resources/utils.js +++ b/Shared (Extension)/Resources/utils.js @@ -1,3 +1,4 @@ +const DB_VERSION = 1; const storage = browser.storage.local; export const RECOMMENDED_RELAYS = [ new URL('wss://relay.damus.io'), @@ -5,18 +6,41 @@ export const RECOMMENDED_RELAYS = [ new URL('wss://nostr-relay.derekross.me'), new URL('wss://relay.snort.social'), ]; +// prettier-ignore +export const KINDS = [ + [0, 'Metadata', 'https://github.com/nostr-protocol/nips/blob/master/01.md'], + [1, 'Text', 'https://github.com/nostr-protocol/nips/blob/master/01.md'], + [2, 'Recommend Relay', 'https://github.com/nostr-protocol/nips/blob/master/01.md'], + [3, 'Contacts', 'https://github.com/nostr-protocol/nips/blob/master/02.md'], + [4, 'Encrypted Direct Messages', 'https://github.com/nostr-protocol/nips/blob/master/04.md'], + [5, 'Event Deletion', 'https://github.com/nostr-protocol/nips/blob/master/09.md'], + [7, 'Reaction', 'https://github.com/nostr-protocol/nips/blob/master/25.md'], + [40, 'Channel Creation', 'https://github.com/nostr-protocol/nips/blob/master/28.md'], + [41, 'Channel Metadata', 'https://github.com/nostr-protocol/nips/blob/master/28.md'], + [42, 'Channel Message', 'https://github.com/nostr-protocol/nips/blob/master/28.md'], + [43, 'Channel Hide Message', 'https://github.com/nostr-protocol/nips/blob/master/28.md'], + [44, 'Channel Mute User', 'https://github.com/nostr-protocol/nips/blob/master/28.md'], +]; export async function initialize() { await getOrSetDefault('profileIndex', 0); await getOrSetDefault('profiles', [await generateProfile()]); - await getOrSetDefault('version', 0); + let version = (await storage.get({ version: 0 })).version; + console.log('DB version: ', version); + while (version < DB_VERSION) { + version = await migrate(version, DB_VERSION); + await storage.set({ version }); + } } -export async function bglog(msg, module = null) { - await browser.runtime.sendMessage({ - kind: 'log', - payload: { msg, module }, - }); +async function migrate(version, goal) { + if (version === 0) { + console.log('Migrating to version 1.'); + let profiles = await getProfiles(); + profiles.forEach(profile => (profile.hosts = {})); + await storage.set({ profiles }); + return version + 1; + } } export async function getProfiles() { @@ -72,7 +96,7 @@ export async function generateProfile(name = 'Default') { return { name, privKey: await generatePrivateKey(), - hosts: [], + hosts: {}, relays: [], }; } @@ -127,3 +151,58 @@ export async function saveRelays(profileIndex, relays) { export async function get(item) { return (await storage.get(item))[item]; } + +export async function getPermissions(index = null) { + if (!index) { + index = await getProfileIndex(); + } + let profile = await getProfile(index); + let hosts = await profile.hosts; + return hosts; +} + +export async function getPermission(host, action) { + let index = await getProfileIndex(); + let profile = await getProfile(index); + console.log(host, action); + console.log('profile: ', profile); + return profile.hosts?.[host]?.[action] || 'ask'; +} + +export async function setPermission(host, action, perm, index = null) { + let profiles = await getProfiles(); + if (!index) { + index = await getProfileIndex(); + } + let profile = profiles[index]; + let newPerms = profile.hosts[host] || {}; + newPerms = { ...newPerms, [action]: perm }; + profile.hosts[host] = newPerms; + profiles[index] = profile; + await storage.set({ profiles }); +} + +export function humanPermission(p) { + // Handle special case where event signing includes a kind number + if (p.startsWith('signEvent:')) { + let [e, n] = p.split(':'); + n = parseInt(n); + let nname = KINDS.find(k => k[0] === n)?.[1] || `Unknown (Kind ${n})`; + return `Sign event: ${nname}`; + } + + switch (p) { + case 'getPubKey': + return 'Read public key'; + case 'signEvent': + return 'Sign event'; + case 'getRelays': + return 'Read relay list'; + case 'nip04.encrypt': + return 'Encrypt private message'; + case 'nip04.decrypt': + return 'Decrypt private message'; + default: + return 'Unknown'; + } +} diff --git a/build.js b/build.js index e37c035..7476a90 100755 --- a/build.js +++ b/build.js @@ -18,6 +18,7 @@ require('esbuild') 'nostr.build': './Shared (Extension)/Resources/nostr.js', 'popup.build': './Shared (Extension)/Resources/popup.js', 'options.build': './Shared (Extension)/Resources/options.js', + 'permission.build': './Shared (Extension)/Resources/permission.js', }, outdir: './Shared (Extension)/Resources', sourcemap: 'inline', diff --git a/extras/pfp.png b/extras/pfp.png new file mode 100644 index 0000000..f3120aa Binary files /dev/null and b/extras/pfp.png differ diff --git a/package-lock.json b/package-lock.json index e12aa96..d3f1203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "license": "ISC", "dependencies": { "alpinejs": "^3.10.5", - "nostr-tools": "^1.1.1" + "async-mutex": "^0.4.0", + "json-format-highlight": "^1.0.4", + "nostr-tools": "^1.2.1" }, "devDependencies": { "@tailwindcss/forms": "^0.5.3", @@ -570,6 +572,14 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/async-mutex": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -879,6 +889,11 @@ "node": ">=0.12.0" } }, + "node_modules/json-format-highlight": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/json-format-highlight/-/json-format-highlight-1.0.4.tgz", + "integrity": "sha512-RqenIjKr1I99XfXPAml9G7YlEZg/GnsH7emWyWJh2yuGXqHW8spN7qx6/ME+MoIBb35/fxrMC9Jauj6nvGe4Mg==" + }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -950,9 +965,9 @@ } }, "node_modules/nostr-tools": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.1.1.tgz", - "integrity": "sha512-mxgjbHR6nx2ACBNa2tBpeM/glsPWqxHPT1Kszx/XfzL+kUdi1Gm3Xz1UcaODQ2F84IFtCKNLO+aF31ZfTAhSYQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.2.1.tgz", + "integrity": "sha512-SL0sst29mrQ7oUPGQn+NMH4yuTe69a8S4bliNpYB2IG0fDl3Cx+xSLnuCTb4nZiNalatYsA5l+751wQiDGA3+A==", "dependencies": { "@noble/hashes": "^0.5.7", "@noble/secp256k1": "^1.7.0", @@ -1322,6 +1337,11 @@ "node": ">=8.0" } }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 083f71b..e9a1a8c 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "license": "ISC", "dependencies": { "alpinejs": "^3.10.5", - "nostr-tools": "^1.1.1" + "async-mutex": "^0.4.0", + "json-format-highlight": "^1.0.4", + "nostr-tools": "^1.2.1" }, "devDependencies": { - "esbuild": "^0.16.17", "@tailwindcss/forms": "^0.5.3", + "esbuild": "^0.16.17", "prettier": "^2.8.3", "tailwindcss": "^3.2.4" } -} \ No newline at end of file +}