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,12 +1,15 @@
import browser from 'webextension-polyfill'
import {
validateEvent,
getSignature,
finalizeEvent,
getEventHash,
getPublicKey,
nip19
nip19,
utils
} from 'nostr-tools'
import { nip04 } from 'nostr-tools'
const { hexToBytes } = utils
import { Mutex } from 'async-mutex'
import {
@@ -19,7 +22,7 @@ import {
const { encrypt, decrypt } = nip04
let openPrompt = null
let promptMutex = new Mutex()
const promptMutex = new Mutex()
let releasePromptMutex = () => {}
browser.runtime.onInstalled.addListener((_, __, reason) => {
@@ -27,7 +30,7 @@ browser.runtime.onInstalled.addListener((_, __, reason) => {
})
browser.runtime.onMessage.addListener(async (req, sender) => {
let {prompt} = req
const { prompt } = req
if (prompt) {
handlePromptMessage(req, sender)
@@ -38,12 +41,12 @@ browser.runtime.onMessage.addListener(async (req, sender) => {
browser.runtime.onMessageExternal.addListener(
async ({ type, params }, sender) => {
let extensionId = new URL(sender.url).host
const extensionId = new URL(sender.url).host
return handleContentScriptMessage({ type, params, host: extensionId })
}
)
browser.windows.onRemoved.addListener(windowId => {
browser.windows.onRemoved.addListener((_windowId) => {
if (openPrompt) {
// calling this with a simple "no" response will not store anything, so it's fine
// it will just return a failure
@@ -56,15 +59,15 @@ async function handleContentScriptMessage({type, params, host}) {
// authorized, and we won't do anything with private key here, so do a separate handler
switch (type) {
case 'replaceURL': {
let {protocol_handler: ph} = await browser.storage.local.get([
const { protocol_handler: ph } = await browser.storage.local.get([
'protocol_handler'
])
if (!ph) return false
let {url} = params
let raw = url.split('nostr:')[1]
let {type, data} = nip19.decode(raw)
let replacements = {
const { url } = params
const raw = url.split('nostr:')[1]
const { type, data } = nip19.decode(raw)
const replacements = {
raw,
hrp: type,
hex:
@@ -95,7 +98,7 @@ async function handleContentScriptMessage({type, params, host}) {
// acquire mutex here before reading policies
releasePromptMutex = await promptMutex.acquire()
let allowed = await getPermissionStatus(
const allowed = await getPermissionStatus(
host,
type,
type === 'signEvent' ? params.event : undefined
@@ -115,8 +118,8 @@ async function handleContentScriptMessage({type, params, host}) {
} else {
// ask for authorization
try {
let id = Math.random().toString().slice(4)
let qs = new URLSearchParams({
const id = Math.random().toString().slice(4)
const qs = new URLSearchParams({
host,
id,
params: JSON.stringify(params),
@@ -124,7 +127,7 @@ async function handleContentScriptMessage({type, params, host}) {
})
// 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 }
const url = `${browser.runtime.getURL(
'prompt.html'
@@ -158,38 +161,39 @@ async function handleContentScriptMessage({type, params, host}) {
}
// if we're here this means it was accepted
let results = await browser.storage.local.get('private_key')
if (!results || !results.private_key) {
const results = await browser.storage.local.get('private_key')
if (!results?.private_key) {
return { error: 'no private key found' }
}
let sk = results.private_key
const sk = results.private_key
try {
switch (type) {
case 'getPublicKey': {
return getPublicKey(sk)
return getPublicKey(hexToBytes(sk))
}
case 'getRelays': {
let results = await browser.storage.local.get('relays')
const results = await browser.storage.local.get('relays')
return results.relays || {}
}
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 (!validateEvent(event)) return {error: {message: 'invalid event'}}
if (!validateEvent(event))
return { error: { message: 'invalid event' } }
event.sig = await getSignature(event, sk)
return event
const signedEvent = finalizeEvent(event, hexToBytes(sk))
return signedEvent
}
case 'nip04.encrypt': {
let {peer, plaintext} = params
const { peer, plaintext } = params
return encrypt(sk, peer, plaintext)
}
case 'nip04.decrypt': {
let {peer, ciphertext} = params
const { peer, ciphertext } = params
return decrypt(sk, peer, ciphertext)
}
}

View File

@@ -22,21 +22,18 @@ function matchConditions(conditions, 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++) {
let accept = answers[i]
let {conditions} = policies?.[host]?.[accept]?.[type] || {}
const accept = answers[i]
const { conditions } = policies?.[host]?.[accept]?.[type] || {}
if (conditions) {
if (type === 'signEvent') {
if (matchConditions(conditions, event)) {
return accept // may be true or false
} 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 {
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) {
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 (Object.keys(conditions).length === 0) {
conditions = {}
} else {
// 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.kinds && conditions.kinds) {
Object.keys(existingConditions.kinds).forEach(kind => {
Object.keys(existingConditions.kinds).forEach((kind) => {
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
let other = !accept
let reverse = policies?.[host]?.[other]?.[type]
const other = !accept
const reverse = policies?.[host]?.[other]?.[type]
if (
reverse &&
JSON.stringify(reverse.conditions) === JSON.stringify(conditions)
@@ -87,15 +84,15 @@ export async function updatePermission(host, type, accept, conditions) {
}
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]
browser.storage.local.set({ policies })
}
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) {
let action = answer ? 'allowed' : 'denied'
const action = answer ? 'allowed' : 'denied'
browser.notifications.create(undefined, {
type: 'basic',
title: `${type} ${action} for ${host}`,

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ window.nostr = {
},
_call(type, params) {
let id = Math.random().toString().slice(-4)
const id = Math.random().toString().slice(-4)
console.log(
'%c[nostrconnect:%c' +
id +
@@ -60,7 +60,7 @@ window.nostr = {
}
}
window.addEventListener('message', message => {
window.addEventListener('message', (message) => {
if (
!message.data ||
message.data.response === null ||
@@ -71,8 +71,8 @@ window.addEventListener('message', message => {
return
if (message.data.response.error) {
let error = new Error(
`${EXTENSION}: ` + message.data.response.error.message
const error = new Error(
`${EXTENSION}: ${message.data.response.error.message}`
)
error.stack = message.data.response.error.stack
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 (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) {
replacing = false
return

View File

@@ -1,7 +1,7 @@
import browser from 'webextension-polyfill'
import React, {useState, useCallback, useEffect} from 'react'
import { useState, useCallback, useEffect } from 'react'
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 * as Tabs from '@radix-ui/react-tabs'
import { LogoIcon } from './icons'
@@ -9,34 +9,57 @@ import {removePermissions} from './common'
import * as Checkbox from '@radix-ui/react-checkbox'
function Options() {
let [privKey, setPrivKey] = useState('')
let [relays, setRelays] = useState([])
let [newRelayURL, setNewRelayURL] = useState('')
let [policies, setPermissions] = useState([])
let [protocolHandler, setProtocolHandler] = useState('https://njump.me/{raw}')
let [hidingPrivateKey, hidePrivateKey] = useState(true)
let [showNotifications, setNotifications] = useState(false)
let [messages, setMessages] = useState([])
let [handleNostrLinks, setHandleNostrLinks] = useState(false)
let [showProtocolHandlerHelp, setShowProtocolHandlerHelp] = useState(false)
let [unsavedChanges, setUnsavedChanges] = useState([])
const [privKey, setPrivKey] = useState('')
const [relays, setRelays] = useState([])
const [newRelayURL, setNewRelayURL] = useState('')
const [policies, setPermissions] = useState([])
const [protocolHandler, setProtocolHandler] = useState(
'https://njump.me/{raw}'
)
const [hidingPrivateKey, hidePrivateKey] = useState(true)
const [showNotifications, setNotifications] = useState(false)
const [messages, setMessages] = 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)
setMessages(messages)
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(() => {
browser.storage.local
.get(['private_key', 'relays', 'protocol_handler', 'notifications'])
.then(results => {
.then((results) => {
if (results.private_key) {
setPrivKey(nip19.nsecEncode(results.private_key))
}
if (results.relays) {
let relaysList = []
for (let url in results.relays) {
const relaysList = []
for (const url in results.relays) {
relaysList.push({
url,
policy: results.relays[url]
@@ -57,28 +80,7 @@ function Options() {
useEffect(() => {
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)
}
}, [loadPermissions])
return (
<div className="w-screen h-screen flex flex-col items-center justify-center">
@@ -179,7 +181,7 @@ function Options() {
<div className="font-semibold text-base">Preferred Relays:</div>
<div className="flex flex-col gap-2">
{relays.map(({ url, policy }, i) => (
<div key={i} className="flex items-center gap-4">
<div key={url} className="flex items-center gap-4">
<input
value={url}
onChange={changeRelayURL.bind(null, i)}
@@ -205,6 +207,7 @@ function Options() {
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
aria-hidden="true"
>
<path
strokeLinecap="round"
@@ -240,6 +243,7 @@ function Options() {
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
aria-hidden="true"
>
<path
strokeLinecap="round"
@@ -258,6 +262,7 @@ function Options() {
</div>
</div>
<button
type="button"
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"
>
@@ -268,14 +273,15 @@ function Options() {
<div className="flex gap-2">
<input
value={newRelayURL}
onChange={e => setNewRelayURL(e.target.value)}
onKeyDown={e => {
onChange={(e) => setNewRelayURL(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') addNewRelay()
}}
placeholder="wss://"
className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted"
/>
<button
type="button"
disabled={!newRelayURL}
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"
@@ -344,6 +350,7 @@ function Options() {
</td>
<td>
<button
type="button"
onClick={handleRevoke}
data-host={host}
data-accept={accept}
@@ -358,11 +365,11 @@ function Options() {
)}
{!policies.length && (
<tr>
{Array(5)
.fill('N/A')
.map((v, i) => (
<td key={i}>{v}</td>
))}
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
</tr>
)}
</tbody>
@@ -387,6 +394,7 @@ function Options() {
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
aria-hidden="true"
>
<path
strokeLinecap="round"
@@ -413,6 +421,7 @@ function Options() {
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
aria-hidden="true"
>
<path
strokeLinecap="round"
@@ -438,6 +447,7 @@ function Options() {
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
aria-hidden="true"
>
<path
strokeLinecap="round"
@@ -458,7 +468,10 @@ function Options() {
onChange={handleChangeProtocolHandler}
/>
{!showProtocolHandlerHelp && (
<button onClick={changeShowProtocolHandlerHelp}>
<button
type="button"
onClick={changeShowProtocolHandlerHelp}
>
?
</button>
)}
@@ -487,6 +500,7 @@ examples:
</div>
</div>
<button
type="button"
disabled={!unsavedChanges.length}
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"
@@ -498,13 +512,14 @@ examples:
)
async function handleKeyChange(e) {
let key = e.target.value.toLowerCase().trim()
const key = e.target.value.toLowerCase().trim()
setPrivKey(key)
addUnsavedChanges('private_key')
}
async function generate() {
setPrivKey(nip19.nsecEncode(generatePrivateKey()))
const sk = generateSecretKey()
setPrivKey(nip19.nsecEncode(utils.bytesToHex(sk)))
addUnsavedChanges('private_key')
}
@@ -517,7 +532,7 @@ examples:
let hexOrEmptyKey = privKey
try {
let {type, data} = nip19.decode(privKey)
const { type, data } = nip19.decode(privKey)
if (type === 'nsec') hexOrEmptyKey = data
} catch (_) {}
@@ -580,7 +595,7 @@ examples:
}
async function handleRevoke(e) {
let {host, accept, type} = e.target.dataset
const { host, accept, type } = e.target.dataset
if (
window.confirm(
`revoke all ${
@@ -601,7 +616,7 @@ examples:
}
async function requestBrowserNotificationPermissions() {
let granted = await browser.permissions.request({
const granted = await browser.permissions.request({
permissions: ['notifications']
})
if (!granted) setNotifications(false)
@@ -646,14 +661,14 @@ examples:
}
function addUnsavedChanges(section) {
if (!unsavedChanges.find(s => s === section)) {
if (!unsavedChanges.find((s) => s === section)) {
unsavedChanges.push(section)
setUnsavedChanges(unsavedChanges)
}
}
async function saveChanges() {
for (let section of unsavedChanges) {
for (const section of unsavedChanges) {
switch (section) {
case 'private_key':
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) {
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++) {
let accept = answers[i]
let {conditions} = policies?.[host]?.[accept]?.[type] || {}
const accept = answers[i]
const { conditions } = policies?.[host]?.[accept]?.[type] || {}
if (conditions) {
if (type === 'signEvent') {
if (matchConditions(conditions, event)) {
return accept // may be true or false
} 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 {
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) {
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 (Object.keys(conditions).length === 0) {
conditions = {}
} else {
// 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.kinds && conditions.kinds) {
Object.keys(existingConditions.kinds).forEach(kind => {
Object.keys(existingConditions.kinds).forEach((kind) => {
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
let other = !accept
let reverse = policies?.[host]?.[other]?.[type]
const other = !accept
const reverse = policies?.[host]?.[other]?.[type]
if (
reverse &&
JSON.stringify(reverse.conditions) === JSON.stringify(conditions)
@@ -87,15 +84,15 @@ export async function updatePermission(host, type, accept, conditions) {
}
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]
browser.storage.local.set({ policies })
}
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) {
let action = answer ? 'allowed' : 'denied'
const action = answer ? 'allowed' : 'denied'
browser.notifications.create(undefined, {
type: 'basic',
title: `${type} ${action} for ${host}`,

View File

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

View File

@@ -29,7 +29,7 @@ window.nostr = {
},
_call(type, params) {
let id = Math.random().toString().slice(-4)
const id = Math.random().toString().slice(-4)
console.log(
'%c[nostrconnect:%c' +
id +
@@ -60,7 +60,7 @@ window.nostr = {
}
}
window.addEventListener('message', message => {
window.addEventListener('message', (message) => {
if (
!message.data ||
message.data.response === null ||
@@ -71,8 +71,8 @@ window.addEventListener('message', message => {
return
if (message.data.response.error) {
let error = new Error(
`${EXTENSION}: ` + message.data.response.error.message
const error = new Error(
`${EXTENSION}: ${message.data.response.error.message}`
)
error.stack = message.data.response.error.stack
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 (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) {
replacing = false
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 { render } from 'react-dom'
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 { SettingsIcon } from './icons'
import { minidenticon } from 'minidenticons'
import * as Tabs from '@radix-ui/react-tabs'
function Popup() {
let [keys, setKeys] = useState(null)
let avatarURI = useMemo(
const [keys, setKeys] = useState(null)
const avatarURI = useMemo(
() =>
keys
? 'data:image/svg+xml;utf8,' +
@@ -25,27 +25,27 @@ function Popup() {
}
useEffect(() => {
browser.storage.local.get(['private_key', 'relays']).then(results => {
browser.storage.local.get(['private_key', 'relays']).then((results) => {
if (results.private_key) {
let hexKey = getPublicKey(results.private_key)
let npubKey = nip19.npubEncode(hexKey)
const hexKey = getPublicKey(results.private_key)
const npubKey = nip19.npubEncode(hexKey)
setKeys({ npub: npubKey, hex: hexKey })
if (results.relays) {
let relaysList = []
for (let url in results.relays) {
const relaysList = []
for (const url in results.relays) {
if (results.relays[url].write) {
relaysList.push(url)
if (relaysList.length >= 3) break
}
}
if (relaysList.length) {
let nprofileKey = nip19.nprofileEncode({
const nprofileKey = nip19.nprofileEncode({
pubkey: hexKey,
relays: relaysList
})
setKeys(prev => ({...prev, nprofile: nprofileKey}))
setKeys((prev) => ({ ...prev, nprofile: nprofileKey }))
}
}
} else {
@@ -71,6 +71,7 @@ function Popup() {
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
aria-hidden="true"
>
<path
strokeLinecap="round"
@@ -95,6 +96,7 @@ function Popup() {
<img
src={avatarURI}
className="w-9 h-9 rounded-full bg-muted"
alt="Avatar"
/>
) : (
<div className="w-9 h-9 rounded-full bg-muted" />

View File

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

View File

@@ -1,33 +1,33 @@
{
"license": "WTFPL",
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-tabs": "^1.0.4",
"@nostr/tools": "npm:@jsr/nostr__tools@^2.23.3",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-tabs": "^1.1.13",
"async-mutex": "^0.3.2",
"esbuild": "^0.14.54",
"eslint": "^8.54.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-react": "^7.33.2",
"events": "^3.3.0",
"minidenticons": "^4.2.0",
"nostr-tools": "^1.17.0",
"prettier": "^2.8.8",
"minidenticons": "^4.2.1",
"nostr-tools": "^2.8.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-native-svg": "^13.14.0",
"react-qr-code": "^2.0.12",
"react-native-svg": "^13.14.1",
"react-qr-code": "^2.0.18",
"use-boolean-state": "^1.0.2",
"use-debounce": "^7.0.1",
"webextension-polyfill": "^0.8.0"
},
"scripts": {
"dev": "./build.js; pnpm exec tailwindcss -i ./extension/style.css -o ./extension/build/style.css --watch",
"build": "pnpm exec 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: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"
"dev": "./build.js; bunx tailwindcss -i ./extension/style.css -o ./extension/output/style.css --watch",
"build": "bunx tailwindcss -i ./extension/style.css -o ./extension/output/style.css; ./build.js prod",
"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": "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": {
"@biomejs/biome": "^2.4.10",
"esbuild-plugin-copy": "^2.1.1",
"tailwindcss": "^3.3.5"
"tailwindcss": "^3.4.19"
}
}

9197
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