chore: format and lint

This commit is contained in:
Ren Amamiya
2026-04-08 11:28:39 +07:00
parent 5b7b06ff5d
commit 387796faa3
26 changed files with 28646 additions and 25809 deletions

View File

@@ -1,148 +0,0 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 2020,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module",
"allowImportExportEverywhere": false
},
"env": {
"es6": true,
"node": true
},
"plugins": [
"react",
"babel"
],
"globals": {
"document": false,
"navigator": false,
"window": false,
"location": false,
"URL": false,
"URLSearchParams": false,
"fetch": false,
"EventSource": false,
"localStorage": false,
"sessionStorage": false
},
"rules": {
"react/jsx-uses-vars": 2,
"react/jsx-no-undef": 2,
"react/jsx-uses-react": 2,
"accessor-pairs": 2,
"arrow-spacing": [2, { "before": true, "after": true }],
"block-spacing": [2, "always"],
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"comma-dangle": 0,
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"constructor-super": 2,
"curly": [0, "multi-line"],
"dot-location": [2, "property"],
"eol-last": 2,
"eqeqeq": [2, "allow-null"],
"generator-star-spacing": [2, { "before": true, "after": true }],
"handle-callback-err": [2, "^(err|error)$" ],
"indent": 0,
"jsx-quotes": [2, "prefer-double"],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"keyword-spacing": [2, { "before": true, "after": true }],
"new-cap": 0,
"new-parens": 0,
"no-array-constructor": 2,
"no-caller": 2,
"no-class-assign": 2,
"no-cond-assign": 2,
"no-const-assign": 2,
"no-control-regex": 0,
"no-debugger": 0,
"no-delete-var": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-empty-pattern": 2,
"no-eval": 0,
"no-ex-assign": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": [2, "functions"],
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-func-assign": 2,
"no-implied-eval": 2,
"no-inner-declarations": [0, "functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": [2, { "allowLoop": false, "allowSwitch": false }],
"no-lone-blocks": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": [2, { "max": 2 }],
"no-native-reassign": 2,
"no-negated-in-lhs": 2,
"no-new": 0,
"no-new-func": 2,
"no-new-object": 2,
"no-new-require": 2,
"no-new-symbol": 2,
"no-new-wrappers": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-path-concat": 0,
"no-proto": 2,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-return-assign": 0,
"no-self-assign": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-sparse-arrays": 2,
"no-this-before-super": 2,
"no-throw-literal": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-undef-init": 2,
"no-unexpected-multiline": 2,
"no-unneeded-ternary": [2, { "defaultAssignment": false }],
"no-unreachable": 2,
"no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_"}],
"no-useless-call": 2,
"no-useless-constructor": 2,
"no-with": 2,
"one-var": [0, { "initialized": "never" }],
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
"padded-blocks": [2, "never"],
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [2, "always"],
"space-before-function-paren": 0,
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"spaced-comment": 0,
"template-curly-spacing": [2, "never"],
"use-isnan": 2,
"valid-typeof": 2,
"wrap-iife": [2, "any"],
"yield-star-spacing": [2, "both"],
"yoda": 0,
"no-unsafe-optional-chaning": 0
}
}

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io

View File

@@ -1,9 +0,0 @@
arrowParens: avoid
bracketSpacing: false
jsxBracketSameLine: false
printWidth: 80
proseWrap: preserve
semi: false
singleQuote: true
trailingComma: none
useTabs: false

38
AGENTS.md Normal file
View File

@@ -0,0 +1,38 @@
# AGENTS.md
## Build Commands
```bash
bun run dev # Development: runs build.js + tailwindcss --watch
bun run build # Production build → extension/output
bun run package:chrome # Creates extension/releases/nostrconnect_chrome.zip
bun run package:firefox # Creates extension/releases/nostrconnect_firefox.xpi
bun run lint # Run Biome linter
bun run format # Format with Biome (auto-fix)
```
## Key Facts
- **Package manager**: bun (preferred) or pnpm. Uses `@jsr` registry for `@nostr/tools`
- **Build output**: `extension/output/` (not `dist/`)
- **Ignore `extension/output/`**: It contains generated JS/HTML/CSS and is gitignored
- **Tailwind source**: `extension/style.css``extension/output/style.css`
## Architecture
| File | Purpose |
| ---------------------------------------- | --------------------------------------------------------- |
| `background.js` | Core logic: state, permissions, crypto (signEvent, nip04) |
| `nostr-provider.js` | Injected into web pages, provides `window.nostr` |
| `content-script.js` | Bridges provider ↔ background via postMessage |
| `popup.jsx`, `prompt.jsx`, `options.jsx` | React UI components |
| `extension/chrome/manifest.json` | Chrome Manifest V3 config |
| `extension/firefox/manifest.json` | Firefox Manifest V2 config |
The build script (`build.js`) auto-selects the correct manifest based on `prod`/`firefox` args.
## Code Style
- No semicolons
- Single quotes
- Biome (configured in `biome.json`)

View File

@@ -1,13 +0,0 @@
module.exports = api => {
return {
presets: [
[
'@quasar/babel-preset-app',
api.caller(caller => caller && caller.target === 'node')
? { targets: { node: 'current' } }
: {}
]
]
}
}

47
biome.json Normal file
View File

@@ -0,0 +1,47 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": ["**/*.js", "**/*.jsx"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "warn"
},
"style": {
"noNonNullAssertion": "off"
},
"a11y": {
"recommended": true
},
"complexity": {
"recommended": true
},
"suspicious": {
"recommended": true
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded",
"trailingCommas": "none",
"arrowParentheses": "always"
}
}
}

1790
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,16 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import { import {
validateEvent, validateEvent,
getSignature, finalizeEvent,
getEventHash, getEventHash,
getPublicKey, getPublicKey,
nip19 nip19,
utils
} from 'nostr-tools' } from 'nostr-tools'
import {nip04} from 'nostr-tools' import { nip04 } from 'nostr-tools'
import {Mutex} from 'async-mutex'
const { hexToBytes } = utils
import { Mutex } from 'async-mutex'
import { import {
NO_PERMISSIONS_REQUIRED, NO_PERMISSIONS_REQUIRED,
@@ -16,10 +19,10 @@ import {
showNotification showNotification
} from './common' } from './common'
const {encrypt, decrypt} = nip04 const { encrypt, decrypt } = nip04
let openPrompt = null let openPrompt = null
let promptMutex = new Mutex() const promptMutex = new Mutex()
let releasePromptMutex = () => {} let releasePromptMutex = () => {}
browser.runtime.onInstalled.addListener((_, __, reason) => { browser.runtime.onInstalled.addListener((_, __, reason) => {
@@ -27,7 +30,7 @@ browser.runtime.onInstalled.addListener((_, __, reason) => {
}) })
browser.runtime.onMessage.addListener(async (req, sender) => { browser.runtime.onMessage.addListener(async (req, sender) => {
let {prompt} = req const { prompt } = req
if (prompt) { if (prompt) {
handlePromptMessage(req, sender) handlePromptMessage(req, sender)
@@ -37,34 +40,34 @@ browser.runtime.onMessage.addListener(async (req, sender) => {
}) })
browser.runtime.onMessageExternal.addListener( browser.runtime.onMessageExternal.addListener(
async ({type, params}, sender) => { async ({ type, params }, sender) => {
let extensionId = new URL(sender.url).host const extensionId = new URL(sender.url).host
return handleContentScriptMessage({type, params, host: extensionId}) return handleContentScriptMessage({ type, params, host: extensionId })
} }
) )
browser.windows.onRemoved.addListener(windowId => { browser.windows.onRemoved.addListener((_windowId) => {
if (openPrompt) { if (openPrompt) {
// calling this with a simple "no" response will not store anything, so it's fine // calling this with a simple "no" response will not store anything, so it's fine
// it will just return a failure // it will just return a failure
handlePromptMessage({accept: false}, null) handlePromptMessage({ accept: false }, null)
} }
}) })
async function handleContentScriptMessage({type, params, host}) { async function handleContentScriptMessage({ type, params, host }) {
if (NO_PERMISSIONS_REQUIRED[type]) { if (NO_PERMISSIONS_REQUIRED[type]) {
// authorized, and we won't do anything with private key here, so do a separate handler // authorized, and we won't do anything with private key here, so do a separate handler
switch (type) { switch (type) {
case 'replaceURL': { case 'replaceURL': {
let {protocol_handler: ph} = await browser.storage.local.get([ const { protocol_handler: ph } = await browser.storage.local.get([
'protocol_handler' 'protocol_handler'
]) ])
if (!ph) return false if (!ph) return false
let {url} = params const { url } = params
let raw = url.split('nostr:')[1] const raw = url.split('nostr:')[1]
let {type, data} = nip19.decode(raw) const { type, data } = nip19.decode(raw)
let replacements = { const replacements = {
raw, raw,
hrp: type, hrp: type,
hex: hex:
@@ -75,8 +78,8 @@ async function handleContentScriptMessage({type, params, host}) {
: type === 'nevent' : type === 'nevent'
? data.id ? data.id
: null, : null,
p_or_e: {npub: 'p', note: 'e', nprofile: 'p', nevent: 'e'}[type], p_or_e: { npub: 'p', note: 'e', nprofile: 'p', nevent: 'e' }[type],
u_or_n: {npub: 'u', note: 'n', nprofile: 'u', nevent: 'n'}[type], u_or_n: { npub: 'u', note: 'n', nprofile: 'u', nevent: 'n' }[type],
relay0: type === 'nprofile' ? data.relays[0] : null, relay0: type === 'nprofile' ? data.relays[0] : null,
relay1: type === 'nprofile' ? data.relays[1] : null, relay1: type === 'nprofile' ? data.relays[1] : null,
relay2: type === 'nprofile' ? data.relays[2] : null relay2: type === 'nprofile' ? data.relays[2] : null
@@ -95,7 +98,7 @@ async function handleContentScriptMessage({type, params, host}) {
// acquire mutex here before reading policies // acquire mutex here before reading policies
releasePromptMutex = await promptMutex.acquire() releasePromptMutex = await promptMutex.acquire()
let allowed = await getPermissionStatus( const allowed = await getPermissionStatus(
host, host,
type, type,
type === 'signEvent' ? params.event : undefined type === 'signEvent' ? params.event : undefined
@@ -115,8 +118,8 @@ async function handleContentScriptMessage({type, params, host}) {
} else { } else {
// ask for authorization // ask for authorization
try { try {
let id = Math.random().toString().slice(4) const id = Math.random().toString().slice(4)
let qs = new URLSearchParams({ const qs = new URLSearchParams({
host, host,
id, id,
params: JSON.stringify(params), params: JSON.stringify(params),
@@ -124,8 +127,8 @@ async function handleContentScriptMessage({type, params, host}) {
}) })
// prompt will be resolved with true or false // prompt will be resolved with true or false
let accept = await new Promise((resolve, reject) => { const accept = await new Promise((resolve, reject) => {
openPrompt = {resolve, reject} openPrompt = { resolve, reject }
const url = `${browser.runtime.getURL( const url = `${browser.runtime.getURL(
'prompt.html' 'prompt.html'
)}?${qs.toString()}` )}?${qs.toString()}`
@@ -146,7 +149,7 @@ async function handleContentScriptMessage({type, params, host}) {
}) })
// denied, stop here // denied, stop here
if (!accept) return {error: 'denied'} if (!accept) return { error: 'denied' }
} catch (err) { } catch (err) {
// errored, stop here // errored, stop here
releasePromptMutex() releasePromptMutex()
@@ -158,47 +161,48 @@ async function handleContentScriptMessage({type, params, host}) {
} }
// if we're here this means it was accepted // if we're here this means it was accepted
let results = await browser.storage.local.get('private_key') const results = await browser.storage.local.get('private_key')
if (!results || !results.private_key) { if (!results?.private_key) {
return {error: 'no private key found'} return { error: 'no private key found' }
} }
let sk = results.private_key const sk = results.private_key
try { try {
switch (type) { switch (type) {
case 'getPublicKey': { case 'getPublicKey': {
return getPublicKey(sk) return getPublicKey(hexToBytes(sk))
} }
case 'getRelays': { case 'getRelays': {
let results = await browser.storage.local.get('relays') const results = await browser.storage.local.get('relays')
return results.relays || {} return results.relays || {}
} }
case 'signEvent': { case 'signEvent': {
let {event} = params const { event } = params
if (!event.pubkey) event.pubkey = getPublicKey(sk) if (!event.pubkey) event.pubkey = getPublicKey(hexToBytes(sk))
if (!event.id) event.id = getEventHash(event) if (!event.id) event.id = getEventHash(event)
if (!validateEvent(event)) return {error: {message: 'invalid event'}} if (!validateEvent(event))
return { error: { message: 'invalid event' } }
event.sig = await getSignature(event, sk) const signedEvent = finalizeEvent(event, hexToBytes(sk))
return event return signedEvent
} }
case 'nip04.encrypt': { case 'nip04.encrypt': {
let {peer, plaintext} = params const { peer, plaintext } = params
return encrypt(sk, peer, plaintext) return encrypt(sk, peer, plaintext)
} }
case 'nip04.decrypt': { case 'nip04.decrypt': {
let {peer, ciphertext} = params const { peer, ciphertext } = params
return decrypt(sk, peer, ciphertext) return decrypt(sk, peer, ciphertext)
} }
} }
} catch (error) { } catch (error) {
return {error: {message: error.message, stack: error.stack}} return { error: { message: error.message, stack: error.stack } }
} }
} }
async function handlePromptMessage({host, type, accept, conditions}, sender) { async function handlePromptMessage({ host, type, accept, conditions }, sender) {
// return response // return response
openPrompt?.resolve?.(accept) openPrompt?.resolve?.(accept)

View File

@@ -22,21 +22,18 @@ function matchConditions(conditions, event) {
} }
export async function getPermissionStatus(host, type, event) { export async function getPermissionStatus(host, type, event) {
let {policies} = await browser.storage.local.get('policies') const { policies } = await browser.storage.local.get('policies')
let answers = [true, false] const answers = [true, false]
for (let i = 0; i < answers.length; i++) { for (let i = 0; i < answers.length; i++) {
let accept = answers[i] const accept = answers[i]
let {conditions} = policies?.[host]?.[accept]?.[type] || {} const { conditions } = policies?.[host]?.[accept]?.[type] || {}
if (conditions) { if (conditions) {
if (type === 'signEvent') { if (type === 'signEvent') {
if (matchConditions(conditions, event)) { if (matchConditions(conditions, event)) {
return accept // may be true or false return accept // may be true or false
} else { } else {
// if this doesn't match we just continue so it will either match for the opposite answer (reject)
// or it will end up returning undefined at the end
continue
} }
} else { } else {
return accept // may be true or false return accept // may be true or false
@@ -48,17 +45,17 @@ export async function getPermissionStatus(host, type, event) {
} }
export async function updatePermission(host, type, accept, conditions) { export async function updatePermission(host, type, accept, conditions) {
let {policies = {}} = await browser.storage.local.get('policies') const { policies = {} } = await browser.storage.local.get('policies')
// if the new conditions is "match everything", override the previous // if the new conditions is "match everything", override the previous
if (Object.keys(conditions).length === 0) { if (Object.keys(conditions).length === 0) {
conditions = {} conditions = {}
} else { } else {
// if we already had a policy for this, merge the conditions // if we already had a policy for this, merge the conditions
let existingConditions = policies[host]?.[accept]?.[type]?.conditions const existingConditions = policies[host]?.[accept]?.[type]?.conditions
if (existingConditions) { if (existingConditions) {
if (existingConditions.kinds && conditions.kinds) { if (existingConditions.kinds && conditions.kinds) {
Object.keys(existingConditions.kinds).forEach(kind => { Object.keys(existingConditions.kinds).forEach((kind) => {
conditions.kinds[kind] = true conditions.kinds[kind] = true
}) })
} }
@@ -66,8 +63,8 @@ export async function updatePermission(host, type, accept, conditions) {
} }
// if we have a reverse policy (accept / reject) that is exactly equal to this, remove it // if we have a reverse policy (accept / reject) that is exactly equal to this, remove it
let other = !accept const other = !accept
let reverse = policies?.[host]?.[other]?.[type] const reverse = policies?.[host]?.[other]?.[type]
if ( if (
reverse && reverse &&
JSON.stringify(reverse.conditions) === JSON.stringify(conditions) JSON.stringify(reverse.conditions) === JSON.stringify(conditions)
@@ -83,19 +80,19 @@ export async function updatePermission(host, type, accept, conditions) {
created_at: Math.round(Date.now() / 1000) created_at: Math.round(Date.now() / 1000)
} }
browser.storage.local.set({policies}) browser.storage.local.set({ policies })
} }
export async function removePermissions(host, accept, type) { export async function removePermissions(host, accept, type) {
let {policies = {}} = await browser.storage.local.get('policies') const { policies = {} } = await browser.storage.local.get('policies')
delete policies[host]?.[accept]?.[type] delete policies[host]?.[accept]?.[type]
browser.storage.local.set({policies}) browser.storage.local.set({ policies })
} }
export async function showNotification(host, answer, type, params) { export async function showNotification(host, answer, type, params) {
let ok = await browser.storage.local.get('notifications') const ok = await browser.storage.local.get('notifications')
if (ok) { if (ok) {
let action = answer ? 'allowed' : 'denied' const action = answer ? 'allowed' : 'denied'
browser.notifications.create(undefined, { browser.notifications.create(undefined, {
type: 'basic', type: 'basic',
title: `${type} ${action} for ${host}`, title: `${type} ${action} for ${host}`,

View File

@@ -3,14 +3,14 @@ import browser from 'webextension-polyfill'
const EXTENSION = 'nostrconnect' const EXTENSION = 'nostrconnect'
// inject the script that will provide window.nostr // inject the script that will provide window.nostr
let script = document.createElement('script') const script = document.createElement('script')
script.setAttribute('async', 'false') script.setAttribute('async', 'false')
script.setAttribute('type', 'text/javascript') script.setAttribute('type', 'text/javascript')
script.setAttribute('src', browser.runtime.getURL('nostr-provider.js')) script.setAttribute('src', browser.runtime.getURL('nostr-provider.js'))
document.head.appendChild(script) document.head.appendChild(script)
// listen for messages from that script // listen for messages from that script
window.addEventListener('message', async message => { window.addEventListener('message', async (message) => {
if (message.source !== window) return if (message.source !== window) return
if (!message.data) return if (!message.data) return
if (!message.data.params) return if (!message.data.params) return
@@ -25,12 +25,12 @@ window.addEventListener('message', async message => {
host: location.host host: location.host
}) })
} catch (error) { } catch (error) {
response = {error} response = { error }
} }
// return response // return response
window.postMessage( window.postMessage(
{id: message.data.id, ext: EXTENSION, response}, { id: message.data.id, ext: EXTENSION, response },
message.origin message.origin
) )
}) })

View File

@@ -1,5 +1,3 @@
import React from 'react'
export function LogoIcon() { export function LogoIcon() {
return ( return (
<svg <svg
@@ -8,6 +6,7 @@ export function LogoIcon() {
height="56" height="56"
fill="none" fill="none"
viewBox="0 0 56 56" viewBox="0 0 56 56"
aria-label="Nostr Connect logo"
> >
<rect width="56" height="56" fill="#EEECFD" rx="16"></rect> <rect width="56" height="56" fill="#EEECFD" rx="16"></rect>
<rect <rect
@@ -70,6 +69,7 @@ export function SettingsIcon(props) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
aria-label="Settings"
{...props} {...props}
> >
<path <path

View File

@@ -11,7 +11,7 @@ window.nostr = {
}, },
async signEvent(event) { async signEvent(event) {
return this._call('signEvent', {event}) return this._call('signEvent', { event })
}, },
async getRelays() { async getRelays() {
@@ -20,16 +20,16 @@ window.nostr = {
nip04: { nip04: {
async encrypt(peer, plaintext) { async encrypt(peer, plaintext) {
return window.nostr._call('nip04.encrypt', {peer, plaintext}) return window.nostr._call('nip04.encrypt', { peer, plaintext })
}, },
async decrypt(peer, ciphertext) { async decrypt(peer, ciphertext) {
return window.nostr._call('nip04.decrypt', {peer, ciphertext}) return window.nostr._call('nip04.decrypt', { peer, ciphertext })
} }
}, },
_call(type, params) { _call(type, params) {
let id = Math.random().toString().slice(-4) const id = Math.random().toString().slice(-4)
console.log( console.log(
'%c[nostrconnect:%c' + '%c[nostrconnect:%c' +
id + id +
@@ -46,7 +46,7 @@ window.nostr = {
'font-weight:bold;color:#90b12d;font-family:monospace' 'font-weight:bold;color:#90b12d;font-family:monospace'
) )
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._requests[id] = {resolve, reject} this._requests[id] = { resolve, reject }
window.postMessage( window.postMessage(
{ {
id, id,
@@ -60,7 +60,7 @@ window.nostr = {
} }
} }
window.addEventListener('message', message => { window.addEventListener('message', (message) => {
if ( if (
!message.data || !message.data ||
message.data.response === null || message.data.response === null ||
@@ -71,8 +71,8 @@ window.addEventListener('message', message => {
return return
if (message.data.response.error) { if (message.data.response.error) {
let error = new Error( const error = new Error(
`${EXTENSION}: ` + message.data.response.error.message `${EXTENSION}: ${message.data.response.error.message}`
) )
error.stack = message.data.response.error.stack error.stack = message.data.response.error.stack
window.nostr._requests[message.data.id].reject(error) window.nostr._requests[message.data.id].reject(error)
@@ -104,7 +104,9 @@ async function replaceNostrSchemeLink(e) {
if (e.target.tagName !== 'A' || !e.target.href.startsWith('nostr:')) return if (e.target.tagName !== 'A' || !e.target.href.startsWith('nostr:')) return
if (replacing === false) return if (replacing === false) return
let response = await window.nostr._call('replaceURL', {url: e.target.href}) const response = await window.nostr._call('replaceURL', {
url: e.target.href
})
if (response === false) { if (response === false) {
replacing = false replacing = false
return return

View File

@@ -1,42 +1,65 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import React, {useState, useCallback, useEffect} from 'react' import { useState, useCallback, useEffect } from 'react'
import {render} from 'react-dom' import { render } from 'react-dom'
import {generatePrivateKey, nip19} from 'nostr-tools' import { generateSecretKey, nip19, utils } from 'nostr-tools'
import QRCode from 'react-qr-code' import QRCode from 'react-qr-code'
import * as Tabs from '@radix-ui/react-tabs' import * as Tabs from '@radix-ui/react-tabs'
import {LogoIcon} from './icons' import { LogoIcon } from './icons'
import {removePermissions} from './common' import { removePermissions } from './common'
import * as Checkbox from '@radix-ui/react-checkbox' import * as Checkbox from '@radix-ui/react-checkbox'
function Options() { function Options() {
let [privKey, setPrivKey] = useState('') const [privKey, setPrivKey] = useState('')
let [relays, setRelays] = useState([]) const [relays, setRelays] = useState([])
let [newRelayURL, setNewRelayURL] = useState('') const [newRelayURL, setNewRelayURL] = useState('')
let [policies, setPermissions] = useState([]) const [policies, setPermissions] = useState([])
let [protocolHandler, setProtocolHandler] = useState('https://njump.me/{raw}') const [protocolHandler, setProtocolHandler] = useState(
let [hidingPrivateKey, hidePrivateKey] = useState(true) 'https://njump.me/{raw}'
let [showNotifications, setNotifications] = useState(false) )
let [messages, setMessages] = useState([]) const [hidingPrivateKey, hidePrivateKey] = useState(true)
let [handleNostrLinks, setHandleNostrLinks] = useState(false) const [showNotifications, setNotifications] = useState(false)
let [showProtocolHandlerHelp, setShowProtocolHandlerHelp] = useState(false) const [messages, setMessages] = useState([])
let [unsavedChanges, setUnsavedChanges] = useState([]) const [handleNostrLinks, setHandleNostrLinks] = useState(false)
const [showProtocolHandlerHelp, setShowProtocolHandlerHelp] = useState(false)
const [unsavedChanges, setUnsavedChanges] = useState([])
const showMessage = useCallback(msg => { const showMessage = useCallback((msg) => {
messages.push(msg) messages.push(msg)
setMessages(messages) setMessages(messages)
setTimeout(() => setMessages([]), 3000) setTimeout(() => setMessages([]), 3000)
}) })
const loadPermissions = useCallback(async () => {
const { policies = {} } = await browser.storage.local.get('policies')
const list = []
Object.entries(policies).forEach(([host, accepts]) => {
Object.entries(accepts).forEach(([accept, types]) => {
Object.entries(types).forEach(([type, { conditions, created_at }]) => {
list.push({
host,
type,
accept,
conditions,
created_at
})
})
})
})
setPermissions(list)
}, [])
useEffect(() => { useEffect(() => {
browser.storage.local browser.storage.local
.get(['private_key', 'relays', 'protocol_handler', 'notifications']) .get(['private_key', 'relays', 'protocol_handler', 'notifications'])
.then(results => { .then((results) => {
if (results.private_key) { if (results.private_key) {
setPrivKey(nip19.nsecEncode(results.private_key)) setPrivKey(nip19.nsecEncode(results.private_key))
} }
if (results.relays) { if (results.relays) {
let relaysList = [] const relaysList = []
for (let url in results.relays) { for (const url in results.relays) {
relaysList.push({ relaysList.push({
url, url,
policy: results.relays[url] policy: results.relays[url]
@@ -57,28 +80,7 @@ function Options() {
useEffect(() => { useEffect(() => {
loadPermissions() loadPermissions()
}, []) }, [loadPermissions])
async function loadPermissions() {
let {policies = {}} = await browser.storage.local.get('policies')
let list = []
Object.entries(policies).forEach(([host, accepts]) => {
Object.entries(accepts).forEach(([accept, types]) => {
Object.entries(types).forEach(([type, {conditions, created_at}]) => {
list.push({
host,
type,
accept,
conditions,
created_at
})
})
})
})
setPermissions(list)
}
return ( return (
<div className="w-screen h-screen flex flex-col items-center justify-center"> <div className="w-screen h-screen flex flex-col items-center justify-center">
@@ -178,8 +180,8 @@ function Options() {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="font-semibold text-base">Preferred Relays:</div> <div className="font-semibold text-base">Preferred Relays:</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{relays.map(({url, policy}, i) => ( {relays.map(({ url, policy }, i) => (
<div key={i} className="flex items-center gap-4"> <div key={url} className="flex items-center gap-4">
<input <input
value={url} value={url}
onChange={changeRelayURL.bind(null, i)} onChange={changeRelayURL.bind(null, i)}
@@ -205,6 +207,7 @@ function Options() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-4 h-4" className="w-4 h-4"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -240,6 +243,7 @@ function Options() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-4 h-4" className="w-4 h-4"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -258,6 +262,7 @@ function Options() {
</div> </div>
</div> </div>
<button <button
type="button"
onClick={removeRelay.bind(null, i)} onClick={removeRelay.bind(null, i)}
className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted" className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
> >
@@ -268,14 +273,15 @@ function Options() {
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
value={newRelayURL} value={newRelayURL}
onChange={e => setNewRelayURL(e.target.value)} onChange={(e) => setNewRelayURL(e.target.value)}
onKeyDown={e => { onKeyDown={(e) => {
if (e.key === 'Enter') addNewRelay() if (e.key === 'Enter') addNewRelay()
}} }}
placeholder="wss://" placeholder="wss://"
className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted" className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted"
/> />
<button <button
type="button"
disabled={!newRelayURL} disabled={!newRelayURL}
onClick={addNewRelay} onClick={addNewRelay}
className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted" className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
@@ -317,7 +323,7 @@ function Options() {
</thead> </thead>
<tbody> <tbody>
{policies.map( {policies.map(
({host, type, accept, conditions, created_at}) => ( ({ host, type, accept, conditions, created_at }) => (
<tr <tr
key={ key={
host + type + accept + JSON.stringify(conditions) host + type + accept + JSON.stringify(conditions)
@@ -344,6 +350,7 @@ function Options() {
</td> </td>
<td> <td>
<button <button
type="button"
onClick={handleRevoke} onClick={handleRevoke}
data-host={host} data-host={host}
data-accept={accept} data-accept={accept}
@@ -358,11 +365,11 @@ function Options() {
)} )}
{!policies.length && ( {!policies.length && (
<tr> <tr>
{Array(5) <td>N/A</td>
.fill('N/A') <td>N/A</td>
.map((v, i) => ( <td>N/A</td>
<td key={i}>{v}</td> <td>N/A</td>
))} <td>N/A</td>
</tr> </tr>
)} )}
</tbody> </tbody>
@@ -387,6 +394,7 @@ function Options() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-4 h-4" className="w-4 h-4"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -413,6 +421,7 @@ function Options() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-5 h-5" className="w-5 h-5"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -438,6 +447,7 @@ function Options() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-4 h-4" className="w-4 h-4"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -458,7 +468,10 @@ function Options() {
onChange={handleChangeProtocolHandler} onChange={handleChangeProtocolHandler}
/> />
{!showProtocolHandlerHelp && ( {!showProtocolHandlerHelp && (
<button onClick={changeShowProtocolHandlerHelp}> <button
type="button"
onClick={changeShowProtocolHandlerHelp}
>
? ?
</button> </button>
)} )}
@@ -487,6 +500,7 @@ examples:
</div> </div>
</div> </div>
<button <button
type="button"
disabled={!unsavedChanges.length} disabled={!unsavedChanges.length}
onClick={saveChanges} onClick={saveChanges}
className="w-full h-10 bg-primary rounded-xl font-bold inline-flex items-center justify-center text-white disabled:cursor-not-allowed disabled:opacity-70 transform active:translate-y-1 transition-transform ease-in-out duration-75" className="w-full h-10 bg-primary rounded-xl font-bold inline-flex items-center justify-center text-white disabled:cursor-not-allowed disabled:opacity-70 transform active:translate-y-1 transition-transform ease-in-out duration-75"
@@ -498,13 +512,14 @@ examples:
) )
async function handleKeyChange(e) { async function handleKeyChange(e) {
let key = e.target.value.toLowerCase().trim() const key = e.target.value.toLowerCase().trim()
setPrivKey(key) setPrivKey(key)
addUnsavedChanges('private_key') addUnsavedChanges('private_key')
} }
async function generate() { async function generate() {
setPrivKey(nip19.nsecEncode(generatePrivateKey())) const sk = generateSecretKey()
setPrivKey(nip19.nsecEncode(utils.bytesToHex(sk)))
addUnsavedChanges('private_key') addUnsavedChanges('private_key')
} }
@@ -517,7 +532,7 @@ examples:
let hexOrEmptyKey = privKey let hexOrEmptyKey = privKey
try { try {
let {type, data} = nip19.decode(privKey) const { type, data } = nip19.decode(privKey)
if (type === 'nsec') hexOrEmptyKey = data if (type === 'nsec') hexOrEmptyKey = data
} catch (_) {} } catch (_) {}
@@ -544,7 +559,7 @@ examples:
function changeRelayURL(i, ev) { function changeRelayURL(i, ev) {
setRelays([ setRelays([
...relays.slice(0, i), ...relays.slice(0, i),
{url: ev.target.value, policy: relays[i].policy}, { url: ev.target.value, policy: relays[i].policy },
...relays.slice(i + 1) ...relays.slice(i + 1)
]) ])
addUnsavedChanges('relays') addUnsavedChanges('relays')
@@ -555,7 +570,7 @@ examples:
...relays.slice(0, i), ...relays.slice(0, i),
{ {
url: relays[i].url, url: relays[i].url,
policy: {...relays[i].policy, [cat]: !relays[i].policy[cat]} policy: { ...relays[i].policy, [cat]: !relays[i].policy[cat] }
}, },
...relays.slice(i + 1) ...relays.slice(i + 1)
]) ])
@@ -572,7 +587,7 @@ examples:
if (!newRelayURL.startsWith('wss://')) return if (!newRelayURL.startsWith('wss://')) return
relays.push({ relays.push({
url: newRelayURL, url: newRelayURL,
policy: {read: true, write: true} policy: { read: true, write: true }
}) })
setRelays(relays) setRelays(relays)
addUnsavedChanges('relays') addUnsavedChanges('relays')
@@ -580,7 +595,7 @@ examples:
} }
async function handleRevoke(e) { async function handleRevoke(e) {
let {host, accept, type} = e.target.dataset const { host, accept, type } = e.target.dataset
if ( if (
window.confirm( window.confirm(
`revoke all ${ `revoke all ${
@@ -601,14 +616,14 @@ examples:
} }
async function requestBrowserNotificationPermissions() { async function requestBrowserNotificationPermissions() {
let granted = await browser.permissions.request({ const granted = await browser.permissions.request({
permissions: ['notifications'] permissions: ['notifications']
}) })
if (!granted) setNotifications(false) if (!granted) setNotifications(false)
} }
async function saveNotifications() { async function saveNotifications() {
await browser.storage.local.set({notifications: showNotifications}) await browser.storage.local.set({ notifications: showNotifications })
showMessage('saved notifications!') showMessage('saved notifications!')
} }
@@ -616,8 +631,8 @@ examples:
await browser.storage.local.set({ await browser.storage.local.set({
relays: Object.fromEntries( relays: Object.fromEntries(
relays relays
.filter(({url}) => url.trim() !== '') .filter(({ url }) => url.trim() !== '')
.map(({url, policy}) => [url.trim(), policy]) .map(({ url, policy }) => [url.trim(), policy])
) )
}) })
showMessage('saved relays!') showMessage('saved relays!')
@@ -641,19 +656,19 @@ examples:
} }
async function saveNostrProtocolHandlerSettings() { async function saveNostrProtocolHandlerSettings() {
await browser.storage.local.set({protocol_handler: protocolHandler}) await browser.storage.local.set({ protocol_handler: protocolHandler })
showMessage('saved protocol handler!') showMessage('saved protocol handler!')
} }
function addUnsavedChanges(section) { function addUnsavedChanges(section) {
if (!unsavedChanges.find(s => s === section)) { if (!unsavedChanges.find((s) => s === section)) {
unsavedChanges.push(section) unsavedChanges.push(section)
setUnsavedChanges(unsavedChanges) setUnsavedChanges(unsavedChanges)
} }
} }
async function saveChanges() { async function saveChanges() {
for (let section of unsavedChanges) { for (const section of unsavedChanges) {
switch (section) { switch (section) {
case 'private_key': case 'private_key':
await saveKey() await saveKey()

File diff suppressed because it is too large Load Diff

View File

@@ -22,21 +22,18 @@ function matchConditions(conditions, event) {
} }
export async function getPermissionStatus(host, type, event) { export async function getPermissionStatus(host, type, event) {
let {policies} = await browser.storage.local.get('policies') const { policies } = await browser.storage.local.get('policies')
let answers = [true, false] const answers = [true, false]
for (let i = 0; i < answers.length; i++) { for (let i = 0; i < answers.length; i++) {
let accept = answers[i] const accept = answers[i]
let {conditions} = policies?.[host]?.[accept]?.[type] || {} const { conditions } = policies?.[host]?.[accept]?.[type] || {}
if (conditions) { if (conditions) {
if (type === 'signEvent') { if (type === 'signEvent') {
if (matchConditions(conditions, event)) { if (matchConditions(conditions, event)) {
return accept // may be true or false return accept // may be true or false
} else { } else {
// if this doesn't match we just continue so it will either match for the opposite answer (reject)
// or it will end up returning undefined at the end
continue
} }
} else { } else {
return accept // may be true or false return accept // may be true or false
@@ -48,17 +45,17 @@ export async function getPermissionStatus(host, type, event) {
} }
export async function updatePermission(host, type, accept, conditions) { export async function updatePermission(host, type, accept, conditions) {
let {policies = {}} = await browser.storage.local.get('policies') const { policies = {} } = await browser.storage.local.get('policies')
// if the new conditions is "match everything", override the previous // if the new conditions is "match everything", override the previous
if (Object.keys(conditions).length === 0) { if (Object.keys(conditions).length === 0) {
conditions = {} conditions = {}
} else { } else {
// if we already had a policy for this, merge the conditions // if we already had a policy for this, merge the conditions
let existingConditions = policies[host]?.[accept]?.[type]?.conditions const existingConditions = policies[host]?.[accept]?.[type]?.conditions
if (existingConditions) { if (existingConditions) {
if (existingConditions.kinds && conditions.kinds) { if (existingConditions.kinds && conditions.kinds) {
Object.keys(existingConditions.kinds).forEach(kind => { Object.keys(existingConditions.kinds).forEach((kind) => {
conditions.kinds[kind] = true conditions.kinds[kind] = true
}) })
} }
@@ -66,8 +63,8 @@ export async function updatePermission(host, type, accept, conditions) {
} }
// if we have a reverse policy (accept / reject) that is exactly equal to this, remove it // if we have a reverse policy (accept / reject) that is exactly equal to this, remove it
let other = !accept const other = !accept
let reverse = policies?.[host]?.[other]?.[type] const reverse = policies?.[host]?.[other]?.[type]
if ( if (
reverse && reverse &&
JSON.stringify(reverse.conditions) === JSON.stringify(conditions) JSON.stringify(reverse.conditions) === JSON.stringify(conditions)
@@ -83,19 +80,19 @@ export async function updatePermission(host, type, accept, conditions) {
created_at: Math.round(Date.now() / 1000) created_at: Math.round(Date.now() / 1000)
} }
browser.storage.local.set({policies}) browser.storage.local.set({ policies })
} }
export async function removePermissions(host, accept, type) { export async function removePermissions(host, accept, type) {
let {policies = {}} = await browser.storage.local.get('policies') const { policies = {} } = await browser.storage.local.get('policies')
delete policies[host]?.[accept]?.[type] delete policies[host]?.[accept]?.[type]
browser.storage.local.set({policies}) browser.storage.local.set({ policies })
} }
export async function showNotification(host, answer, type, params) { export async function showNotification(host, answer, type, params) {
let ok = await browser.storage.local.get('notifications') const ok = await browser.storage.local.get('notifications')
if (ok) { if (ok) {
let action = answer ? 'allowed' : 'denied' const action = answer ? 'allowed' : 'denied'
browser.notifications.create(undefined, { browser.notifications.create(undefined, {
type: 'basic', type: 'basic',
title: `${type} ${action} for ${host}`, title: `${type} ${action} for ${host}`,

View File

@@ -3,12 +3,7 @@
"description": "Nostr Signer Extension", "description": "Nostr Signer Extension",
"version": "0.1.2", "version": "0.1.2",
"homepage_url": "https://github.com/reyamir/nostr-connect", "homepage_url": "https://github.com/reyamir/nostr-connect",
"manifest_version": 2, "manifest_version": 3,
"browser_specific_settings": {
"gecko": {
"id": "{e665d138-0e5b-4b7a-ab91-7af834eda7a2}"
}
},
"icons": { "icons": {
"16": "icons/icon16.png", "16": "icons/icon16.png",
"32": "icons/icon32.png", "32": "icons/icon32.png",
@@ -17,19 +12,25 @@
}, },
"options_page": "options.html", "options_page": "options.html",
"background": { "background": {
"scripts": ["background.build.js"] "service_worker": "background.build.js"
}, },
"browser_action": { "action": {
"default_title": "Nostr Connect", "default_title": "Nostr Connect",
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["content-script.build.js"] "js": ["content-script.build.js"],
"all_frames": true
} }
], ],
"permissions": ["storage"], "permissions": ["storage"],
"optional_permissions": ["notifications"], "optional_permissions": ["notifications"],
"web_accessible_resources": ["nostr-provider.js"] "web_accessible_resources": [
{
"resources": ["nostr-provider.js"],
"matches": ["https://*/*", "http://localhost:*/*"]
}
]
} }

View File

@@ -11,7 +11,7 @@ window.nostr = {
}, },
async signEvent(event) { async signEvent(event) {
return this._call('signEvent', {event}) return this._call('signEvent', { event })
}, },
async getRelays() { async getRelays() {
@@ -20,16 +20,16 @@ window.nostr = {
nip04: { nip04: {
async encrypt(peer, plaintext) { async encrypt(peer, plaintext) {
return window.nostr._call('nip04.encrypt', {peer, plaintext}) return window.nostr._call('nip04.encrypt', { peer, plaintext })
}, },
async decrypt(peer, ciphertext) { async decrypt(peer, ciphertext) {
return window.nostr._call('nip04.decrypt', {peer, ciphertext}) return window.nostr._call('nip04.decrypt', { peer, ciphertext })
} }
}, },
_call(type, params) { _call(type, params) {
let id = Math.random().toString().slice(-4) const id = Math.random().toString().slice(-4)
console.log( console.log(
'%c[nostrconnect:%c' + '%c[nostrconnect:%c' +
id + id +
@@ -46,7 +46,7 @@ window.nostr = {
'font-weight:bold;color:#90b12d;font-family:monospace' 'font-weight:bold;color:#90b12d;font-family:monospace'
) )
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._requests[id] = {resolve, reject} this._requests[id] = { resolve, reject }
window.postMessage( window.postMessage(
{ {
id, id,
@@ -60,7 +60,7 @@ window.nostr = {
} }
} }
window.addEventListener('message', message => { window.addEventListener('message', (message) => {
if ( if (
!message.data || !message.data ||
message.data.response === null || message.data.response === null ||
@@ -71,8 +71,8 @@ window.addEventListener('message', message => {
return return
if (message.data.response.error) { if (message.data.response.error) {
let error = new Error( const error = new Error(
`${EXTENSION}: ` + message.data.response.error.message `${EXTENSION}: ${message.data.response.error.message}`
) )
error.stack = message.data.response.error.stack error.stack = message.data.response.error.stack
window.nostr._requests[message.data.id].reject(error) window.nostr._requests[message.data.id].reject(error)
@@ -104,7 +104,9 @@ async function replaceNostrSchemeLink(e) {
if (e.target.tagName !== 'A' || !e.target.href.startsWith('nostr:')) return if (e.target.tagName !== 'A' || !e.target.href.startsWith('nostr:')) return
if (replacing === false) return if (replacing === false) return
let response = await window.nostr._call('replaceURL', {url: e.target.href}) const response = await window.nostr._call('replaceURL', {
url: e.target.href
})
if (response === false) { if (response === false) {
replacing = false replacing = false
return return

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,15 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import {render} from 'react-dom' import { render } from 'react-dom'
import {getPublicKey, nip19} from 'nostr-tools' import { getPublicKey, nip19 } from 'nostr-tools'
import React, {useState, useMemo, useEffect} from 'react' import { useState, useMemo, useEffect } from 'react'
import QRCode from 'react-qr-code' import QRCode from 'react-qr-code'
import {SettingsIcon} from './icons' import { SettingsIcon } from './icons'
import {minidenticon} from 'minidenticons' import { minidenticon } from 'minidenticons'
import * as Tabs from '@radix-ui/react-tabs' import * as Tabs from '@radix-ui/react-tabs'
function Popup() { function Popup() {
let [keys, setKeys] = useState(null) const [keys, setKeys] = useState(null)
let avatarURI = useMemo( const avatarURI = useMemo(
() => () =>
keys keys
? 'data:image/svg+xml;utf8,' + ? 'data:image/svg+xml;utf8,' +
@@ -25,27 +25,27 @@ function Popup() {
} }
useEffect(() => { useEffect(() => {
browser.storage.local.get(['private_key', 'relays']).then(results => { browser.storage.local.get(['private_key', 'relays']).then((results) => {
if (results.private_key) { if (results.private_key) {
let hexKey = getPublicKey(results.private_key) const hexKey = getPublicKey(results.private_key)
let npubKey = nip19.npubEncode(hexKey) const npubKey = nip19.npubEncode(hexKey)
setKeys({npub: npubKey, hex: hexKey}) setKeys({ npub: npubKey, hex: hexKey })
if (results.relays) { if (results.relays) {
let relaysList = [] const relaysList = []
for (let url in results.relays) { for (const url in results.relays) {
if (results.relays[url].write) { if (results.relays[url].write) {
relaysList.push(url) relaysList.push(url)
if (relaysList.length >= 3) break if (relaysList.length >= 3) break
} }
} }
if (relaysList.length) { if (relaysList.length) {
let nprofileKey = nip19.nprofileEncode({ const nprofileKey = nip19.nprofileEncode({
pubkey: hexKey, pubkey: hexKey,
relays: relaysList relays: relaysList
}) })
setKeys(prev => ({...prev, nprofile: nprofileKey})) setKeys((prev) => ({ ...prev, nprofile: nprofileKey }))
} }
} }
} else { } else {
@@ -71,6 +71,7 @@ function Popup() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-6 h-6" className="w-6 h-6"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -95,6 +96,7 @@ function Popup() {
<img <img
src={avatarURI} src={avatarURI}
className="w-9 h-9 rounded-full bg-muted" className="w-9 h-9 rounded-full bg-muted"
alt="Avatar"
/> />
) : ( ) : (
<div className="w-9 h-9 rounded-full bg-muted" /> <div className="w-9 h-9 rounded-full bg-muted" />

View File

@@ -1,31 +1,31 @@
import browser from 'webextension-polyfill' import browser from 'webextension-polyfill'
import {render} from 'react-dom' import { render } from 'react-dom'
import React, {useState} from 'react' import { useState } from 'react'
import {PERMISSION_NAMES} from './common' import { PERMISSION_NAMES } from './common'
import {LogoIcon} from './icons' import { LogoIcon } from './icons'
import * as Checkbox from '@radix-ui/react-checkbox' import * as Checkbox from '@radix-ui/react-checkbox'
function Prompt() { function Prompt() {
const [isRemember, setIsRemember] = useState(false) const [isRemember, setIsRemember] = useState(false)
let qs = new URLSearchParams(location.search) const qs = new URLSearchParams(location.search)
let id = qs.get('id') const id = qs.get('id')
let host = qs.get('host') const host = qs.get('host')
let type = qs.get('type') const type = qs.get('type')
let params, event let params, event
try { try {
params = JSON.parse(qs.get('params')) params = JSON.parse(qs.get('params'))
if (Object.keys(params).length === 0) params = null if (Object.keys(params).length === 0) params = null
else if (params.event) event = params.event else if (params.event) event = params.event
} catch (err) { } catch (_err) {
params = null params = null
} }
function authorizeHandler(accept) { function authorizeHandler(accept) {
const conditions = isRemember ? {} : null const conditions = isRemember ? {} : null
return function (ev) { return (ev) => {
ev.preventDefault() ev.preventDefault()
browser.runtime.sendMessage({ browser.runtime.sendMessage({
prompt: true, prompt: true,
@@ -73,6 +73,7 @@ function Prompt() {
strokeWidth={1.5} strokeWidth={1.5}
stroke="currentColor" stroke="currentColor"
className="w-4 h-4" className="w-4 h-4"
aria-hidden="true"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -88,12 +89,14 @@ function Prompt() {
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
type="button"
onClick={authorizeHandler(false)} onClick={authorizeHandler(false)}
className="flex-1 h-10 rounded-lg shadow-sm border border-primary inline-flex items-center justify-center font-semibold" className="flex-1 h-10 rounded-lg shadow-sm border border-primary inline-flex items-center justify-center font-semibold"
> >
Reject Reject
</button> </button>
<button <button
type="button"
onClick={authorizeHandler(true)} onClick={authorizeHandler(true)}
className="flex-1 h-10 rounded-lg shadow-sm border border-secondary bg-primary text-white inline-flex items-center justify-center font-semibold" className="flex-1 h-10 rounded-lg shadow-sm border border-secondary bg-primary text-white inline-flex items-center justify-center font-semibold"
> >

View File

@@ -1,33 +1,33 @@
{ {
"license": "WTFPL", "license": "WTFPL",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.0.4", "@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-tabs": "^1.1.13",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"esbuild": "^0.14.54", "esbuild": "^0.14.54",
"eslint": "^8.54.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-react": "^7.33.2",
"events": "^3.3.0", "events": "^3.3.0",
"minidenticons": "^4.2.0", "minidenticons": "^4.2.1",
"nostr-tools": "^1.17.0", "nostr-tools": "^2.8.1",
"prettier": "^2.8.8",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-native-svg": "^13.14.0", "react-native-svg": "^13.14.1",
"react-qr-code": "^2.0.12", "react-qr-code": "^2.0.18",
"use-boolean-state": "^1.0.2", "use-boolean-state": "^1.0.2",
"use-debounce": "^7.0.1", "use-debounce": "^7.0.1",
"webextension-polyfill": "^0.8.0" "webextension-polyfill": "^0.8.0"
}, },
"scripts": { "scripts": {
"dev": "./build.js; pnpm exec tailwindcss -i ./extension/style.css -o ./extension/build/style.css --watch", "dev": "./build.js; bunx tailwindcss -i ./extension/style.css -o ./extension/output/style.css --watch",
"build": "pnpm exec tailwindcss -i ./extension/style.css -o ./extension/output/style.css; ./build.js prod", "build": "bunx tailwindcss -i ./extension/style.css -o ./extension/output/style.css; ./build.js prod",
"package:chrome": "pnpm exec tailwindcss -i ./extension/style.css -o ./extension/output/style.css --minify; ./build.js prod; cd extension/output; zip -r archive *; cd ../../; mv extension/output/archive.zip extension/releases/nostrconnect_chrome.zip", "package:chrome": "bunx tailwindcss -i ./extension/style.css -o ./extension/output/style.css --minify; ./build.js prod; cd extension/output; zip -r archive *; cd ../../; mv extension/output/archive.zip extension/releases/nostrconnect_chrome.zip",
"package:firefox": "pnpm exec tailwindcss -i ./extension/style.css -o ./extension/output/style.css --minify; ./build.js prod firefox; cd extension/output; zip -r archive *; cd ../../; mv extension/output/archive.zip extension/releases/nostrconnect_firefox.xpi" "package:firefox": "bunx tailwindcss -i ./extension/style.css -o ./extension/output/style.css --minify; ./build.js prod firefox; cd extension/output; zip -r archive *; cd ../../; mv extension/output/archive.zip extension/releases/nostrconnect_firefox.xpi",
"lint": "biome lint ./extension",
"format": "biome format --write ./extension"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.10",
"esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-copy": "^2.1.1",
"tailwindcss": "^3.3.5" "tailwindcss": "^3.4.19"
} }
} }

9201
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
allowBuilds:
esbuild: false