Merge pull request #4 from ursuscamp/permissions

Permissions UI with user requests and modification post-saving.
This commit is contained in:
Ryan Breen
2023-02-05 20:43:29 -05:00
committed by GitHub
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"
}
}
}