Merge pull request #4 from ursuscamp/permissions
Permissions UI with user requests and modification post-saving.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
43
Shared (Extension)/Resources/permission.html
Normal file
43
Shared (Extension)/Resources/permission.html
Normal 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>
|
||||
103
Shared (Extension)/Resources/permission.js
Normal file
103
Shared (Extension)/Resources/permission.js
Normal 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();
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
1
build.js
1
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',
|
||||
|
||||
BIN
extras/pfp.png
Normal file
BIN
extras/pfp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
28
package-lock.json
generated
28
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user