Merge pull request #21 from fishcakeday/ios18-updates

Make extension work on the latest iOS (18)
This commit is contained in:
Ryan Breen
2024-09-22 14:38:36 -04:00
committed by GitHub
37 changed files with 3136 additions and 2488 deletions

View File

@@ -188,7 +188,6 @@
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>"; };
944A6E02299F2FBB0032C2E3 /* experimental */ = {isa = PBXFileReference; lastKnownFileType = folder; path = experimental; sourceTree = "<group>"; };
944A6E0D299F32070032C2E3 /* wizards */ = {isa = PBXFileReference; lastKnownFileType = folder; path = wizards; sourceTree = "<group>"; };
944A6E12299F39D30032C2E3 /* permission */ = {isa = PBXFileReference; lastKnownFileType = folder; path = permission; sourceTree = "<group>"; };
944A6E38299F46270032C2E3 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
944A6E3E299F46D30032C2E3 /* NostoreApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostoreApp.swift; sourceTree = "<group>"; };
@@ -294,7 +293,6 @@
children = (
9471E91E29A470C700EA623B /* event_history */,
944A6E12299F39D30032C2E3 /* permission */,
944A6E0D299F32070032C2E3 /* wizards */,
944A6E02299F2FBB0032C2E3 /* experimental */,
948C69E4297F8BA600FB3574 /* options.build.css */,
948C69E1297F891F00FB3574 /* options.build.js */,
@@ -461,7 +459,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1500;
LastUpgradeCheck = 1600;
TargetAttributes = {
941B03AE296FA90400CA291E = {
CreatedOnToolsVersion = 14.2;
@@ -503,7 +501,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
944A6E0E299F32070032C2E3 /* wizards in Resources */,
941B03E0296FA90400CA291E /* Icon.png in Resources */,
944A6E03299F2FBB0032C2E3 /* experimental in Resources */,
941B03DE296FA90400CA291E /* Main.html in Resources */,
@@ -520,7 +517,6 @@
files = (
941B03E1296FA90400CA291E /* Icon.png in Resources */,
941B03E3296FA90400CA291E /* Style.css in Resources */,
944A6E0F299F32070032C2E3 /* wizards in Resources */,
941B03E5296FA90400CA291E /* Script.js in Resources */,
944A6E14299F39D30032C2E3 /* permission in Resources */,
941B03E9296FA90400CA291E /* Assets.xcassets in Resources */,
@@ -534,7 +530,6 @@
buildActionMask = 2147483647;
files = (
9471E91F29A470C700EA623B /* event_history in Resources */,
944A6E10299F32070032C2E3 /* wizards in Resources */,
941B0413297110F100CA291E /* background.build.js in Resources */,
944A6E15299F39D30032C2E3 /* permission in Resources */,
948C69E82982DFE900FB3574 /* background.html in Resources */,
@@ -572,7 +567,6 @@
buildActionMask = 2147483647;
files = (
9471E92029A470C700EA623B /* event_history in Resources */,
944A6E11299F32070032C2E3 /* wizards in Resources */,
941B0414297110F100CA291E /* background.build.js in Resources */,
944A6E16299F39D30032C2E3 /* permission in Resources */,
948C69E92982DFE900FB3574 /* background.html in Resources */,
@@ -860,7 +854,6 @@
941B0403296FA90400CA291E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
@@ -902,7 +895,6 @@
941B0404296FA90400CA291E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
@@ -960,7 +952,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.2.0;
OTHER_LDFLAGS = (
"-framework",
@@ -993,7 +985,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 1.2.0;
OTHER_LDFLAGS = (
"-framework",
@@ -1011,7 +1003,6 @@
941B040A296FA90400CA291E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "macOS (App)/nostore.entitlements";
@@ -1048,7 +1039,6 @@
941B040B296FA90400CA291E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "macOS (App)/nostore.entitlements";

View File

@@ -4,10 +4,10 @@ This is a [NIP-07][nip07] compatible extension for signing nostr events.
## Features
* Login with nostr (`getPublicKey`).
* Post nostr event (`signEvent`).
* Encrypted direct messages (`nip04.encrypt` and `nip04.decrypt`).
* Multiple profiles.
- Login with nostr (`getPublicKey`).
- Post nostr event (`signEvent`).
- Encrypted direct messages (`nip04.encrypt` and `nip04.decrypt`).
- Multiple profiles.
## Installation

View File

@@ -1,20 +1,39 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'"
/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<link rel="stylesheet" href="../Style.css">
<link rel="stylesheet" href="../Style.css" />
<script src="../Script.js" defer></script>
</head>
<body>
<img src="../Icon.png" width="128" height="128" alt="Nostore Icon">
<p class="platform-ios">You can turn on Nostores Safari extension in Settings.</p>
<p class="platform-mac state-unknown">You can turn on Nostores extension in Safari Extensions preferences.</p>
<p class="platform-mac state-on">Nostores extension is currently on. You can turn it off in Safari Extensions preferences.</p>
<p class="platform-mac state-off">Nostores extension is currently off. You can turn it on in Safari Extensions preferences.</p>
<button class="platform-mac open-preferences">Quit and Open Safari Extensions Preferences…</button>
<img src="../Icon.png" width="128" height="128" alt="Nostore Icon" />
<p class="platform-ios">
You can turn on Nostores Safari extension in Settings.
</p>
<p class="platform-mac state-unknown">
You can turn on Nostores extension in Safari Extensions
preferences.
</p>
<p class="platform-mac state-on">
Nostores extension is currently on. You can turn it off in Safari
Extensions preferences.
</p>
<p class="platform-mac state-off">
Nostores extension is currently off. You can turn it on in Safari
Extensions preferences.
</p>
<button class="platform-mac open-preferences">
Quit and Open Safari Extensions Preferences…
</button>
</body>
</html>

View File

@@ -2,13 +2,20 @@ function show(platform, enabled, useSettingsInsteadOfPreferences) {
document.body.classList.add(`platform-${platform}`);
if (useSettingsInsteadOfPreferences) {
document.getElementsByClassName('platform-mac state-on')[0].innerText = "Nostores extension is currently on. You can turn it off in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac state-off')[0].innerText = "Nostores extension is currently off. You can turn it on in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on Nostores extension in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…";
document.getElementsByClassName('platform-mac state-on')[0].innerText =
'Nostores extension is currently on. You can turn it off in the Extensions section of Safari Settings.';
document.getElementsByClassName('platform-mac state-off')[0].innerText =
'Nostores extension is currently off. You can turn it on in the Extensions section of Safari Settings.';
document.getElementsByClassName(
'platform-mac state-unknown'
)[0].innerText =
'You can turn on Nostores extension in the Extensions section of Safari Settings.';
document.getElementsByClassName(
'platform-mac open-preferences'
)[0].innerText = 'Quit and Open Safari Settings…';
}
if (typeof enabled === "boolean") {
if (typeof enabled === 'boolean') {
document.body.classList.toggle(`state-on`, enabled);
document.body.classList.toggle(`state-off`, !enabled);
} else {
@@ -18,7 +25,9 @@ function show(platform, enabled, useSettingsInsteadOfPreferences) {
}
function openPreferences() {
webkit.messageHandlers.controller.postMessage("open-preferences");
webkit.messageHandlers.controller.postMessage('open-preferences');
}
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
document
.querySelector('button.open-preferences')
.addEventListener('click', openPreferences);

View File

@@ -1,12 +1,11 @@
import {
generatePrivateKey,
getPublicKey,
signEvent,
nip04,
nip19,
nip26,
getEventHash,
generateSecretKey,
getPublicKey,
finalizeEvent,
} from 'nostr-tools';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { Mutex } from 'async-mutex';
import {
getProfileIndex,
@@ -14,7 +13,6 @@ import {
getProfile,
getPermission,
setPermission,
feature,
} from './utilities/utils';
import { saveEvent } from './utilities/db';
@@ -40,15 +38,13 @@ browser.runtime.onMessage.addListener((message, _sender, sendResponse) => {
deny(message);
return Promise.resolve(true);
case 'generatePrivateKey':
return Promise.resolve(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':
@@ -63,7 +59,7 @@ browser.runtime.onMessage.addListener((message, _sender, sendResponse) => {
case 'nip04.decrypt':
case 'getRelays':
validations[uuid] = sendResponse;
setDelegation(message).then(() => ask(uuid, message));
ask(uuid, message);
setTimeout(() => {
prompt.release?.();
}, 10_000);
@@ -88,6 +84,11 @@ async function forceRelease() {
}
}
async function generatePrivateKey_() {
const sk = generateSecretKey();
return bytesToHex(sk);
}
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();
@@ -178,35 +179,32 @@ async function savePrivateKey([index, privKey]) {
privKey = nip19.decode(privKey).data;
}
let profiles = await get('profiles');
profiles[index].privKey = privKey;
profiles[index].privKey = bytesToHex(privKey);
await storage.set({ profiles });
return true;
}
async function getNsec(index) {
let profile = await getProfile(index);
let nsec = nip19.nsecEncode(profile.privKey);
let nsec = nip19.nsecEncode(hexToBytes(profile.privKey));
return nsec;
}
async function getNpub(index) {
let profile = await getProfile(index);
let pubKey = getPublicKey(profile.privKey);
let pubKey = getPublicKey(hexToBytes(profile.privKey));
let npub = nip19.npubEncode(pubKey);
return npub;
}
async function getPrivKey() {
let profile = await currentProfile();
return profile.privKey;
return hexToBytes(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;
@@ -220,11 +218,8 @@ async function currentProfile() {
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);
let sk = await getPrivKey();
event = finalizeEvent(event, sk);
saveEvent({
event,
metadata: { host, signed_at: Math.round(Date.now() / 1000) },
@@ -253,42 +248,3 @@ async function getRelays() {
});
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,
]);
}

View File

@@ -1,12 +1,11 @@
<!DOCTYPE html>
<!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">
<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="event_history.build.js"></script>
<link rel="stylesheet" href="/options.build.css">
<link rel="stylesheet" href="/options.build.css" />
<title>Event History</title>
<style>
@@ -18,7 +17,9 @@
<body class="text-fuchsia-900 p-3.5 lg:p-32" x-data="eventLog">
<p>
<a href="/options.html" class="border-none hover:underline">← Back</a>
<a href="/options.html" class="border-none hover:underline"
>← Back</a
>
</p>
<h1 class="section-header">Event History</h1>
@@ -29,7 +30,12 @@
<div class="grid grid-cols-2 xl:grid-cols-4 gap-4">
<div>
<label for="view">View</label>
<select id="view" class="input" x-model="view" @change="reload">
<select
id="view"
class="input"
x-model="view"
@change="reload"
>
<option value="created_at">created_at</option>
<option value="kind">kind</option>
<option value="host">host</option>
@@ -39,7 +45,12 @@
<div>
<label for="sort">Order</label>
<select id="sort" x-model="sort" class="input" @change="reload">
<select
id="sort"
x-model="sort"
class="input"
@change="reload"
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
@@ -47,24 +58,47 @@
<div>
<label for="max">Max</label>
<input type="number" x-model.number="max" @input.debounce.750ms="reload" class="input" min="10">
<input
type="number"
x-model.number="max"
@input.debounce.750ms="reload"
class="input"
min="10"
/>
</div>
<div></div>
<div x-show="view === 'created_at'" x-cloak>
<label for="fromCreatedAt">From</label>
<input type="date" id="fromCreatedAt" x-model="fromCreatedAt" class="input" @change="reload">
<input
type="date"
id="fromCreatedAt"
x-model="fromCreatedAt"
class="input"
@change="reload"
/>
</div>
<div x-show="view === 'created_at'" x-cloak>
<label for="toCreatedAt">To</label>
<input type="date" id="toCreatedAt" x-model="toCreatedAt" class="input" @change="reload">
<input
type="date"
id="toCreatedAt"
x-model="toCreatedAt"
class="input"
@change="reload"
/>
</div>
<div x-show="view === 'kind'" x-cloak>
<label for="kindShortcut">Quick Select</label>
<select id="kindShortcut" class="input" @change="quickKindSelect" x-model="quickKind">
<select
id="kindShortcut"
class="input"
@change="quickKindSelect"
x-model="quickKind"
>
<option></option>
<template x-for="k in kinds">
<option :value="k[0]" x-text="k[1]"></option>
@@ -74,17 +108,34 @@
<div x-show="view === 'kind'" x-cloak>
<label for="fromKind">From</label>
<input type="number" id="fromKind" x-model.number="fromKind" class="input" @change="reload">
<input
type="number"
id="fromKind"
x-model.number="fromKind"
class="input"
@change="reload"
/>
</div>
<div x-show="view === 'kind'" x-cloak>
<label for="toKind">To</label>
<input type="number" id="toKind" x-model.number="toKind" class="input" @change="reload">
<input
type="number"
id="toKind"
x-model.number="toKind"
class="input"
@change="reload"
/>
</div>
<div x-show="view === 'host'" x-cloak>
<label for="host">Host</label>
<select id="host" class="input" x-model="host" @change="reload">
<select
id="host"
class="input"
x-model="host"
@change="reload"
>
<option value=""></option>
<template x-for="h in allHosts">
<option :value="h" x-text="h"></option>
@@ -94,7 +145,12 @@
<div x-show="view === 'pubkey'" x-cloak>
<label for="profiles">Profiles</label>
<select id="profiles" class="input" x-model="profile" @change="pkFromProfile">
<select
id="profiles"
class="input"
x-model="profile"
@change="pkFromProfile"
>
<option value=""></option>
<template x-for="p in profileNames">
<option :value="p" x-text="p"></option>
@@ -104,39 +160,69 @@
<div x-show="view === 'pubkey'" x-cloak>
<label for="pubkey">Pubkey</label>
<input type="text" class="input" x-model="pubkey" @input.debounce="reload">
<input
type="text"
class="input"
x-model="pubkey"
@input.debounce="reload"
/>
</div>
</div>
<div>
<button class="button mt-3" @click="saveAll">Save all</button>
<button class="button mt-3" @click="deleteAll">Delete all</button>
<button class="button mt-3" @click="deleteAll">
Delete all
</button>
</div>
</div>
<div class="text-sm italic mt-1 text-right">Click/tap event body to copy the raw event.</div>
<div class="text-sm italic mt-1 text-right">
Click/tap event body to copy the raw event.
</div>
<template x-for="(event, index) in events">
<div class="mt-3 border-solid border border-fuchsia-700 rounded-lg">
<div class="select-none flex cursor-pointer text-sm md:text-xl"
@click="selected = selected === index ? null : index">
<div class="flex-none w-14 p-4 font-extrabold" x-text="selected === index ? '-' : '+'"></div>
<div class="flex-1 w-64 p-4" x-text="formatDate(event.metadata.signed_at)"></div>
<div class="flex-1 w-64 p-4" x-text="event.metadata.host"></div>
<div class="flex-1 w-64 p-4" x-text="formatKind(event.event.kind)"></div>
<div
class="select-none flex cursor-pointer text-sm md:text-xl"
@click="selected = selected === index ? null : index"
>
<div
class="flex-none w-14 p-4 font-extrabold"
x-text="selected === index ? '-' : '+'"
></div>
<div
class="flex-1 w-64 p-4"
x-text="formatDate(event.metadata.signed_at)"
></div>
<div
class="flex-1 w-64 p-4"
x-text="event.metadata.host"
></div>
<div
class="flex-1 w-64 p-4"
x-text="formatKind(event.event.kind)"
></div>
</div>
<div @click.prevent="copyEvent(index)" class="cursor-pointer">
<pre class="rounded-b-lg bg-slate-200 text-sm md:text-xl" x-html="highlight(event)" x-show="selected === index"
x-transition:enter.opacity.delay.75ms x-transition:leave.opacity x-cloak>
</pre>
<pre
class="rounded-b-lg bg-slate-200 text-sm md:text-xl"
x-html="highlight(event)"
x-show="selected === index"
x-transition:enter.opacity.delay.75ms
x-transition:leave.opacity
x-cloak
></pre>
</div>
</div>
</template>
<div class="fixed top-0 right-0 bg-fuchsia-800 rounded-md p-4 text-white m-8 drop-shadow-md" x-show="copied" x-cloack>
<div
class="fixed top-0 right-0 bg-fuchsia-800 rounded-md p-4 text-white m-8 drop-shadow-md"
x-show="copied"
x-cloack
>
Event copied!
</div>
</body>
</html>

View File

@@ -1,34 +1,44 @@
<!DOCTYPE html>
<!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">
<link rel="stylesheet" href="/options.build.css">
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/options.build.css" />
<script src="/experimental/experimental.build.js" defer></script>
<title>Experimental Features</title>
</head>
<body class="text-fuchsia-900 p-3.5 lg:p-32" x-data="experimental">
<p>
<a href="/options.html" class="border-none hover:underline">← Back</a>
<a href="/options.html" class="border-none hover:underline"
>← Back</a
>
</p>
<h1 class="text-3xl lg:text-6xl font-bold md:text-center">Experimental Features</h1>
<h1 class="text-3xl lg:text-6xl font-bold md:text-center">
Experimental Features
</h1>
<p class="mt-3 text-center font-bold italic">
These things may only work partially, or not
work at all. Caveat
These things may only work partially, or not work at all. Caveat
emptor!
</p>
<template x-for="feature in features" :key="feature[0]">
<div class="mt-4">
<input class="checkbox" type="checkbox" :id="feature[0]" x-model="feature[1]"
@change="change(feature[0], feature[1])">
<label :for="feature[0]" x-text="feature[2]" class="font-bold"></label>
<input
class="checkbox"
type="checkbox"
:id="feature[0]"
x-model="feature[1]"
@change="change(feature[0], feature[1])"
/>
<label
:for="feature[0]"
x-text="feature[2]"
class="font-bold"
></label>
<p x-text="feature[3]" class="italic"></p>
</div>
</template>
</body>
</html>

View File

@@ -2,9 +2,9 @@ import Alpine from 'alpinejs';
const FEATURES = [
[
'delegation',
'NIP-26 Delegation Profiles',
'Allow user to create delegated profiles that obey the NIP-26 standard. Requires client support.',
'none',
'NIP-XX: None',
'Reserved for the future use.',
],
];

View File

@@ -17,12 +17,8 @@
},
"content_scripts": [
{
"js": [
"content.build.js"
],
"matches": [
"<all_urls>"
]
"js": ["content.build.js"],
"matches": ["<all_urls>"]
}
],
"action": {
@@ -39,10 +35,7 @@
"options_ui": {
"page": "options.html"
},
"permissions": [
"storage",
"clipboardWrite"
],
"permissions": ["storage", "clipboardWrite"],
"web_accessible_resources": [
{
"resources": [
@@ -51,21 +44,16 @@
"options.build.js",
"options.build.css",
"options.html",
"wizards/delegation/delegation.html",
"wizards/delegation/delegation.build.js",
"wizards/delegation/delegation.build.css",
"experimental/experimental.html",
"experimental/experimental.build.js",
"event_history/event_history.html",
"event_history/event_history.build.js"
],
"matches": [
"<all_urls>"
]
"matches": ["<all_urls>"]
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'unsafe-eval'"
"extension_pages": "object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-src 'self'; font-src 'self'; media-src 'self'; child-src 'self';"
},
"browser_specific_settings": {
"safari": {

View File

@@ -2,7 +2,9 @@
@tailwind components;
@tailwind utilities;
[x-cloak] { display: none; }
[x-cloak] {
display: none;
}
@layer components {
.button {

View File

@@ -1,12 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="options.build.css">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="options.build.css" />
<script defer src="options.build.js"></script>
</head>
<body x-data="options" class="text-fuchsia-900 p-3.5 lg:p-32">
@@ -15,7 +13,7 @@
<!-- PROFILES -->
<div class="mt-6">
<label for="profiles">Profile</label>
<br>
<br />
<select class="input" x-model.number="profileIndex" id="profiles">
<template x-for="(name, index) in profileNames" :key="index">
<option x-text="name" :value="index"></option>
@@ -23,54 +21,80 @@
</select>
<div class="block md:inline p-3 pl-0 md:p-0">
<button class="button" @click.prevent="newProfile">New</button>
<button class="button" @click.prevent="newDelegated" x-show="delegationActive" x-cloak>New Delegate</button>
<button class="button" @click.prevent="deleteProfile">Delete</button>
<button class="button" @click.prevent="deleteProfile">
Delete
</button>
</div>
</div>
<!-- KEYS -->
<div class="section">
<h2 class="section-header">Keys</h2>
<p class="text-sm italic">Provide your <code class="not-italic">nsec</code> or legacy (hexadecimal) private keys.
<p class="text-sm italic">
Provide your <code class="not-italic">nsec</code> or legacy
(hexadecimal) private keys.
</p>
<form @submit.prevent="saveProfile">
<div class="mt-3">
<label for="profile-name">Profile Name</label>
<br>
<input x-model="profileName" type="text" class="input" autocapitalize="off" autocomplete="off" spellcheck="off">
</div>
<div class="mt-3" x-show="delegate" x-cloak>
<div class="mb-1">
<span class="text-red-700 font-bold">This is is a delegated profile.</span>
</div>
<label for="delegator-pub-key">Delegator Public Key</label>
<br>
<input id="delegator-pub-key" x-model="delegator" class="input" autocapitalize="off" autocomplete="off"
spellcheck="off" :disabled="delegate">
<br />
<input
x-model="profileName"
type="text"
class="input"
autocapitalize="off"
autocomplete="off"
spellcheck="off"
/>
</div>
<div class="mt-3">
<label for="priv-key">Private Key</label>
<br>
<input x-model="privKey" :type="visibilityClass" class="input" :class="validKeyClass" autocapitalize="off"
autocomplete="off" spellcheck="off" :disabled="delegate">
<a href="" @click.prevent="visible = !visible" x-text="(visible && 'Hide') || 'Show'" class="border-none"></a>
<br />
<input
x-model="privKey"
:type="visibilityClass"
class="input"
:class="validKeyClass"
autocapitalize="off"
autocomplete="off"
spellcheck="off"
/>
<a
href=""
@click.prevent="visible = !visible"
x-text="(visible && 'Hide') || 'Show'"
class="border-none"
></a>
</div>
<div class="mt-3">
<label for="pub-key">Public Key</label>
<br>
<input x-model="pubKey" type="text" class="input" disabled>
<a href="" class="border-none" @click.prevent="copyPubKey" x-text="copied ? 'Copied!' : 'Copy'"></a>
<br />
<input
x-model="pubKey"
type="text"
class="input"
disabled
/>
<a
href=""
class="border-none"
@click.prevent="copyPubKey"
x-text="copied ? 'Copied!' : 'Copy'"
></a>
</div>
<div class="mt-3">
<button class="button" :disabled="!needsSave || !validKey" @click.prevent="saveProfile">Save</button>
<button
class="button"
:disabled="!needsSave || !validKey"
@click.prevent="saveProfile"
>
Save
</button>
</div>
</form>
</div>
<!-- RELAYS -->
@@ -78,7 +102,9 @@
<h2 class="section-header">Relays</h2>
<p class="text-sm italic">Add relay suggestions for clients.</p>
<template x-if="hasRelays">
<table class="mt-3 text-xs md:text-base table-auto md:table-fixed">
<table
class="mt-3 text-xs md:text-base table-auto md:table-fixed"
>
<thead class="font-bold text-lg">
<td class="p-2 text-center">URL</td>
<td class="p-2 text-center">Read</td>
@@ -89,13 +115,28 @@
<tr>
<td class="p-2 w-1/3" x-text="relay.url"></td>
<td class="p-2 text-center">
<input class="checkbox" type="checkbox" x-model="relay.read" @change="await saveRelays()">
<input
class="checkbox"
type="checkbox"
x-model="relay.read"
@change="await saveRelays()"
/>
</td>
<td class="p-2 text-center">
<input class="checkbox" type="checkbox" x-model="relay.write" @change="await saveRelays()">
<input
class="checkbox"
type="checkbox"
x-model="relay.write"
@change="await saveRelays()"
/>
</td>
<td class="p-2 text-center">
<button class="button" @click.prevent="await deleteRelay(index)">Delete</button>
<button
class="button"
@click.prevent="await deleteRelay(index)"
>
Delete
</button>
</td>
</tr>
</template>
@@ -110,33 +151,49 @@
<div class="mt-3" x-show="hasRecommendedRelays" x-cloak>
<select x-model="recommendedRelay" class="input">
<option value="" disabled selected>Recommended Relays</option>
<option value="" disabled selected>
Recommended Relays
</option>
<template x-for="relay in recommendedRelays">
<option :value="relay" x-text="relay"></option>
</template>
</select>
</div>
<input class="mt-3 input" x-model="newRelay" type="text" @keyup.enter="await addRelay()" placeholder="wss://..."
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="off">
<input
class="mt-3 input"
x-model="newRelay"
type="text"
@keyup.enter="await addRelay()"
placeholder="wss://..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="off"
/>
<div class="block md:inline p-3 pl-0 md:p-0">
<button class="button" @click="await addRelay()">Add</button>
</div>
<div class="text-red-500 font-bold" x-show="urlError.length > 0" x-text="urlError" x-cloak></div>
<div
class="text-red-500 font-bold"
x-show="urlError.length > 0"
x-text="urlError"
x-cloak
></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.
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>
<br />
<select id="app" class="input" x-model="host">
<option value=""></option>
<template x-for="permHost in permHosts">
@@ -145,19 +202,31 @@
</select>
</div>
<p x-show="permHosts.length === 0" x-cloak class="font-bold mt-3">You have not remembered any app requests yet.</p>
<p x-show="permHosts.length === 0" x-cloak 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" x-cloak>
<table
class="mt-3 text-xs md:text-base table-fixed"
x-show="hostPerms.length > 0"
x-cloak
>
<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">
<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)">
<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>
@@ -170,15 +239,23 @@
<div class="mt-6">
<button class="button" @click.prevent="closeOptions">Close</button>
<button class="button" @click.prevent="clearData">Clear Data</button>
<button class="button" @click.prevent="clearData">
Clear Data
</button>
</div>
<div class="mt-6">
<a href="experimental/experimental.html" class="border-none hover:underline">Experimental
features →</a>
<br>
<a href="event_history/event_history.html" class="border-none hover:underline">Event history</a>
<a
href="experimental/experimental.html"
class="border-none hover:underline"
>Experimental features</a
>
<br />
<a
href="event_history/event_history.html"
class="border-none hover:underline"
>Event history →</a
>
</div>
</body>
</html>

View File

@@ -16,8 +16,6 @@ import {
KINDS,
humanPermission,
validateKey,
feature,
getDelegator,
} from './utilities/utils';
const log = console.log;
@@ -42,10 +40,7 @@ Alpine.data('options', () => ({
host: '',
permHosts: [],
hostPerms: [],
delegationActive: false,
visible: false,
delegate: false,
delegator: '',
copied: false,
setPermission,
go,
@@ -77,8 +72,6 @@ Alpine.data('options', () => ({
await this.getProfileIndex();
this.setProfileIndexFromSearch();
await this.refreshProfile();
this.delegationActive = await feature('delegation');
},
async refreshProfile() {
@@ -88,7 +81,6 @@ Alpine.data('options', () => ({
await this.getNpub();
await this.getRelays();
await this.getPermissions();
await this.getDelegate();
},
// Profile functions
@@ -117,27 +109,12 @@ Alpine.data('options', () => ({
this.profileIndex = await getProfileIndex();
},
async getDelegate() {
let [delegate, delegator] = await getDelegator(this.profileIndex);
this.delegate = delegate;
this.delegator = await browser.runtime.sendMessage({
kind: 'npubEncode',
payload: delegator,
});
},
async newProfile() {
let newIndex = await newProfile();
await this.getProfileNames();
this.profileIndex = newIndex;
},
newDelegated() {
window.location = browser.runtime.getURL(
'wizards/delegation/delegation.html'
);
},
async deleteProfile() {
if (
confirm(

View File

@@ -1,28 +1,35 @@
<!DOCTYPE html>
<!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">
<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">
<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>
<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>.
<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>.
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">
@@ -34,10 +41,18 @@
<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">Remember selection<span x-show="isSigningEvent" x-cloak> (by event kind)</span></label>
<input
class="checkbox"
type="checkbox"
id="remember"
x-model="remember"
/>
<label for="remember"
>Remember selection<span x-show="isSigningEvent" x-cloak>
(by event kind)</span
></label
>
</div>
</div>
</body>
</html>

View File

@@ -2,7 +2,6 @@
color-scheme: light dark;
}
body {
width: 300px;
padding: 15px;

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="popup.css">
<link rel="stylesheet" href="options.build.css">
<link rel="stylesheet" href="popup.css" />
<link rel="stylesheet" href="options.build.css" />
<script defer src="popup.build.js"></script>
</head>
@@ -14,19 +13,49 @@
<label for="profile">Active Profile</label>
<div class="profile-buttons flex flex-row gap-2">
<div class="grow">
<select x-model.number="profileIndex" name="profile" id="profile" class="input">
<template x-for="(prof, index) in profileNames" :key="index">
<select
x-model.number="profileIndex"
name="profile"
id="profile"
class="input"
>
<template
x-for="(prof, index) in profileNames"
:key="index"
>
<option x-text="prof" :value="index"></option>
</template>
</select>
</div>
<button class="button p-1.5" @click.prevent="await copyNpub()" style="display: block;" title="Copy npub">
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5"
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000">
<path d="M19.4 20H9.6a.6.6 0 01-.6-.6V9.6a.6.6 0 01.6-.6h9.8a.6.6 0 01.6.6v9.8a.6.6 0 01-.6.6z"
stroke="#f5d0fe" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M15 9V4.6a.6.6 0 00-.6-.6H4.6a.6.6 0 00-.6.6v9.8a.6.6 0 00.6.6H9" stroke="#f5d0fe"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<button
class="button p-1.5"
@click.prevent="await copyNpub()"
style="display: block"
title="Copy npub"
>
<?xml version="1.0" encoding="UTF-8"?><svg
width="24px"
height="24px"
stroke-width="1.5"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="#000000"
>
<path
d="M19.4 20H9.6a.6.6 0 01-.6-.6V9.6a.6.6 0 01.6-.6h9.8a.6.6 0 01.6.6v9.8a.6.6 0 01-.6.6z"
stroke="#f5d0fe"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M15 9V4.6a.6.6 0 00-.6-.6H4.6a.6.6 0 00-.6.6v9.8a.6.6 0 00.6.6H9"
stroke="#f5d0fe"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
</button>
</div>
@@ -34,22 +63,25 @@
<div class="relay" x-show="relayCount < 1 && showRelayReminder" x-cloak>
<span>
You do not have any relays setup for this profile. Would you like to add some recommended
relays now?
You do not have any relays setup for this profile. Would you
like to add some recommended relays now?
</span>
<br>
<button class="button" @click="await addRelays()">Add Relays</button>
<br />
<button class="button" @click="await addRelays()">
Add Relays
</button>
<button class="button" @click="noThanks">No Thanks</button>
</div>
<div class="help">
<button class="button p-1.5" @click="await openOptions()">Settings</button>
<button class="button p-1.5" @click="await openOptions()">
Settings
</button>
</div>
<div class="disclaimer">
No user data is collected or transmitted.
All private keys are stored in the extension's sequestered local browser storage.
No user data is collected or transmitted. All private keys are
stored in the extension's sequestered local browser storage.
</div>
</body>
</html>

View File

@@ -87,7 +87,6 @@ async function migrate(version, goal) {
if (version === 1) {
console.log('migrating to version 2.');
let profiles = await getProfiles();
profiles.forEach(profile => (profile.delegate = false));
await storage.set({ profiles });
return version + 1;
}
@@ -156,7 +155,6 @@ export async function generateProfile(name = 'Default') {
privKey: await generatePrivateKey(),
hosts: {},
relays: [],
delegate: false,
relayReminder: true,
};
}
@@ -293,13 +291,6 @@ export async function toggleRelayReminder() {
await storage.set({ profiles });
}
export async function getDelegator(index) {
let profiles = await getProfiles();
let profile = profiles[index];
console.log(profile);
return [profile.delegate, profile.delegator];
}
export async function getNpub() {
let index = await getProfileIndex();
return await browser.runtime.sendMessage({

View File

@@ -1,59 +0,0 @@
<!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="delegation.build.js"></script>
<link rel="stylesheet" href="/options.build.css">
<title>Delegation Wizard</title>
</head>
<body x-data="delegated" class="text-fuchsia-900 p-3.5 lg:p-32">
<h1 class="text-3xl lg:text-6xl font-bold md:text-center">New Delegated Profile</h1>
<p class="mt-6">
A delegated user, as laid out in the
<a href="https://github.com/nostr-protocol/nips/blob/master/26.md" @click.prevent="openNip($event)">NIP-26
specification</a>,
is a user that is authorized to sign events for a different user. Additional limits can be
put on the delegated account, such as time limits.
</p>
<p class="mt-3">
This may be useful if you wish to keep your main key stored safely offline, but still post as that user
with a throwaway key.
</p>
<div class="section">
<h2 class="section-header">Delegator</h2>
<p class="text-small italic">The secure account to be delegated.</p>
<div class="mt-3">
<label for="delegator">Delegator Private Key</label>
<br>
<input type="text" class="input" x-model="privKey" :class="validKeyClass" autocapitalize="off" autocomplete="off"
spellcheck="off">
<p class="text-xs italic">This is not stored, but only used to sign a token which is stored instead.</p>
</div>
<div class="mt-3">
<label for="duration">Delegated Duration</label>
<br>
<select x-model.number="duration" class="input">
<option value="1">1 day</option>
<option value="7">7 days</option>
<option value="30">30 days</option>
</select>
</div>
</div>
<div class="mt-6">
<button class="button" @click.prevent="goBack">Back</button>
<button class="button" @click.prevent="save" :disabled="!isKeyValid">Save</button>
</div>
</body>
</html>

View File

@@ -1,86 +0,0 @@
import Alpine from 'alpinejs';
import {
generateProfile,
getProfiles,
validateKey,
} from '../../utilities/utils';
import { getPublicKey, nip26, nip19 } from 'nostr-tools';
const storage = browser.storage.local;
Alpine.data('delegated', () => ({
privKey: '',
duration: 7,
profile: {},
async init() {
this.profile = await generateProfile('New Delegate');
this.profile.delegate = true;
},
openNip(event) {
browser.tabs.create({ url: event.target.href, active: true });
},
goBack() {
window.location = browser.runtime.getURL('options.html');
},
async save() {
let profiles = await getProfiles();
// We need to jankify this Alpine proxy object so it's in the right format
// when we save it to storage
let profile = JSON.parse(JSON.stringify(this.profile));
profile.delegator = getPublicKey(this.decodedPrivKey);
profile.delegation = this.getDelegation();
profiles.push(profile);
let profileIndex = profiles.length - 1;
await storage.set({ profiles, profileIndex });
window.location = `${browser.runtime.getURL(
'options.html'
)}?index=${profileIndex}`;
},
getDelegation() {
let pubkey = getPublicKey(this.profile.privKey);
let delegation = nip26.createDelegation(this.decodedPrivKey, {
pubkey,
until: this.until,
since: Math.round(Date.now() / 1000) - 1,
});
console.log(delegation);
return delegation;
},
// Properties
get isKeyValid() {
return validateKey(this.privKey);
},
get validKeyClass() {
return this.isKeyValid
? ''
: 'ring-2 ring-rose-500 focus:ring-2 focus:ring-rose-500 border-transparent focus:border-transparent';
},
get until() {
return Math.round(Date.now() / 1000) + 60 * 60 * 24 * this.duration;
},
get decodedPrivKey() {
if (!this.isKeyValid) {
return null;
}
if (this.privKey.startsWith('nsec')) {
return nip19.decode(this.privKey).data;
}
return this.privKey;
},
}));
Alpine.start();

View File

@@ -1,15 +1,5 @@
#!/usr/bin/env node
let watch =
process.argv[2] === 'watch'
? {
onRebuild(error, result) {
if (error) console.error('watch rebuild failed: ', error);
else console.log('watch rebuild succeeded: ', result);
},
}
: false;
require('esbuild')
.build({
entryPoints: {
@@ -22,8 +12,6 @@ require('esbuild')
'./Shared (Extension)/Resources/permission/permission.js',
'experimental/experimental.build':
'./Shared (Extension)/Resources/experimental/experimental.js',
'wizards/delegation/delegation.build':
'./Shared (Extension)/Resources/wizards/delegation/delegation.js',
'event_history/event_history.build':
'./Shared (Extension)/Resources/event_history/event_history.js',
},
@@ -31,6 +19,5 @@ require('esbuild')
sourcemap: 'inline',
bundle: true,
// minify: true,
watch,
})
.catch(() => process.exit(1));

1428
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"popup.html"
],
"scripts": {
"build": "./build.js",
"build": "tailwindcss -i './Shared (Extension)/Resources/options.css' -o './Shared (Extension)/Resources/options.build.css' && ./build.js",
"watch": "./build.js watch",
"watch-tailwind": "tailwindcss -i './Shared (Extension)/Resources/options.css' -o './Shared (Extension)/Resources/options.build.css' --watch",
"test": "echo \"Error: no test specified\" && exit 1"
@@ -17,16 +17,17 @@
"author": "",
"license": "ISC",
"dependencies": {
"alpinejs": "^3.10.5",
"async-mutex": "^0.4.0",
"idb": "^7.1.1",
"@alpinejs/csp": "^3.14.1",
"alpinejs": "^3.14.1",
"async-mutex": "^0.5.0",
"idb": "^8.0.0",
"json-format-highlight": "^1.0.4",
"nostr-tools": "^1.3.0"
"nostr-tools": "^2.7.2"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"esbuild": "^0.16.17",
"prettier": "^2.8.3",
"tailwindcss": "^3.2.4"
"@tailwindcss/forms": "^0.5.9",
"esbuild": "^0.23.1",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.12"
}
}