Permissions UI with user requests and modification post-saving.

Commits:

Basic, functional permission tab when requesting getPubKey. Only allows one time deny.

getPubKey and getRelays for sure working after prompt.

Changed the prompt to use a query string instead of background script queue. This should prevent any disconnect between the user expecting one thing and getting a different prompt.

It is not working using query string and working quite nicely.

Finally figured out the secret sauce to only open one prompt at a time.

Allow and deny now both work, with the option to remember the request next time.

Still tweaking lots of events to try and get the prompts working smoothly

Nice rendering for the event query message.

Tweaking the migration setup and the tab opening/closing code.

When remembering signing events, it is now scoped by event kind, as well!

Include extra event information in event signing dialogs.

Change confirm buttons to confirm dialog box.

Update nostr-tools to 1.2.1

The interface for app permissions looks good. Ready to work on functionality now.

Don't show app permissions section unless there are things to show.

Fix bug when saving a "Deny".

Additional formatting changes to options page for permissions UI.

Fine permissions seem to be working nicely!

Quick usability fix so that App Permissions section appears on Options page, even when there are no options selected.

Bumping build to #5. Preparing for new build release.
This commit is contained in:
Ryan Breen
2023-01-31 01:09:05 -05:00
parent a833db74aa
commit c832aa22af
16 changed files with 602 additions and 98 deletions

View File

@@ -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 = "<group>"; };
941B042F2978CDF900CA291E /* Icon-16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-16.png"; sourceTree = "<group>"; };
941B04302978CDF900CA291E /* Icon-64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-64.png"; sourceTree = "<group>"; };
944A6DD22988BA200032C2E3 /* permission.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = permission.html; sourceTree = "<group>"; };
944A6DD52988BD230032C2E3 /* permission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = permission.js; sourceTree = "<group>"; };
944A6DD82988D7900032C2E3 /* permission.build.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = permission.build.js; sourceTree = "<group>"; };
948C69D8297F887600FB3574 /* options.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = options.html; sourceTree = "<group>"; };
948C69DB297F88A200FB3574 /* options.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = options.css; sourceTree = "<group>"; };
948C69DC297F88A200FB3574 /* options.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = options.js; sourceTree = "<group>"; };
@@ -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;

View File

@@ -7,12 +7,12 @@
<key>Nostore (iOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
<key>Nostore (macOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
<key>nostore (iOS).xcscheme_^#shared#^_</key>
<dict>

View File

@@ -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) {

View File

@@ -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}`;

View File

@@ -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];
});

View File

@@ -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;
}
}

View File

@@ -22,9 +22,8 @@
</template>
</select>
<div class="block md:inline p-3 pl-0 md:p-0">
<button class="button" @click="await newProfile()">New</button>
<button class="button" @click="confirmDelete = true" x-show="!confirmDelete">Delete</button>
<button class="button" @click="deleteProfile" x-show="confirmDelete">Confirm Delete</button>
<button class="button" @click.prevent="await newProfile()">New</button>
<button class="button" @click.prevent="deleteProfile">Delete</button>
</div>
</div>
@@ -53,7 +52,7 @@
</div>
<div class="mt-3">
<button class="button" :disabled="!needsSave" @click="saveProfile">Save</button>
<button class="button" :disabled="!needsSave" @click.prevent="saveProfile">Save</button>
</div>
</form>
@@ -81,7 +80,7 @@
<input class="checkbox" type="checkbox" x-model="relay.write" @change="await saveRelays()">
</td>
<td class="p-2 text-center">
<button class="button" @click="await deleteRelay(index)">Delete</button>
<button class="button" @click.prevent="await deleteRelay(index)">Delete</button>
</td>
</tr>
</template>
@@ -111,10 +110,52 @@
<div class="text-red-500 font-bold" x-show="urlError.length > 0" x-text="urlError"></div>
</div>
<!-- PERMISSIONS -->
<div class="section">
<h2 class="section-header">App Permissions</h2>
<p class="text-sm italic">
Permissions granted to individual applications.
Everything defaults to <span class="font-bold">Ask</span> unless you have saved a different option.
</p>
<div class="mt-3" x-show="permHosts.length > 0">
<label for="app">Apps</label>
<br>
<select id="app" class="input" x-model="host">
<option value=""></option>
<template x-for="permHost in permHosts">
<option :value="permHost" x-text="permHost"></option>
</template>
</select>
</div>
<p x-show="permHosts.length === 0" class="font-bold mt-3">You have not remembered any app requests yet.</p>
<table class="mt-3 text-xs md:text-base table-fixed" x-show="hostPerms.length > 0">
<thead class="font-bold text-lg">
<td class="p-2 text-center">App Request</td>
<td class="p-2 text-center">Action</td>
</thead>
<template x-for="[etype, humanName, perm] in hostPerms" :key="etype">
<tr>
<td class="p-2 w-1/3 md:w-2/4" x-text="humanName"></td>
<td class="p-2 text-center">
<select class="input" :value="perm"
@change="await setPermission(host, etype, $event.target.value, profileIndex)">
<option value="ask">Ask</option>
<option value="allow">Allow</option>
<option value="deny">Deny</option>
</select>
</td>
</tr>
</template>
</table>
</div>
<div class="mt-6">
<button class="button" @click="window.close()">Close</button>
<button class="button" @click="confirmClear = true" x-show="!confirmClear">Clear Data</button>
<button class="button" @click="clearData" x-show="confirmClear">Confirm Clear</button>
<button class="button" @click.prevent="window.close()">Close</button>
<button class="button" @click.prevent="clearData">Clear Data</button>
</div>
</body>

View File

@@ -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

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script defer src="permission.build.js"></script>
<link rel="stylesheet" href="options.build.css">
<title>Permission Requested</title>
</head>
<body x-data="permission">
<div class="text-center">
<h1 class="section-header mt-5 text-center">App is requesting permission</h1>
<p class="mt-6 text-center">
The host
<span class="text-lg font-bold" x-text="host"></span>
is requesting the following permission:
<span class="text-lg font-bold" x-text="humanPermission"></span>.
</p>
<p x-show="isSigningEvent">
Event kind is <a :href="eventInfo.nip" class="text-lg font-bold" x-text="eventInfo.desc"
@click.prevent="await openNip()"></a>.
</p>
<template x-if="isSigningEvent">
<div class="inline-block text-left">
<pre class="mt-6" x-html="humanEvent"></pre>
</div>
</template>
<div class="mt-6 text-center">
<button class="button" @click="await allow()">Allow</button>
<button class="button" @click="await deny()">Deny</button>
<input class="checkbox" type="checkbox" id="remember" x-model="remember">
<label for="remember">Remeber selection<span x-show="isSigningEvent"> (by event kind)</span></label>
</div>
</div>
</body>
</html>

View File

@@ -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();

View File

@@ -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();

View File

@@ -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';
}
}

View File

@@ -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',

BIN
extras/pfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

28
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}
}