Show/Hide Button for private key in Options page. Move experimental page into separate sub-folder. Move delegation wizard to sub-folder. Move permission page into separate folder. Basic functional SwiftUI look for the app. Beginning to define the main app view. NavigationStack and Privacy Policy Show App Icon on main screen. Getting Started: macOS Getting Started: iPhone Getting Started: iPad Removing old default UIKit code. Added "No Thanks" toggle to the relay reminder. Clearly indicate in the Settings page when a profile is a delegated profile. Changed recommended relays to all public relays. Use x-cloak in all the places. Fix bundle display name to use capital N. Added copy button to pubkey in settings. Window default size. Updating event kind list. Allow events to be copied by clicking on them in the event log. Tweaking the colors for a more purple-ish look. Added Tips and Tricks view to native app. Move utilities modules into separate folder. Rename event_log files to event_history to escape some content blockers. Renamed Event Log to Event History in the UI as well.
293 lines
7.8 KiB
JavaScript
293 lines
7.8 KiB
JavaScript
import {
|
|
generatePrivateKey,
|
|
getPublicKey,
|
|
signEvent,
|
|
nip04,
|
|
nip19,
|
|
nip26,
|
|
getEventHash,
|
|
} from 'nostr-tools';
|
|
import { Mutex } from 'async-mutex';
|
|
import {
|
|
getProfileIndex,
|
|
get,
|
|
getProfile,
|
|
getPermission,
|
|
setPermission,
|
|
feature,
|
|
} from './utilities/utils';
|
|
import { saveEvent } from './utilities/db';
|
|
|
|
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.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);
|
|
case 'createDelegation':
|
|
return createDelegation(message.payload);
|
|
case 'calcPubKey':
|
|
return Promise.resolve(getPublicKey(message.payload));
|
|
case 'npubEncode':
|
|
return Promise.resolve(nip19.npubEncode(message.payload));
|
|
|
|
// window.nostr
|
|
case 'getPubKey':
|
|
case 'signEvent':
|
|
case 'nip04.encrypt':
|
|
case 'nip04.decrypt':
|
|
case 'getRelays':
|
|
validations[uuid] = sendResponse;
|
|
setDelegation(message).then(() => ask(uuid, message));
|
|
setTimeout(() => {
|
|
prompt.release?.();
|
|
}, 10_000);
|
|
return true;
|
|
default:
|
|
return Promise.resolve();
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (permission === 'allow') {
|
|
complete({
|
|
payload: uuid,
|
|
origKind: kind,
|
|
event: payload,
|
|
remember: false,
|
|
host,
|
|
});
|
|
prompt.release();
|
|
return;
|
|
}
|
|
|
|
if (permission === 'deny') {
|
|
deny({ payload: uuid, origKind: kind, host });
|
|
prompt.release();
|
|
return;
|
|
}
|
|
|
|
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/permission.html?${qs.toString()}`,
|
|
openerTabId: tab.id,
|
|
});
|
|
prompt.tabId = p.id;
|
|
return true;
|
|
}
|
|
|
|
function complete({ payload, origKind, event, remember, host }) {
|
|
sendResponse = validations[payload];
|
|
|
|
if (remember) {
|
|
let mKind =
|
|
origKind === 'signEvent' ? `signEvent:${event.kind}` : origKind;
|
|
setPermission(host, mKind, 'allow');
|
|
}
|
|
|
|
if (sendResponse) {
|
|
switch (origKind) {
|
|
case 'getPubKey':
|
|
getPubKey().then(pk => {
|
|
sendResponse(pk);
|
|
});
|
|
break;
|
|
case 'signEvent':
|
|
signEvent_(event, host).then(e => sendResponse(e));
|
|
break;
|
|
case 'nip04.encrypt':
|
|
nip04Encrypt(event).then(e => sendResponse(e));
|
|
break;
|
|
case 'nip04.decrypt':
|
|
nip04Decrypt(event).then(e => sendResponse(e));
|
|
break;
|
|
case 'getRelays':
|
|
getRelays().then(e => sendResponse(e));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function deny({ origKind, host, payload, remember, event }) {
|
|
sendResponse = validations[payload];
|
|
|
|
if (remember) {
|
|
let mKind =
|
|
origKind === 'signEvent' ? `signEvent:${event.kind}` : origKind;
|
|
setPermission(host, mKind, 'deny');
|
|
}
|
|
|
|
sendResponse?.(undefined);
|
|
return false;
|
|
}
|
|
|
|
// Options
|
|
async function savePrivateKey([index, privKey]) {
|
|
if (privKey.startsWith('nsec')) {
|
|
privKey = nip19.decode(privKey).data;
|
|
}
|
|
let profiles = await get('profiles');
|
|
profiles[index].privKey = privKey;
|
|
await storage.set({ profiles });
|
|
return true;
|
|
}
|
|
|
|
async function getNsec(index) {
|
|
let profile = await getProfile(index);
|
|
let nsec = nip19.nsecEncode(profile.privKey);
|
|
return nsec;
|
|
}
|
|
|
|
async function getNpub(index) {
|
|
let profile = await getProfile(index);
|
|
let pubKey = getPublicKey(profile.privKey);
|
|
let npub = nip19.npubEncode(pubKey);
|
|
return npub;
|
|
}
|
|
|
|
async function getPrivKey() {
|
|
let profile = await currentProfile();
|
|
return profile.privKey;
|
|
}
|
|
|
|
async function getPubKey() {
|
|
let pi = await getProfileIndex();
|
|
let profile = await getProfile(pi);
|
|
if (profile.delegate) {
|
|
return profile.delegator;
|
|
}
|
|
let privKey = await getPrivKey();
|
|
let pubKey = getPublicKey(privKey);
|
|
return pubKey;
|
|
}
|
|
|
|
async function currentProfile() {
|
|
let index = await getProfileIndex();
|
|
let profiles = await get('profiles');
|
|
return profiles[index];
|
|
}
|
|
|
|
async function signEvent_(event, host) {
|
|
event = JSON.parse(JSON.stringify(event));
|
|
let privKey = await getPrivKey();
|
|
let pubKey = getPublicKey(privKey);
|
|
event.pubkey = pubKey;
|
|
event.id = getEventHash(event);
|
|
event.sig = signEvent(event, privKey);
|
|
saveEvent({
|
|
event,
|
|
metadata: { host, signed_at: Math.round(Date.now() / 1000) },
|
|
});
|
|
return event;
|
|
}
|
|
|
|
async function nip04Encrypt({ pubKey, plainText }) {
|
|
let privKey = await getPrivKey();
|
|
return nip04.encrypt(privKey, pubKey, plainText);
|
|
}
|
|
|
|
async function nip04Decrypt({ pubKey, cipherText }) {
|
|
let privKey = await getPrivKey();
|
|
return nip04.decrypt(privKey, pubKey, cipherText);
|
|
}
|
|
|
|
async function getRelays() {
|
|
let profile = await currentProfile();
|
|
let relays = profile.relays;
|
|
let relayObj = {};
|
|
// The getRelays call expects this to be returned as an object, not array
|
|
relays.forEach(relay => {
|
|
let { url, read, write } = relay;
|
|
relayObj[url] = { read, write };
|
|
});
|
|
return relayObj;
|
|
}
|
|
|
|
function createDelegation({
|
|
kind,
|
|
delegatorPrivKey,
|
|
delegateePubKey,
|
|
until,
|
|
since,
|
|
}) {
|
|
delegatorPrivKey = nip19.decode(delegatorPrivKey).data;
|
|
let delegation = nip26.createDelegation(delegatorPrivKey, {
|
|
pubkey: delegateePubKey,
|
|
until,
|
|
// kind,
|
|
// since,
|
|
});
|
|
return Promise.resolve(delegation);
|
|
}
|
|
|
|
async function setDelegation({ payload }) {
|
|
if (!payload) return;
|
|
let active = await feature('delegation');
|
|
if (!active) return;
|
|
|
|
let { delegate, delegation } = await currentProfile();
|
|
|
|
// Nothing to do if this is not a delegate
|
|
if (!delegate) {
|
|
return;
|
|
}
|
|
|
|
payload.tags = payload.tags || [];
|
|
|
|
payload.tags.push([
|
|
'delegation',
|
|
delegation.from,
|
|
delegation.cond,
|
|
delegation.sig,
|
|
]);
|
|
}
|