├── .gitignore ├── popup-screenshot.png ├── dist ├── assets │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.ttf │ │ ├── icomoon.woff │ │ └── icomoon.svg │ ├── icons │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 32x32.png │ │ └── 48x48.png │ ├── img │ │ └── store-banner.jpeg │ └── css │ │ ├── style.build.css.map │ │ └── style.build.css ├── options.html ├── popup.html ├── prompt.html ├── manifest.json └── nostr-provider.js ├── src ├── helpers │ ├── copyToClipboard.jsx │ ├── identicon.jsx │ └── hideStringMiddle.jsx ├── pages │ ├── NotFoundPage.jsx │ ├── HomePage.jsx │ ├── SigninPage.jsx │ ├── SignupPage.jsx │ ├── GeneratorPage.jsx │ └── VaultPage.jsx ├── middlewares │ ├── PrivateRoute.jsx │ └── AuthContext.jsx ├── components │ ├── Secrets.jsx │ ├── ResetVault.jsx │ ├── HeaderVault.jsx │ ├── Navbar.jsx │ ├── ExportVault.jsx │ ├── LockedVault.jsx │ ├── ImportVault.jsx │ ├── Relays.jsx │ ├── ChangePassword.jsx │ └── Accounts.jsx ├── utils.js ├── modals │ ├── Modal.jsx │ ├── SecretsModal.jsx │ ├── DeriveAccountModal.jsx │ ├── AccountDetailsModal.jsx │ ├── GenerateRandomAccountModal.jsx │ ├── ImportAccountModal.jsx │ └── EditAccountModal.jsx ├── content-script.jsx ├── popup.jsx ├── layouts │ └── MainLayout.jsx ├── options.jsx ├── prompt.jsx ├── assets │ └── css │ │ ├── _fonts.scss │ │ ├── style.build.css.map │ │ ├── style.scss │ │ └── style.build.css ├── contexts │ └── MainContext.jsx ├── common.js └── background.jsx ├── package.json ├── privacy.txt ├── README.md └── nostrame.svg /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.build.js 3 | *.zip 4 | -------------------------------------------------------------------------------- /popup-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/popup-screenshot.png -------------------------------------------------------------------------------- /dist/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /dist/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /dist/assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/128x128.png -------------------------------------------------------------------------------- /dist/assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/16x16.png -------------------------------------------------------------------------------- /dist/assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/32x32.png -------------------------------------------------------------------------------- /dist/assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/48x48.png -------------------------------------------------------------------------------- /dist/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /dist/assets/img/store-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/img/store-banner.jpeg -------------------------------------------------------------------------------- /src/helpers/copyToClipboard.jsx: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify' 2 | 3 | export default function copyToClipboard(e, text) { 4 | e.preventDefault() 5 | navigator.clipboard.writeText(text) 6 | toast.success("Copied with success") 7 | } -------------------------------------------------------------------------------- /src/helpers/identicon.jsx: -------------------------------------------------------------------------------- 1 | import Identicon from "identicon.js" 2 | 3 | export default async function getIdenticon(pubkey) { 4 | if (pubkey.length < 15) return "" 5 | 6 | const svg = new Identicon(pubkey, { format: "svg" }).toString() 7 | return svg 8 | } 9 | -------------------------------------------------------------------------------- /dist/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nostrame 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dist/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nostrame 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dist/prompt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nostrame 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router-dom' 3 | 4 | const NotFoundPage = () => { 5 | return ( 6 |
7 |

404 Not Found

8 |

This page does not exist

9 | Go Back 11 |
12 | ) 13 | } 14 | 15 | export default NotFoundPage 16 | -------------------------------------------------------------------------------- /src/middlewares/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Navigate } from 'react-router-dom'; 3 | import { useAuth } from './AuthContext'; 4 | 5 | const PrivateRoute = ({ Component }) => { 6 | const { isAuthenticated } = useAuth(); 7 | 8 | return isAuthenticated ? : ; 9 | }; 10 | 11 | export default PrivateRoute; -------------------------------------------------------------------------------- /src/components/Secrets.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import SecretsModal from '../modals/SecretsModal' 3 | 4 | const Secrets = () => { 5 | const [showSecretsModal, setShowSecretsModal] = useState(false) 6 | 7 | return ( 8 | <> 9 |

Secrets

10 | 11 | 12 | setShowSecretsModal(false)} 15 | > 16 | 17 | ) 18 | } 19 | export default Secrets 20 | -------------------------------------------------------------------------------- /src/helpers/hideStringMiddle.jsx: -------------------------------------------------------------------------------- 1 | export default function hideStringMiddle(inputString, startChars = 10, endChars = 8) { 2 | if (inputString.length <= startChars + endChars) { 3 | return inputString; // Return the string as is if its length is less than or equal to the combined length of startChars and endChars 4 | } 5 | 6 | const hiddenPart = '.'.repeat(3); // Create a string of dots (or any character you want to use to hide) 7 | 8 | // Slice and combine the string to show the startChars, hiddenPart, and endChars 9 | const result = inputString.slice(0, startChars) + hiddenPart + inputString.slice(-endChars); 10 | 11 | return result; 12 | } -------------------------------------------------------------------------------- /src/components/ResetVault.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React from 'react' 3 | 4 | const ResetVault = ({ fetchData }) => { 5 | const handleResetVault = async () => { 6 | if (confirm("Are you sure you want to reset the vault? Make sure if you have made a backup before you continue.")) { 7 | await browser.storage.local.set({ 8 | encryptedVault: '', 9 | vault: {}, 10 | password: '', 11 | isAuthenticated: false, 12 | }) 13 | fetchData() 14 | } 15 | } 16 | 17 | return ( 18 | <> 19 |

Reset Vault

20 | 21 | 22 | 23 | ) 24 | } 25 | export default ResetVault 26 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export class LRUCache { 2 | constructor(maxSize) { 3 | this.maxSize = maxSize 4 | this.map = new Map() 5 | this.keys = [] 6 | } 7 | 8 | clear() { 9 | this.map.clear() 10 | } 11 | 12 | has(k) { 13 | return this.map.has(k) 14 | } 15 | 16 | get(k) { 17 | const v = this.map.get(k) 18 | 19 | if (v !== undefined) { 20 | this.keys.push(k) 21 | 22 | if (this.keys.length > this.maxSize * 2) { 23 | this.keys.splice(-this.maxSize) 24 | } 25 | } 26 | 27 | return v 28 | } 29 | 30 | set(k, v) { 31 | this.map.set(k, v) 32 | this.keys.push(k) 33 | 34 | if (this.map.size > this.maxSize) { 35 | this.map.delete(this.keys.shift()) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nostrame", 3 | "version": "0.1.0", 4 | "license": "WTFPL", 5 | "type": "module", 6 | "dependencies": { 7 | "async-mutex": "^0.5.0", 8 | "crypto-js": "^4.2.0", 9 | "identicon.js": "^2.3.3", 10 | "nostr-tools": "^2.5.1", 11 | "qrcode.react": "^3.1.0", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.23.1", 15 | "react-toastify": "^10.0.5", 16 | "webextension-polyfill": "^0.11.0" 17 | }, 18 | "scripts": { 19 | "build": "./build.js prod", 20 | "watch": "node ./build.js watch", 21 | "package": "./build.js prod; cd dist; zip -r archive *; cd ..; mv dist/archive.zip ./nostrame.zip" 22 | }, 23 | "devDependencies": { 24 | "chokidar": "^3.6.0", 25 | "esbuild": "0.21.3", 26 | "sass": "^1.77.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modals/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const Modal = ({ id, isOpen, onClose, children }) => { 4 | const [isActive, setIsActive] = useState(isOpen); 5 | 6 | useEffect(() => { 7 | setIsActive(isOpen) 8 | }, [isOpen]) 9 | 10 | const closeModal = () => { 11 | setIsActive(false); 12 | onClose(); 13 | }; 14 | 15 | return ( 16 | <> 17 | {isActive && ( 18 |
19 |
20 |
e.stopPropagation()}> 21 | 22 | {children} 23 |
24 |
25 |
26 | )} 27 | 28 | ); 29 | }; 30 | 31 | export default Modal; 32 | 33 | -------------------------------------------------------------------------------- /src/components/HeaderVault.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | import Accounts from '../components/Accounts' 4 | 5 | const HeaderVault = () => { 6 | const [isAuthenticated, setIsAuthenticated] = useState(false) 7 | 8 | useEffect(() => { 9 | fetchData() 10 | 11 | browser.storage.onChanged.addListener(function(changes, area) { 12 | fetchData() 13 | }) 14 | }, []) 15 | 16 | const fetchData = async () => { 17 | const storage = await browser.storage.local.get(['isAuthenticated']) 18 | 19 | if (storage.isAuthenticated) { 20 | setIsAuthenticated(true) 21 | } 22 | } 23 | 24 | return ( 25 |
26 |

27 | Nostrame 28 |

29 | 30 | { isAuthenticated && ( 31 | 32 | )} 33 |
34 | ) 35 | } 36 | export default HeaderVault 37 | -------------------------------------------------------------------------------- /privacy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy for Nostrame 2 | 3 | Nostrame does not collect, store, or share any personal information from its users. All data processed by the extension remains locally on the user’s device and is not transmitted to any external servers, except when shared with default relays or user-defined relays, which are used as part of the standard operation of the Nostr protocol. 4 | 5 | The data shared with relays consists exclusively of the information that the user chooses to make public, in accordance with the functionalities provided by the Nostr protocol. 6 | 7 | We are committed to ensuring your privacy and only share data with relays necessary for the proper operation of the extension within the Nostr network. If future updates to the extension require additional data processing, this privacy policy will be updated accordingly. 8 | 9 | If you have any questions regarding this privacy policy, feel free to contact us. 10 | -------------------------------------------------------------------------------- /src/middlewares/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { createContext, useContext, useState, useEffect } from 'react' 3 | 4 | const AuthContext = createContext() 5 | 6 | export const AuthProvider = ({ children }) => { 7 | const [isAuthenticated, setIsAuthenticated] = useState(false) 8 | 9 | useEffect(() => { 10 | fetchData() 11 | }, []) 12 | 13 | const fetchData = async () => { 14 | const storage = await browser.storage.local.get('isAuthenticated') 15 | 16 | setIsAuthenticated(storage.isAuthenticated === true) 17 | } 18 | 19 | const login = async () => { 20 | setIsAuthenticated(true) 21 | } 22 | 23 | const logout = () => { 24 | // Perform logout logic 25 | setIsAuthenticated(false) 26 | localStorage.removeItem('isAuthenticated') 27 | } 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export const useAuth = () => useContext(AuthContext) 37 | -------------------------------------------------------------------------------- /src/content-script.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | 3 | // inject the script that will provide window.nostr 4 | let script = document.createElement('script') 5 | script.setAttribute('async', 'false') 6 | script.setAttribute('type', 'text/javascript') 7 | script.setAttribute('src', browser.runtime.getURL('nostr-provider.js')) 8 | document.head.appendChild(script) 9 | 10 | // listen for messages from that script 11 | window.addEventListener('message', async message => { 12 | if (message.source !== window) return 13 | if (!message.data) return 14 | if (!message.data.params) return 15 | if (message.data.ext !== 'Nostrame') return 16 | 17 | // pass on to background 18 | var response 19 | try { 20 | response = await browser.runtime.sendMessage({ 21 | type: message.data.type, 22 | params: message.data.params, 23 | host: location.host 24 | }) 25 | } catch (error) { 26 | response = {error} 27 | } 28 | 29 | // return response 30 | window.postMessage( 31 | {id: message.data.id, ext: 'Nostrame', response}, 32 | message.origin 33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nostrame(Nostr Account Management Extension) 2 | 3 | Nostrame, a powerful Chromium extension that acts as a secure vault for managing your accounts. 4 | 5 | With Nostrame, you can: 6 | 7 | - Derive accounts from a mnemonic seed 8 | - Generate random mnemonic accounts 9 | - NIP-07 - window.nostr capability for web browsers 10 | - Import external accounts 11 | - Set basic metadata on Nostr 12 | - Enjoy encryption secured by a master password 13 | - Lock and unlock the vault with ease 14 | - Easily import and export backups 15 | 16 | Nostrame Popup 17 | 18 | This extension is Chromium-only. 19 | 20 | ## Install 21 | 22 | - [Chrome Extension](https://chromewebstore.google.com/detail/nostrame/phfdiknibomfgpefcicfckkklimoniej) 23 | 24 | ## Develop 25 | 26 | To run the plugin from this code: 27 | 28 | ``` 29 | git clone https://github.com/Anderson-Juhasc/nostrame 30 | cd nostrame 31 | npm i 32 | npm run build 33 | ``` 34 | 35 | then 36 | 37 | 1. go to `chrome://extensions`; 38 | 2. ensure "developer mode" is enabled on the top right; 39 | 3. click on "Load unpackaged"; 40 | 4. select the `extension/` folder of this repository. 41 | 42 | --- 43 | 44 | LICENSE: public domain. 45 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nostrame", 3 | "description": "Nostr Accounts Managing Extension", 4 | "version": "0.1.0", 5 | "homepage_url": "https://github.com/Anderson-Juhasc/nostrame", 6 | "manifest_version": 3, 7 | "icons": { 8 | "16": "assets/icons/16x16.png", 9 | "32": "assets/icons/32x32.png", 10 | "48": "assets/icons/48x48.png", 11 | "128": "assets/icons/128x128.png" 12 | }, 13 | "background": { 14 | "service_worker": "background.build.js" 15 | }, 16 | "options_page": "options.html", 17 | "action": { 18 | "default_title": "Nostrame", 19 | "default_popup": "popup.html" 20 | }, 21 | "content_scripts": [ 22 | { 23 | "run_at": "document_end", 24 | "matches": [""], 25 | "js": ["content-script.build.js"], 26 | "all_frames": true 27 | } 28 | ], 29 | "permissions": ["storage", "windows"], 30 | "optional_permissions": ["notifications"], 31 | "web_accessible_resources": [ 32 | { 33 | "resources": ["nostr-provider.js"], 34 | "matches": [ 35 | "https://*/*", 36 | "http://localhost:*/*", 37 | "http://0.0.0.0:*/*", 38 | "http://127.0.0.1:*/*", 39 | "http://*.localhost/*" 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React from 'react' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Navbar = () => { 6 | const openOptionsButton = async () => { 7 | if (browser.runtime.openOptionsPage) { 8 | browser.runtime.openOptionsPage() 9 | } else { 10 | window.open(browser.runtime.getURL('options.html')) 11 | } 12 | } 13 | 14 | return ( 15 |
16 | 39 |
40 | ) 41 | } 42 | 43 | export default Navbar -------------------------------------------------------------------------------- /src/components/ExportVault.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React from 'react' 3 | 4 | const ExportVault = () => { 5 | const handleVaultExport = async () => { 6 | const storage = await browser.storage.local.get(['encryptedVault']) 7 | const jsonData = JSON.stringify({ vault: storage.encryptedVault }, null, 2) 8 | const blob = new Blob([jsonData], { type: 'application/json' }) 9 | const url = URL.createObjectURL(blob) 10 | const a = document.createElement('a') 11 | 12 | const currentDate = new Date() 13 | const year = currentDate.getFullYear() 14 | const month = ('0' + (currentDate.getMonth() + 1)).slice(-2) // Adding 1 to month since it's zero-based 15 | const day = ('0' + currentDate.getDate()).slice(-2) 16 | const hours = ('0' + currentDate.getHours()).slice(-2) 17 | const minutes = ('0' + currentDate.getMinutes()).slice(-2) 18 | const seconds = ('0' + currentDate.getSeconds()).slice(-2) 19 | 20 | a.href = url 21 | a.download = `NostrameVaultData.${year}_${month}_${day}_${hours}_${minutes}_${seconds}.json` 22 | a.click() 23 | } 24 | 25 | return ( 26 | <> 27 |

Export Vault

28 | 29 | 30 | ) 31 | } 32 | export default ExportVault 33 | -------------------------------------------------------------------------------- /src/popup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HashRouter as Router, Route, Routes } from 'react-router-dom' 3 | import { createRoot } from 'react-dom/client' 4 | import { AuthProvider } from './middlewares/AuthContext' 5 | import PrivateRoute from './middlewares/PrivateRoute' 6 | import MainLayout from './layouts/MainLayout' 7 | import HomePage from './pages/HomePage' 8 | import SigninPage from './pages/SigninPage' 9 | import SignupPage from './pages/SignupPage' 10 | import VaultPage from './pages/VaultPage' 11 | import NotFoundPage from './pages/NotFoundPage' 12 | import GeneratorPage from './pages/GeneratorPage' 13 | 14 | const App = () => { 15 | return ( 16 | 17 | 18 | 19 | }> 20 | } /> 21 | 22 | }> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | const container = document.getElementById('main') 36 | const root = createRoot(container) 37 | root.render() 38 | -------------------------------------------------------------------------------- /src/components/LockedVault.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState } from 'react' 3 | import { decrypt } from '../common' 4 | 5 | const LockedVault = () => { 6 | const [password, setPassword] = useState('') 7 | 8 | const unlockVault = async (e) => { 9 | e.preventDefault() 10 | const storage = await browser.storage.local.get(['encryptedVault']) 11 | const vaultData = decrypt(storage.encryptedVault, password) 12 | await browser.storage.local.set({ 13 | isLocked: false, 14 | vault: vaultData, 15 | password, 16 | }) 17 | setPassword('') 18 | window.location.reload() 19 | } 20 | 21 | return ( 22 |
23 |
24 |

Nostrame

25 |
26 | 27 |
28 |
29 |
30 | 31 |
32 | setPassword(e.target.value)} 40 | /> 41 |
42 | 47 |
48 |
49 |
50 | ) 51 | } 52 | export default LockedVault 53 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | import { Outlet } from 'react-router-dom' 4 | import { ToastContainer } from 'react-toastify' 5 | import HeaderVault from '../components/HeaderVault' 6 | import Navbar from '../components/Navbar' 7 | import LockedVault from '../components/LockedVault' 8 | import { MainProvider } from '../contexts/MainContext' 9 | 10 | const MainLayout = () => { 11 | const [isLocked, setIsLocked] = useState(false) 12 | const [isAuthenticated, setIsAuthenticated] = useState(false) 13 | 14 | useEffect(() => { 15 | fetchData() 16 | 17 | browser.storage.onChanged.addListener(function(changes, area) { 18 | fetchData() 19 | }) 20 | }, []) 21 | 22 | const fetchData = async () => { 23 | const storage = await browser.storage.local.get(['isLocked', 'isAuthenticated']) 24 | 25 | if (storage.isLocked) { 26 | setIsLocked(true) 27 | } 28 | 29 | if (storage.isAuthenticated) { 30 | setIsAuthenticated(true) 31 | } 32 | } 33 | 34 | return ( 35 | <> 36 | {isLocked ? ( 37 | <> 38 | 39 | 40 | ) : ( 41 | <> 42 | 43 | 44 | 45 | {isAuthenticated && ( 46 | 47 | )} 48 | 49 | 50 | 51 | )} 52 | 53 | ) 54 | } 55 | 56 | export default MainLayout 57 | -------------------------------------------------------------------------------- /src/components/ImportVault.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState } from 'react' 3 | import { decrypt } from '../common' 4 | 5 | const ImportVault = ({ fetchData }) => { 6 | const [file, setFile] = useState(null) 7 | const [password, setPassword] = useState('') 8 | 9 | const handleFileChange = (e) => { 10 | e.preventDefault() 11 | const file = e.target.files[0] 12 | setFile(file) 13 | } 14 | 15 | const handleVaultImport = (e) => { 16 | e.preventDefault() 17 | if (file) { 18 | const reader = new FileReader() 19 | reader.onload = async () => { 20 | const encryptedVault = (JSON.parse(reader.result)).vault 21 | try { 22 | const vaultData = decrypt(encryptedVault, password) 23 | await browser.storage.local.set({ 24 | vault: vaultData, 25 | encryptedVault, 26 | isAuthenticated: true, 27 | password 28 | }) 29 | setPassword('') 30 | fetchData() 31 | } catch (e) { 32 | console.log(e) 33 | } 34 | } 35 | reader.readAsText(file) 36 | } 37 | } 38 | 39 | return ( 40 | <> 41 |

Import Vault

42 | 43 |
44 | 45 |
46 | setPassword(e.target.value)} 53 | /> 54 |
55 | 56 |
57 | 58 | ) 59 | } 60 | export default ImportVault 61 | -------------------------------------------------------------------------------- /src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import { Link, Navigate, useNavigate } from 'react-router-dom' 3 | import React, { useEffect, useContext } from 'react' 4 | import { useAuth } from '../middlewares/AuthContext'; 5 | import MainContext from '../contexts/MainContext' 6 | 7 | const HomePage = () => { 8 | const { isAuthenticated } = useAuth() 9 | 10 | if (isAuthenticated) return 11 | 12 | const { updateAccounts } = useContext(MainContext) 13 | 14 | const { login } = useAuth() 15 | const navigate = useNavigate() 16 | 17 | useEffect(() => { 18 | browser.storage.onChanged.addListener(async function(changes, area) { 19 | if (changes.isAuthenticated) { 20 | await login() 21 | await updateAccounts() 22 | navigate('/vault') 23 | } 24 | }) 25 | }, []) 26 | 27 | return ( 28 | <> 29 |
30 |
31 |

Welcome to
Nostrame

32 | 33 | 34 | 35 |
36 | Import existing Vault 37 |
38 | Already have a Vault? Import it using your seed phrase or encrypted keystore file 39 | 40 |
41 | 42 | 43 |
44 | Create new Vault 45 |
46 | New to Nostrame Vault? Let's set it up! This will create a new vault and seed phrase 47 | 48 |
49 |
50 | 51 | ) 52 | } 53 | 54 | export default HomePage 55 | -------------------------------------------------------------------------------- /src/components/Relays.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | 4 | const Relays = () => { 5 | const [relay, setRelay] = useState('') 6 | const [relays, setRelays] = useState([]) 7 | 8 | useEffect(() => { 9 | fetchData() 10 | 11 | browser.storage.onChanged.addListener(function() { 12 | fetchData() 13 | }) 14 | }, []) 15 | 16 | const fetchData = async () => { 17 | const storage = await browser.storage.local.get(['relays']) 18 | setRelays(storage.relays) 19 | } 20 | 21 | const addNewRelay = async (e) => { 22 | e.preventDefault() 23 | 24 | const relayExist = relays.find(item => item === relay) 25 | if (relayExist) { 26 | alert('Please provide a not existing relay') 27 | setRelay('') 28 | return false 29 | } 30 | 31 | relays.push(relay) 32 | setRelays(relays) 33 | setRelay('') 34 | await browser.storage.local.set({ 35 | relays: relays, 36 | }) 37 | } 38 | 39 | const removeRelay = async (index) => { 40 | const newRelays = [...relays] 41 | if (index !== -1) { 42 | newRelays.splice(index, 1) 43 | setRelays(newRelays) 44 | } 45 | await browser.storage.local.set({ 46 | relays: newRelays, 47 | }) 48 | } 49 | 50 | return ( 51 | <> 52 |
53 |

Relays

54 | setRelay(e.target.value)} 61 | /> 62 |
63 | 64 |
65 | 66 |
    67 | {relays.map((relay, index) => ( 68 |
  • 69 | {relay} 70 |   71 | 72 |
  • 73 | ))} 74 |
75 | 76 | ) 77 | } 78 | export default Relays 79 | -------------------------------------------------------------------------------- /src/modals/SecretsModal.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | import Modal from './Modal' 4 | import { decrypt } from '../common' 5 | 6 | const SecretsModal = ({ isOpen, onClose }) => { 7 | const [showModal, setShowModal] = useState(isOpen) 8 | const [password, setPassword] = useState('') 9 | const [vault, setVault] = useState({}) 10 | const [isDecrypted, setIsDecrypted] = useState(false) 11 | 12 | useEffect(() => { 13 | setShowModal(isOpen) 14 | if (!isOpen) { 15 | setIsDecrypted(false) 16 | setVault({}) 17 | } 18 | }, [isOpen]) 19 | 20 | const closeModal = () => { 21 | setShowModal(false) 22 | onClose() 23 | } 24 | 25 | const decryptVault = async (e) => { 26 | e.preventDefault() 27 | 28 | const storage = await browser.storage.local.get(['encryptedVault']) 29 | const decryptedVault = decrypt(storage.encryptedVault, password) 30 | setIsDecrypted(true) 31 | setPassword('') 32 | setVault(decryptedVault) 33 | } 34 | 35 | return ( 36 |
37 | 38 | {!isDecrypted ? ( 39 |
40 | 41 |
42 | setPassword(e.target.value)} 50 | /> 51 |
52 | 53 |
54 | ) : ( 55 | <> 56 |

Secrets

57 |

Mnemonic: {vault.mnemonic}

58 | { vault.passphrase && (

Passphrase: {vault.passphrase}

) } 59 |

Account index: {vault.accountIndex}

60 | 61 | )} 62 |
63 |
64 | ) 65 | } 66 | 67 | export default SecretsModal 68 | -------------------------------------------------------------------------------- /src/options.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import { createRoot } from 'react-dom/client' 3 | import React, { useState, useEffect } from 'react' 4 | import { ToastContainer } from 'react-toastify' 5 | 6 | import ChangePassword from './components/ChangePassword' 7 | import ResetVault from './components/ResetVault' 8 | import Relays from './components/Relays' 9 | import ImportVault from './components/ImportVault' 10 | import ExportVault from './components/ExportVault' 11 | import Secrets from './components/Secrets' 12 | 13 | function Options() { 14 | const [isAuthenticated, setIsAuthenticated] = useState(false) 15 | const [isLocked, setIsLocked] = useState(false) 16 | 17 | useEffect(() => { 18 | fetchData() 19 | 20 | browser.storage.onChanged.addListener(function() { 21 | fetchData() 22 | }); 23 | }, []) 24 | 25 | const fetchData = async () => { 26 | const storage = await browser.storage.local.get(['isAuthenticated', 'isLocked']) 27 | 28 | setIsLocked(storage.isLocked) 29 | setIsAuthenticated(storage.isAuthenticated) 30 | } 31 | 32 | return ( 33 |
34 |
35 |

Options

36 | 37 | {!isLocked ? ( 38 | <> 39 | { isAuthenticated && ( 40 | <> 41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | )} 56 | 57 | { !isAuthenticated && ( 58 | <> 59 | 60 |
61 | 62 | )} 63 | 64 | { isAuthenticated && ( 65 | 66 | )} 67 | 68 | ) : ( 69 |

Vault is locked

70 | )} 71 |
72 | 73 | 74 |
75 | ) 76 | 77 | } 78 | 79 | const container = document.getElementById('main') 80 | const root = createRoot(container) // createRoot(container!) if you use TypeScript 81 | root.render() 82 | -------------------------------------------------------------------------------- /src/modals/DeriveAccountModal.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { privateKeyFromSeedWords } from 'nostr-tools/nip06' 4 | import { SimplePool } from 'nostr-tools/pool' 5 | import { finalizeEvent } from 'nostr-tools/pure' 6 | import { encrypt } from '../common' 7 | import Modal from './Modal' 8 | import MainContext from '../contexts/MainContext' 9 | 10 | const DeriveAccountModal = ({ isOpen, onClose, callBack }) => { 11 | const { updateAccounts } = useContext(MainContext) 12 | 13 | const [showModal, setShowModal] = useState(isOpen) 14 | const [name, setName] = useState('') 15 | 16 | useEffect(() => { 17 | setShowModal(isOpen) 18 | }, [isOpen]) 19 | 20 | const closeModal = () => { 21 | setShowModal(false) 22 | onClose() 23 | } 24 | 25 | const addAccount = async (e) => { 26 | e.preventDefault() 27 | 28 | const storage = await browser.storage.local.get(['vault', 'password', 'relays']) 29 | const vault = storage.vault 30 | 31 | vault.accountIndex++ 32 | const prvKey = privateKeyFromSeedWords(vault.mnemonic, vault.passphrase, vault.accountIndex) 33 | vault.accounts.push({ 34 | prvKey, 35 | }) 36 | vault.accountDefault = prvKey 37 | 38 | if (name || name !== '') { 39 | const pool = new SimplePool() 40 | const relays = storage.relays 41 | const event = { 42 | kind: 0, 43 | created_at: Math.floor(Date.now() / 1000), 44 | tags: [], 45 | content: JSON.stringify({ 46 | name: name, 47 | display_name: name, 48 | }), 49 | } 50 | 51 | const signedEvent = finalizeEvent(event, prvKey) 52 | await Promise.any(pool.publish(relays, signedEvent)) 53 | } 54 | 55 | const encryptedVault = encrypt(vault, storage.password) 56 | await browser.storage.local.set({ 57 | vault, 58 | encryptedVault, 59 | }) 60 | await updateAccounts() 61 | 62 | setName('') 63 | callBack() 64 | } 65 | 66 | return ( 67 |
68 | 69 |
70 | 71 |
72 | setName(e.target.value)} 79 | /> 80 |
81 | 82 |
83 |
84 |
85 | ) 86 | } 87 | 88 | export default DeriveAccountModal 89 | -------------------------------------------------------------------------------- /nostrame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 58 | N 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/components/ChangePassword.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState } from 'react' 3 | import { toast } from 'react-toastify' 4 | import { encrypt, decrypt } from '../common' 5 | 6 | const ChangePassword = ({ fetchData }) => { 7 | const [changePassword, setChangePassword] = useState({ 8 | currentPassword: '', 9 | newPassword: '', 10 | confirmNewPassword: '', 11 | }) 12 | 13 | const changePasswordInput = (e) => { 14 | const { name, value } = e.target; 15 | setChangePassword(prev => ({ 16 | ...prev, 17 | [name]: value 18 | })) 19 | } 20 | 21 | const submitChangePassword = async (e) => { 22 | e.preventDefault() 23 | 24 | const confirmChange = confirm("Are you sure you want to change the password?") 25 | if (!confirmChange) return 26 | 27 | if (changePassword.newPassword !== changePassword.confirmNewPassword) { 28 | alert('New password do not match!') 29 | 30 | setChangePassword({ 31 | currentPassword: '', 32 | newPassword: '', 33 | confirmNewPassword: '', 34 | }) 35 | 36 | return 37 | } 38 | 39 | const storage = await browser.storage.local.get(['encryptedVault']) 40 | try { 41 | const decryptedVault = decrypt(storage.encryptedVault, changePassword.currentPassword) 42 | const encryptedVault = encrypt(decryptedVault, changePassword.newPassword) 43 | await browser.storage.local.set({ 44 | encryptedVault, 45 | password: changePassword.currentPassword 46 | }) 47 | setChangePassword({ 48 | currentPassword: '', 49 | newPassword: '', 50 | confirmNewPassword: '', 51 | }) 52 | fetchData() 53 | toast.success("Your password was changed with success") 54 | } catch (e) { 55 | toast.error("Your password do not match", e) 56 | setChangePassword({ 57 | currentPassword: '', 58 | newPassword: '', 59 | confirmNewPassword: '', 60 | }) 61 | } 62 | } 63 | 64 | return ( 65 |
66 |

Change Password

67 | changePasswordInput(e)} 75 | /> 76 |
77 | changePasswordInput(e)} 85 | /> 86 |
87 | changePasswordInput(e)} 95 | /> 96 |
97 | 98 |
99 | ) 100 | } 101 | export default ChangePassword 102 | -------------------------------------------------------------------------------- /src/prompt.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | 5 | import {PERMISSION_NAMES} from './common' 6 | 7 | function Prompt() { 8 | let qs = new URLSearchParams(location.search) 9 | let id = qs.get('id') 10 | let host = qs.get('host') 11 | let type = qs.get('type') 12 | let params, event 13 | try { 14 | params = JSON.parse(qs.get('params')) 15 | if (Object.keys(params).length === 0) params = null 16 | else if (params.event) event = params.event 17 | } catch (err) { 18 | params = null 19 | } 20 | 21 | return ( 22 | <> 23 |
24 | 25 | {host} 26 | {' '} 27 |

28 | is requesting your permission to {PERMISSION_NAMES[type]}: 29 |

30 |
31 | {params && ( 32 | <> 33 |

now acting on

34 |
 35 |             {JSON.stringify(event || params, null, 2)}
 36 |           
37 | 38 | )} 39 |
46 | 55 | {event?.kind !== undefined && ( 56 | 65 | )} 66 | 69 | {event?.kind !== undefined ? ( 70 | 79 | ) : ( 80 | 89 | )} 90 | 93 |
94 | 95 | ) 96 | 97 | function authorizeHandler(accept, conditions) { 98 | return function (ev) { 99 | ev.preventDefault() 100 | browser.runtime.sendMessage({ 101 | prompt: true, 102 | id, 103 | host, 104 | type, 105 | accept, 106 | conditions 107 | }) 108 | } 109 | } 110 | } 111 | 112 | const container = document.getElementById('main') 113 | const root = createRoot(container) 114 | root.render() 115 | -------------------------------------------------------------------------------- /src/modals/AccountDetailsModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import {QRCodeSVG} from 'qrcode.react' 3 | import Modal from './Modal' 4 | 5 | const AccountDetailsModal = ({ isOpen, onClose, accountData }) => { 6 | const [showModal, setShowModal] = useState(isOpen) 7 | const [account, setAccount] = useState({}) 8 | const [format, setFormat] = useState('bech32') 9 | const [showSecret, setShowSecret] = useState(false) 10 | 11 | useEffect(() => { 12 | setShowModal(isOpen) 13 | }, [isOpen]) 14 | 15 | useEffect(() => { 16 | setAccount(accountData) 17 | }, [accountData]) 18 | 19 | const closeModal = () => { 20 | setShowModal(false) 21 | setShowSecret(false) 22 | onClose() 23 | } 24 | 25 | const copyToClipboard = (e, text) => { 26 | e.preventDefault() 27 | navigator.clipboard.writeText(text) 28 | } 29 | 30 | async function convertFormat(e) { 31 | e.preventDefault() 32 | setFormat(format === 'bech32' ? 'hex' : 'bech32') 33 | } 34 | 35 | return ( 36 |
37 | 38 | { !showSecret ? ( 39 | <> 40 |

{account.name}

41 |
42 | 47 |
48 |

49 | {format === 'bech32' ? 'Npub' : 'Public Key'}: 50 |   51 | convertFormat(e)} title={format === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 52 | 53 | 54 |
55 | {format === 'bech32' ? account.npub : account.pubKey} 56 |   57 | copyToClipboard(e, format === 'bech32' ? account.npub : account.pubKey)} title="Copy"> 58 | 59 | 60 |

61 | 62 | 63 | 64 | ) : ( 65 | <> 66 |

Private key

67 |
68 | 73 |
74 |

75 | {format === 'bech32' ? 'Nsec' : `Private Key`}: 76 |   77 | convertFormat(e)} title={format === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 78 | 79 | 80 |
81 | {format === 'bech32' ? account.nsec : account.prvKey} 82 |   83 | copyToClipboard(e, format === 'bech32' ? account.nsec : account.prvKey)} title="Copy"> 84 | 85 | 86 |

87 | 88 | 89 | 90 | )} 91 |
92 |
93 | ) 94 | } 95 | 96 | export default AccountDetailsModal 97 | -------------------------------------------------------------------------------- /src/modals/GenerateRandomAccountModal.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | import * as nip19 from 'nostr-tools/nip19' 4 | import { hexToBytes, bytesToHex } from '@noble/hashes/utils' 5 | import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' 6 | import { encrypt } from '../common' 7 | import Modal from './Modal' 8 | 9 | const GenerateRandomAccountModal = ({ isOpen, onClose, callBack }) => { 10 | const [showModal, setShowModal] = useState(isOpen) 11 | const [format, setFormat] = useState('bech32') 12 | const [account, setAccount] = useState({}) 13 | 14 | useEffect(() => { 15 | setShowModal(isOpen) 16 | 17 | if (isOpen) { 18 | generateRandomAccount() 19 | } 20 | }, [isOpen]) 21 | 22 | const generateRandomAccount = () => { 23 | const prvKey = bytesToHex(generateSecretKey()) 24 | const nsec = nip19.nsecEncode(hexToBytes(prvKey)) 25 | const pubKey = getPublicKey(prvKey) 26 | const npub = nip19.npubEncode(pubKey) 27 | 28 | setAccount({ 29 | prvKey, 30 | nsec, 31 | pubKey, 32 | npub 33 | }) 34 | } 35 | 36 | const importAccount = async () => { 37 | const storage = await browser.storage.local.get(['vault', 'password']) 38 | const vault = storage.vault 39 | vault.importedAccounts.push({ prvKey: account.prvKey }) 40 | const encryptedVault = encrypt(vault, storage.password) 41 | await browser.storage.local.set({ 42 | vault, 43 | encryptedVault, 44 | }) 45 | 46 | callBack() 47 | closeModal() 48 | } 49 | 50 | const closeModal = () => { 51 | setShowModal(false) 52 | onClose() 53 | } 54 | 55 | const convertFormat = (e) => { 56 | e.preventDefault() 57 | setFormat(format === 'bech32' ? 'hex' : 'bech32') 58 | } 59 | 60 | const copyToClipboard = (e, text) => { 61 | e.preventDefault() 62 | navigator.clipboard.writeText(text) 63 | } 64 | 65 | return ( 66 |
67 | 68 |

Generate Account

69 | 70 |

71 | {format === 'bech32' ? 'Nsec' : `Private Key`}: 72 |   73 | convertFormat(e)} title={format === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 74 | 75 | 76 |
77 | {format === 'bech32' ? account.nsec : account.prvKey} 78 |   79 | copyToClipboard(e, format === 'bech32' ? account.nsec : account.prvKey)} title="Copy"> 80 | 81 | 82 |

83 |

84 | {format === 'bech32' ? 'Npub' : 'Public Key'}: 85 |   86 | convertFormat(e)} title={format === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 87 | 88 | 89 |
90 | {format === 'bech32' ? account.npub : account.pubKey} 91 |   92 | copyToClipboard(e, format === 'bech32' ? account.npub : account.pubKey)} title="Copy"> 93 | 94 | 95 |

96 | 97 | 98 |
99 | 100 |
101 |
102 | ) 103 | } 104 | 105 | export default GenerateRandomAccountModal 106 | -------------------------------------------------------------------------------- /dist/nostr-provider.js: -------------------------------------------------------------------------------- 1 | window.nostr = { 2 | _requests: {}, 3 | _pubkey: null, 4 | 5 | async getPublicKey() { 6 | if (this._pubkey) return this._pubkey 7 | this._pubkey = await this._call('getPublicKey', {}) 8 | return this._pubkey 9 | }, 10 | 11 | async signEvent(event) { 12 | return this._call('signEvent', {event}) 13 | }, 14 | 15 | async getRelays() { 16 | return {} 17 | }, 18 | 19 | nip04: { 20 | async encrypt(peer, plaintext) { 21 | return window.nostr._call('nip04.encrypt', {peer, plaintext}) 22 | }, 23 | 24 | async decrypt(peer, ciphertext) { 25 | return window.nostr._call('nip04.decrypt', {peer, ciphertext}) 26 | } 27 | }, 28 | 29 | nip44: { 30 | async encrypt(peer, plaintext) { 31 | return window.nostr._call('nip44.encrypt', {peer, plaintext}) 32 | }, 33 | 34 | async decrypt(peer, ciphertext) { 35 | return window.nostr._call('nip44.decrypt', {peer, ciphertext}) 36 | } 37 | }, 38 | 39 | _call(type, params) { 40 | let id = Math.random().toString().slice(-4) 41 | console.log( 42 | '%c[Nostrame:%c' + 43 | id + 44 | '%c]%c calling %c' + 45 | type + 46 | '%c with %c' + 47 | JSON.stringify(params || {}), 48 | 'background-color:#f1b912;font-weight:bold;color:white', 49 | 'background-color:#f1b912;font-weight:bold;color:#a92727', 50 | 'background-color:#f1b912;color:white;font-weight:bold', 51 | 'color:auto', 52 | 'font-weight:bold;color:#08589d;font-family:monospace', 53 | 'color:auto', 54 | 'font-weight:bold;color:#90b12d;font-family:monospace' 55 | ) 56 | return new Promise((resolve, reject) => { 57 | this._requests[id] = {resolve, reject} 58 | window.postMessage( 59 | { 60 | id, 61 | ext: 'Nostrame', 62 | type, 63 | params 64 | }, 65 | '*' 66 | ) 67 | }) 68 | } 69 | } 70 | 71 | window.addEventListener('message', message => { 72 | if ( 73 | !message.data || 74 | message.data.response === null || 75 | message.data.response === undefined || 76 | message.data.ext !== 'Nostrame' || 77 | !window.nostr._requests[message.data.id] 78 | ) 79 | return 80 | 81 | if (message.data.response.error) { 82 | let error = new Error('Nostrame: ' + message.data.response.error.message) 83 | error.stack = message.data.response.error.stack 84 | window.nostr._requests[message.data.id].reject(error) 85 | } else { 86 | window.nostr._requests[message.data.id].resolve(message.data.response) 87 | } 88 | 89 | console.log( 90 | '%c[Nostrame:%c' + 91 | message.data.id + 92 | '%c]%c result: %c' + 93 | JSON.stringify( 94 | message?.data?.response || message?.data?.response?.error?.message || {} 95 | ), 96 | 'background-color:#f1b912;font-weight:bold;color:white', 97 | 'background-color:#f1b912;font-weight:bold;color:#a92727', 98 | 'background-color:#f1b912;color:white;font-weight:bold', 99 | 'color:auto', 100 | 'font-weight:bold;color:#08589d' 101 | ) 102 | 103 | delete window.nostr._requests[message.data.id] 104 | }) 105 | 106 | // hack to replace nostr:nprofile.../etc links with something else 107 | let replacing = null 108 | document.addEventListener('mousedown', replaceNostrSchemeLink) 109 | async function replaceNostrSchemeLink(e) { 110 | if (e.target.tagName !== 'A' || !e.target.href.startsWith('nostr:')) return 111 | if (replacing === false) return 112 | 113 | let response = await window.nostr._call('replaceURL', {url: e.target.href}) 114 | if (response === false) { 115 | replacing = false 116 | return 117 | } 118 | 119 | e.target.href = response 120 | } 121 | -------------------------------------------------------------------------------- /src/pages/SigninPage.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | import { privateKeyFromSeedWords, generateSeedWords } from 'nostr-tools/nip06' 4 | import { Link, Navigate, useNavigate } from 'react-router-dom' 5 | import { encrypt } from '../common' 6 | import { useAuth } from '../middlewares/AuthContext'; 7 | 8 | const Signin = () => { 9 | const { isAuthenticated } = useAuth() 10 | if (isAuthenticated) return 11 | 12 | const [password, setPassword] = useState('') 13 | const [vault, setVault] = useState({ 14 | mnemonic: '', 15 | passphrase: '', 16 | accountIndex: 0, 17 | accounts: [], // maybe change to derivedAccounts 18 | importedAccounts: [], 19 | }) 20 | 21 | const { login } = useAuth() 22 | const navigate = useNavigate() 23 | 24 | useEffect(() => { 25 | browser.storage.onChanged.addListener(async function(changes, area) { 26 | if (changes.isAuthenticated) { 27 | await login() 28 | navigate('/vault') 29 | } 30 | }) 31 | }, []) 32 | 33 | const handleVaultChange = (e) => { 34 | const { name, value } = e.target; 35 | setVault(prevVault => ({ 36 | ...prevVault, 37 | [name]: value 38 | })) 39 | } 40 | 41 | async function saveAccount(e) { 42 | e.preventDefault() 43 | 44 | const vaultData = { 45 | mnemonic: vault.mnemonic, 46 | passphrase: vault.passphrase, 47 | accountIndex: 0, 48 | accounts: [], 49 | importedAccounts: [], 50 | } 51 | 52 | const prvKey = privateKeyFromSeedWords(vault.mnemonic, vault.passphrase, vaultData.accountIndex) 53 | vaultData.accounts.push({ 54 | prvKey, 55 | }) 56 | 57 | const encryptedVault = encrypt(vaultData, password) 58 | await browser.storage.local.set({ 59 | vault: vaultData, 60 | encryptedVault, 61 | isAuthenticated: true, 62 | password 63 | }) 64 | 65 | await login() 66 | 67 | return navigate('/vault') 68 | } 69 | 70 | const openOptionsButton = async () => { 71 | if (browser.runtime.openOptionsPage) { 72 | browser.runtime.openOptionsPage() 73 | } else { 74 | window.open(browser.runtime.getURL('options.html')) 75 | } 76 | } 77 | 78 | return ( 79 | <> 80 |
81 |
82 |
83 |

Import Vault

84 |
85 | 86 |
87 | 95 |
96 | 104 |
105 | setPassword(e.target.value)} 113 | /> 114 |
115 | 116 |
117 | 118 | Back 119 |
120 |
121 |
122 | 123 | ) 124 | } 125 | export default Signin 126 | -------------------------------------------------------------------------------- /src/pages/SignupPage.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import { privateKeyFromSeedWords, generateSeedWords } from 'nostr-tools/nip06' 5 | import { Link, Navigate } from 'react-router-dom' 6 | import { encrypt } from '../common' 7 | import { useAuth } from '../middlewares/AuthContext'; 8 | import MainContext from '../contexts/MainContext' 9 | 10 | const Signup = () => { 11 | const { isAuthenticated } = useAuth() 12 | if (isAuthenticated) return 13 | 14 | const { updateAccounts } = useContext(MainContext) 15 | 16 | const [password, setPassword] = useState('') 17 | const [vault, setVault] = useState({ 18 | mnemonic: '', 19 | passphrase: '', 20 | accountIndex: 0, 21 | accounts: [], // maybe change to derivedAccounts 22 | importedAccounts: [], 23 | }) 24 | 25 | const { login } = useAuth() 26 | const navigate = useNavigate() 27 | 28 | useEffect(() => { 29 | handleGenerateSeedWords() 30 | }, []) 31 | 32 | const handleVaultChange = (e) => { 33 | const { name, value } = e.target; 34 | setVault(prevVault => ({ 35 | ...prevVault, 36 | [name]: value 37 | })) 38 | } 39 | 40 | async function saveAccount(e) { 41 | e.preventDefault() 42 | 43 | const vaultData = { 44 | mnemonic: vault.mnemonic, 45 | passphrase: vault.passphrase, 46 | accountIndex: 0, 47 | accounts: [], 48 | importedAccounts: [], 49 | } 50 | 51 | const prvKey = privateKeyFromSeedWords(vault.mnemonic, vault.passphrase, vaultData.accountIndex) 52 | vaultData.accounts.push({ 53 | prvKey, 54 | }) 55 | 56 | vaultData.accountDefault = prvKey 57 | 58 | const encryptedVault = encrypt(vaultData, password) 59 | await browser.storage.local.set({ 60 | vault: vaultData, 61 | encryptedVault, 62 | isAuthenticated: true, 63 | password 64 | }) 65 | 66 | await login() 67 | await updateAccounts() 68 | 69 | return navigate('/vault') 70 | } 71 | 72 | const handleGenerateSeedWords = () => { 73 | let mnemonic = generateSeedWords() 74 | setVault({ 75 | ...vault, 76 | mnemonic: mnemonic 77 | }) 78 | } 79 | 80 | return ( 81 | <> 82 |
83 |
84 |
85 |

Create new vault

86 | 95 |
96 | 97 |
98 | 106 |
107 | setPassword(e.target.value)} 115 | /> 116 |
117 | 118 |
119 | 120 | Back 121 |
122 |
123 |
124 | 125 | ) 126 | } 127 | export default Signup 128 | -------------------------------------------------------------------------------- /src/assets/css/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../onts/icomoon.eot?z8ca6h'); 4 | src: url('../fonts/icomoon.eot?z8ca6h#iefix') format('embedded-opentype'), 5 | url('../fonts/icomoon.ttf?z8ca6h') format('truetype'), 6 | url('../fonts/icomoon.woff?z8ca6h') format('woff'), 7 | url('../fonts/icomoon.svg?z8ca6h#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | font-display: block; 11 | } 12 | 13 | [class^="icon-"], [class*=" icon-"] { 14 | /* use !important to prevent issues with browser extensions that change fonts */ 15 | font-family: 'icomoon' !important; 16 | speak: never; 17 | font-style: normal; 18 | font-weight: normal; 19 | font-variant: normal; 20 | text-transform: none; 21 | line-height: 1; 22 | 23 | /* Better Font Rendering =========== */ 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | .icon-wallet:before { 29 | content: "\e901"; 30 | } 31 | .icon-money:before { 32 | content: "\e901"; 33 | } 34 | .icon-wallet1:before { 35 | content: "\e902"; 36 | } 37 | .icon-money1:before { 38 | content: "\e902"; 39 | } 40 | .icon-cash:before { 41 | content: "\e902"; 42 | } 43 | .icon-wallet2:before { 44 | content: "\e903"; 45 | } 46 | .icon-dots-three-vertical:before { 47 | content: "\e900"; 48 | } 49 | .icon-pencil:before { 50 | content: "\e905"; 51 | } 52 | .icon-file-zip:before { 53 | content: "\e92b"; 54 | } 55 | .icon-copy:before { 56 | content: "\e92c"; 57 | } 58 | .icon-folder-plus:before { 59 | content: "\e931"; 60 | } 61 | .icon-folder-download:before { 62 | content: "\e933"; 63 | } 64 | .icon-folder-upload:before { 65 | content: "\e934"; 66 | } 67 | .icon-qrcode:before { 68 | content: "\e938"; 69 | } 70 | .icon-hour-glass:before { 71 | content: "\e979"; 72 | } 73 | .icon-spinner3:before { 74 | content: "\e97c"; 75 | } 76 | .icon-key:before { 77 | content: "\e98d"; 78 | } 79 | .icon-lock:before { 80 | content: "\e98f"; 81 | } 82 | .icon-unlocked:before { 83 | content: "\e990"; 84 | } 85 | .icon-cog:before { 86 | content: "\e994"; 87 | } 88 | .icon-lab:before { 89 | content: "\e9aa"; 90 | } 91 | .icon-bin:before { 92 | content: "\e9ac"; 93 | } 94 | .icon-bin2:before { 95 | content: "\e9ad"; 96 | } 97 | .icon-switch:before { 98 | content: "\e9b6"; 99 | } 100 | .icon-list:before { 101 | content: "\e9ba"; 102 | } 103 | .icon-tree:before { 104 | content: "\e9bc"; 105 | } 106 | .icon-menu:before { 107 | content: "\e9bd"; 108 | } 109 | .icon-download:before { 110 | content: "\e9c5"; 111 | } 112 | .icon-upload:before { 113 | content: "\e9c6"; 114 | } 115 | .icon-download3:before { 116 | content: "\e9c7"; 117 | } 118 | .icon-sphere:before { 119 | content: "\e9c9"; 120 | } 121 | .icon-earth:before { 122 | content: "\e9ca"; 123 | } 124 | .icon-eye:before { 125 | content: "\e9ce"; 126 | } 127 | .icon-eye-blocked:before { 128 | content: "\e9d1"; 129 | } 130 | .icon-warning:before { 131 | content: "\ea07"; 132 | } 133 | .icon-notification:before { 134 | content: "\ea08"; 135 | } 136 | .icon-plus:before { 137 | content: "\ea0a"; 138 | } 139 | .icon-info:before { 140 | content: "\ea0c"; 141 | } 142 | .icon-cancel-circle:before { 143 | content: "\ea0d"; 144 | } 145 | .icon-cross:before { 146 | content: "\ea0f"; 147 | } 148 | .icon-enter:before { 149 | content: "\ea13"; 150 | } 151 | .icon-exit:before { 152 | content: "\ea14"; 153 | } 154 | .icon-loop2:before { 155 | content: "\ea2e"; 156 | } 157 | .icon-arrow-down:before { 158 | content: "\ea36"; 159 | } 160 | .icon-tab:before { 161 | content: "\ea45"; 162 | } 163 | .icon-move-down:before { 164 | content: "\ea47"; 165 | } 166 | .icon-svg:before { 167 | content: "\eae9"; 168 | } 169 | .icon-home:before { 170 | content: "\e904"; 171 | } 172 | .icon-home3:before { 173 | content: "\e906"; 174 | } 175 | .icon-user:before { 176 | content: "\e971"; 177 | } 178 | .icon-user-plus:before { 179 | content: "\e973"; 180 | } -------------------------------------------------------------------------------- /src/modals/ImportAccountModal.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { useNavigate } from 'react-router-dom'; 4 | import * as nip19 from 'nostr-tools/nip19' 5 | import { bytesToHex, hexToBytes } from '@noble/hashes/utils' 6 | import { encrypt } from '../common' 7 | import Modal from './Modal' 8 | import MainContext from '../contexts/MainContext' 9 | 10 | const ImportAccountModal = ({ isOpen, onClose, callBack }) => { 11 | const { updateAccounts } = useContext(MainContext) 12 | 13 | const navigate = useNavigate() 14 | 15 | const [showModal, setShowModal] = useState(isOpen) 16 | const [prvKey, setPrvKey] = useState('') 17 | 18 | useEffect(() => { 19 | setShowModal(isOpen) 20 | }, [isOpen]) 21 | 22 | const closeModal = () => { 23 | setShowModal(false) 24 | onClose() 25 | } 26 | 27 | const importAccount = async (e) => { 28 | e.preventDefault() 29 | 30 | const storage = await browser.storage.local.get(['vault', 'password']) 31 | const vault = storage.vault 32 | 33 | if (/^nsec/.test(prvKey)) { 34 | try { 35 | let {type, data} = nip19.decode(prvKey) 36 | 37 | if (type === 'nsec') { 38 | const prvKeyHex = bytesToHex(data) 39 | //if (!vault.importedAccounts) { vault.importedAccounts = [] } 40 | const prvKeyExist = vault.importedAccounts.find(obj => obj['prvKey'] === prvKeyHex) 41 | const prvKeyExistInDerived = vault.accounts.find(obj => obj['prvKey'] === prvKeyHex) 42 | if (prvKeyExist || prvKeyExistInDerived) { 43 | alert('Please provide a not existing private key') 44 | setPrvKey('') 45 | return false 46 | } 47 | vault.importedAccounts.push({ prvKey: prvKeyHex }) 48 | vault.accountDefault = prvKeyHex 49 | const encryptedVault = encrypt(vault, storage.password) 50 | await browser.storage.local.set({ 51 | vault, 52 | encryptedVault, 53 | }) 54 | await updateAccounts() 55 | 56 | callBack() 57 | 58 | navigate('/vault') 59 | } 60 | } catch (e) { 61 | console.log(e) 62 | alert('Please provide a valid private key') 63 | setPrvKey('') 64 | } 65 | } else if (/^[0-9a-fA-F]+$/.test(prvKey)) { 66 | try { 67 | let prvKeyBytes = hexToBytes(prvKey) 68 | let prvKeyHex = bytesToHex(prvKeyBytes) 69 | 70 | const prvKeyExist = vault.importedAccounts.find(obj => obj['prvKey'] === prvKeyHex) 71 | const prvKeyExistInDerived = vault.accounts.find(obj => obj['prvKey'] === prvKeyHex) 72 | if (prvKeyExist || prvKeyExistInDerived) { 73 | alert('Please provide a not existing private key') 74 | setPrvKey('') 75 | return false 76 | } 77 | 78 | vault.importedAccounts.push({ prvKey: prvKeyHex }) 79 | vault.accountDefault = prvKeyHex 80 | const encryptedVault = encrypt(vault, storage.password) 81 | await browser.storage.local.set({ 82 | vault, 83 | encryptedVault, 84 | }) 85 | await updateAccounts() 86 | 87 | callBack() 88 | 89 | navigate('/vault') 90 | } catch (e) { 91 | console.log(e) 92 | alert('Please provide a valid private key') 93 | setPrvKey('') 94 | } 95 | } 96 | 97 | setPrvKey('') 98 | } 99 | 100 | return ( 101 |
102 | 103 |
104 | 105 |
106 | setPrvKey(e.target.value)} 114 | /> 115 |
116 | 117 |
118 |
119 |
120 | ) 121 | } 122 | 123 | export default ImportAccountModal 124 | -------------------------------------------------------------------------------- /src/modals/EditAccountModal.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect } from 'react' 3 | import { SimplePool } from 'nostr-tools/pool' 4 | import { finalizeEvent } from 'nostr-tools/pure' 5 | import Modal from './Modal' 6 | 7 | const EditAccountModal = ({ isOpen, onClose, accountData, callBack }) => { 8 | const pool = new SimplePool() 9 | 10 | const [showModal, setShowModal] = useState(isOpen); 11 | const [account, setAccount] = useState({ 12 | name: '', 13 | about: '', 14 | picture: '', 15 | banner: '', 16 | nip05: '', 17 | lud16: '', 18 | }); 19 | 20 | useEffect(() => { 21 | //setAccount(accountData) 22 | setAccount({ 23 | name: accountData.name || '', 24 | about: accountData.about || '', 25 | picture: accountData.picture || '', 26 | banner: accountData.banner || '', 27 | nip05: accountData.nip05 || '', 28 | lud16: accountData.lud16 || '', 29 | }) 30 | }, [accountData]) 31 | 32 | useEffect(() => { 33 | setShowModal(isOpen) 34 | }, [isOpen]) 35 | 36 | const closeModal = () => { 37 | setShowModal(false); 38 | //onClose() 39 | } 40 | 41 | const accountChange = (e) => { 42 | const { name, value } = e.target; 43 | setAccount(prevAccount => ({ 44 | ...prevAccount, 45 | [name]: value 46 | })) 47 | } 48 | 49 | const saveAccount = async (e) => { 50 | e.preventDefault() 51 | const storage = await browser.storage.local.get(['relays']) 52 | 53 | let relays = storage.relays 54 | 55 | try { 56 | let event = { 57 | kind: 0, 58 | created_at: Math.floor(Date.now() / 1000), 59 | tags: [], 60 | content: JSON.stringify({ 61 | name: account.name, 62 | display_name: account.name, 63 | about: account.about, 64 | picture: account.picture, 65 | banner: account.banner, 66 | nip05: account.nip05, 67 | lud16: account.lud16, 68 | }), 69 | } 70 | 71 | const signedEvent = finalizeEvent(event, accountData.prvKey) 72 | await Promise.any(pool.publish(relays, signedEvent)) 73 | 74 | callBack() 75 | } catch (error) { 76 | console.log(error) 77 | } 78 | } 79 | 80 | return ( 81 |
82 | 83 |

Edit Account

84 |
85 | 93 |
94 | 102 |
103 | 111 |
112 | 120 |
121 | 129 |
130 | 138 |
139 | 140 |
141 |
142 |
143 | ) 144 | } 145 | 146 | export default EditAccountModal 147 | -------------------------------------------------------------------------------- /src/pages/GeneratorPage.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import * as nip19 from 'nostr-tools/nip19' 5 | import { privateKeyFromSeedWords, generateSeedWords } from 'nostr-tools/nip06' 6 | import { hexToBytes } from '@noble/hashes/utils' 7 | import { getPublicKey } from 'nostr-tools/pure' 8 | import { encrypt } from '../common' 9 | import MainContext from '../contexts/MainContext' 10 | 11 | const GeneratorPage = () => { 12 | const { updateAccounts } = useContext(MainContext) 13 | 14 | const navigate = useNavigate() 15 | 16 | const [format, setFormat] = useState('bech32') 17 | const [account, setAccount] = useState({}) 18 | const [loaded, setLoaded] = useState(false) 19 | 20 | useEffect(() => { 21 | fetchData() 22 | }, []) 23 | 24 | const fetchData = async () => { 25 | generateRandomAccount() 26 | 27 | setLoaded(true) 28 | } 29 | 30 | const generateRandomAccount = () => { 31 | const mnemonic = generateSeedWords() 32 | const prvKey = privateKeyFromSeedWords(mnemonic) 33 | const nsec = nip19.nsecEncode(hexToBytes(prvKey)) 34 | const pubKey = getPublicKey(prvKey) 35 | const npub = nip19.npubEncode(pubKey) 36 | 37 | setAccount({ 38 | mnemonic, 39 | prvKey, 40 | nsec, 41 | pubKey, 42 | npub 43 | }) 44 | } 45 | 46 | const importAccount = async () => { 47 | const storage = await browser.storage.local.get(['vault', 'password']) 48 | const vault = storage.vault 49 | vault.importedAccounts.push({ prvKey: account.prvKey }) 50 | vault.accountDefault = account.prvKey 51 | const encryptedVault = encrypt(vault, storage.password) 52 | await browser.storage.local.set({ 53 | vault, 54 | encryptedVault, 55 | }) 56 | await updateAccounts() 57 | 58 | navigate('/vault') 59 | } 60 | 61 | const convertFormat = (e) => { 62 | e.preventDefault() 63 | setFormat(format === 'bech32' ? 'hex' : 'bech32') 64 | } 65 | 66 | const copyToClipboard = (e, text) => { 67 | e.preventDefault() 68 | navigator.clipboard.writeText(text) 69 | } 70 | 71 | return ( 72 |
73 |
74 | <> 75 | {loaded ? ( 76 | <> 77 |

Generate Account

78 | 79 |

80 | Mnemonic: 81 |   82 |
83 | {account.mnemonic} 84 |   85 | copyToClipboard(e, account.mnemonic)} title="Copy"> 86 | 87 | 88 |

89 |

90 | {format === 'bech32' ? 'Nsec' : `Private Key`}: 91 |   92 | convertFormat(e)} title={format === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 93 | 94 | 95 |
96 | {format === 'bech32' ? account.nsec : account.prvKey} 97 |   98 | copyToClipboard(e, format === 'bech32' ? account.nsec : account.prvKey)} title="Copy"> 99 | 100 | 101 |

102 |

103 | {format === 'bech32' ? 'Npub' : 'Public Key'}: 104 |   105 | convertFormat(e)} title={format === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 106 | 107 | 108 |
109 | {format === 'bech32' ? account.npub : account.pubKey} 110 |   111 | copyToClipboard(e, format === 'bech32' ? account.npub : account.pubKey)} title="Copy"> 112 | 113 | 114 |

115 | 116 | 117 |
118 | 119 | 120 | ) : ( 121 | <> 122 |
123 | Loading... 124 |
125 | 126 | )} 127 | 128 |
129 |
130 | ) 131 | } 132 | 133 | export default GeneratorPage -------------------------------------------------------------------------------- /src/contexts/MainContext.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { createContext, useState, useEffect } from 'react' 3 | import * as nip19 from 'nostr-tools/nip19' 4 | import { hexToBytes } from '@noble/hashes/utils' 5 | import { getPublicKey } from 'nostr-tools/pure' 6 | import getIdenticon from '../helpers/identicon' 7 | import { SimplePool } from 'nostr-tools/pool' 8 | 9 | const MainContext = createContext() 10 | 11 | export const MainProvider = ({ children }) => { 12 | const [accounts, setAccounts] = useState([]) 13 | const [defaultAccount, setDefaultAccount] = useState({ index: '', name: '', type: '' }) 14 | 15 | const pool = new SimplePool() 16 | 17 | useEffect(() => { 18 | fetchData() 19 | 20 | browser.storage.onChanged.addListener(async (changes, area) => { 21 | if (changes.vault) { 22 | let { newValue, oldValue } = changes.vault 23 | if (newValue.accountDefault !== oldValue.accountDefault) { 24 | //fetchData() 25 | } 26 | } 27 | }) 28 | }, []) 29 | 30 | useEffect(() => { 31 | browser.storage.onChanged.addListener(async (changes, area) => { 32 | if (changes.vault) { 33 | let { newValue, oldValue } = changes.vault 34 | if (newValue.accountDefault !== oldValue.accountDefault) { 35 | //const storage = await browser.storage.local.get() 36 | //const defaultAccount = accounts.find(key => key.prvKey === storage.vault.accountDefault) 37 | //setDefaultAccount(defaultAccount) 38 | //fetchData() 39 | } 40 | } 41 | }) 42 | }, [accounts]) 43 | 44 | const fetchData = async () => { 45 | const storage = await browser.storage.local.get() 46 | 47 | let loadAccounts = [] 48 | let authors = [] 49 | 50 | if (storage.isAuthenticated && !storage.isLocked) { 51 | let l = storage.vault.accounts.length 52 | for (let i = 0; i < l; i++) { 53 | const prvKey = storage.vault.accounts[i].prvKey 54 | const nsec = nip19.nsecEncode(hexToBytes(prvKey)) 55 | const pubKey = getPublicKey(prvKey) 56 | const npub = nip19.npubEncode(pubKey) 57 | authors.push(pubKey) 58 | loadAccounts = [ 59 | ...loadAccounts, 60 | { 61 | index: i, 62 | name: '', 63 | prvKey, 64 | nsec, 65 | pubKey, 66 | npub, 67 | picture: await UserIdenticon(pubKey), 68 | format: 'bech32', 69 | type: 'derived', 70 | } 71 | ] 72 | } 73 | 74 | let len = storage.vault.importedAccounts.length 75 | for (let i = 0; i < len; i++) { 76 | const prvKey = storage.vault.importedAccounts[i].prvKey 77 | const nsec = nip19.nsecEncode(hexToBytes(prvKey)) 78 | const pubKey = getPublicKey(prvKey) 79 | const npub = nip19.npubEncode(pubKey) 80 | authors.push(pubKey) 81 | loadAccounts = [ 82 | ...loadAccounts, 83 | { 84 | index: i, 85 | name: '', 86 | prvKey, 87 | nsec, 88 | pubKey, 89 | npub, 90 | picture: await UserIdenticon(pubKey), 91 | format: 'bech32', 92 | type: 'imported', 93 | } 94 | ] 95 | } 96 | 97 | let relays = storage.relays 98 | let events = await pool.querySync(relays, { kinds: [0], authors }) 99 | 100 | events.forEach(async (item) => { 101 | let content = JSON.parse(item.content) 102 | let len = loadAccounts.length 103 | for (let i = 0; i < len; i++) { 104 | if (loadAccounts[i].pubKey === item.pubkey) { 105 | loadAccounts[i].name = content.display_name 106 | loadAccounts[i].about = content.about 107 | loadAccounts[i].picture = !content.picture || content.picture === '' ? loadAccounts[i].picture : content.picture 108 | loadAccounts[i].banner = !content.banner || content.banner === '' ? '' : content.banner 109 | loadAccounts[i].nip05 = content.nip05 110 | loadAccounts[i].lud16 = content.lud16 111 | } 112 | } 113 | }) 114 | 115 | if (!storage.vault.accountDefault) { 116 | storage.vault.accountDefault = storage.vault.accounts[0].prvKey 117 | } 118 | const defaultAccount = loadAccounts.find(key => key.prvKey === storage.vault.accountDefault) 119 | setDefaultAccount(defaultAccount) 120 | 121 | setAccounts(loadAccounts) 122 | } 123 | } 124 | 125 | const UserIdenticon = async ( pubkey ) => { 126 | const identicon = await getIdenticon(pubkey) 127 | 128 | return `data:image/svg+xml;base64,${identicon}` 129 | } 130 | 131 | const updateAccounts = async () => { 132 | await fetchData() 133 | } 134 | 135 | const updateDefaultAccount = async () => { 136 | const storage = await browser.storage.local.get(['vault']) 137 | const defaultAccount = accounts.find(key => key.prvKey === storage.vault.accountDefault) 138 | setDefaultAccount(defaultAccount) 139 | } 140 | 141 | return ( 142 | 143 | {children} 144 | 145 | ) 146 | } 147 | 148 | export default MainContext 149 | -------------------------------------------------------------------------------- /src/assets/css/style.build.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["file:///home/ghost/projects/nostr-password-manager/extension/assets/css/style.scss","file:///home/ghost/projects/nostr-password-manager/extension/assets/css/_fonts.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_variables.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_toastContainer.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_toast.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_theme.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_closeButton.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_progressBar.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_icons.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_bounce.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_zoom.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_flip.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_slide.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_spin.scss"],"names":[],"mappings":"CAAA,4EAaA,qBACE,sBAGF,KACE,iBACA,8BAUF,KACE,SAOF,KACE,cAQF,GACE,cACA,eAWF,GACE,uBACA,SACA,iBAQF,IACE,gCACA,cAUF,EACE,+BAQF,YACE,mBACA,0BACA,iCAOF,SAEE,mBAQF,cAGE,gCACA,cAOF,MACE,cAQF,QAEE,cACA,cACA,kBACA,wBAGF,IACE,eAGF,IACE,WAUF,IACE,kBAWF,sCAKE,oBACA,eACA,iBACA,SAQF,aAEE,iBAQF,cAEE,oBAOF,gDAIE,0BAOF,wHAIE,kBACA,UAOF,4GAIE,8BAOF,SACE,2BAUF,OACE,sBACA,cACA,cACA,eACA,UACA,mBAOF,SACE,wBAOF,SACE,cAQF,6BAEE,sBACA,UAOF,kFAEE,YAQF,cACE,6BACA,oBAOF,yCACE,wBAQF,6BACE,0BACA,aAUF,QACE,cAOF,QACE,kBAUF,SACE,aAOF,SACE,aAGF,OACE,yBACA,WACA,iBACA,YACA,kBACA,eACA,kBACA,qBACA,qBACA,qCAGF,aACE,yBAIF,EACE,cACA,qBACA,0BAGF,QACE,cC9XF,WACE,sBACA,uCACA,2OAIA,mBACA,kBACA,mBAGF,iCAEE,iCACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAGF,iCACE,YAEF,oBACE,YAEF,sBACE,YAEF,kBACE,YAEF,yBACE,YAEF,6BACE,YAEF,2BACE,YAEF,oBACE,YAEF,wBACE,YAEF,sBACE,YAEF,iBACE,YAEF,kBACE,YAEF,sBACE,YAEF,iBACE,YAEF,iBACE,YAEF,iBACE,YAEF,kBACE,YAEF,oBACE,YAEF,kBACE,YAEF,kBACE,YAEF,kBACE,YAEF,sBACE,YAEF,oBACE,YAEF,uBACE,YAEF,oBACE,YAEF,mBACE,YAEF,iBACE,YAEF,yBACE,YAEF,qBACE,YAEF,0BACE,YAEF,kBACE,YAEF,kBACE,YAEF,2BACE,YAEF,mBACE,YAEF,mBACE,YAEF,kBACE,YAEF,mBACE,YAEF,wBACE,YAEF,iBACE,YAEF,uBACE,YAEF,iBACE,YCjJF,MACE,6BACA,+BACA,+BACA,kCACA,kCACA,gCACA,uDAEA,uDACA,6DACA,6DACA,yDAEA,8BACA,8BACA,kFACA,sFACA,oFACA,wFACA,kCACA,kCACA,mCACA,gCACA,mCACA,yBAEA,qCACA,iCAGA,iCACA,oCACA,oCACA,kCAEA,kCACA,6CAGA,mHAUA,wCACA,2DACA,iEACA,iEACA,6DACA,mCC1DF,2BACE,gCACA,6DACA,eACA,YACA,kCACA,sBACA,WACA,qCACE,8BACA,gCAEF,uCACE,8BACA,SACA,2BAEF,sCACE,8BACA,kCAEF,wCACE,oCACA,gCAEF,0CACE,oCACA,SACA,2BAEF,yCACE,oCACA,kCAIJ,2CACE,2BACE,YACA,UACA,+BACA,SACA,kHAGE,6BACA,wBAEF,2HAGE,mCACA,wBAEF,gCACE,iCACA,cCxDN,iBACE,OACA,kBACA,kBACA,4CACA,sBACA,mBACA,YACA,8CACA,uCACA,aACA,8BACA,4CACA,wCACA,eACA,cAEA,UACA,gBAEA,0BACE,kBACA,WACA,sDACA,yBAEA,kIACE,uBAGF,gDACE,iBAGF,kEACE,UAGF,gCACE,WACA,kBACA,OACA,QACA,0BACA,YAGF,wCACE,MAGF,wCACE,SAGF,wEACE,qBAGF,wEACE,wBAGF,iCACE,WACA,kBACA,OACA,QACA,SACA,YACA,oBACA,WAIJ,sBACE,cAEF,iCACE,eAEF,sBACE,cACA,cACA,YACA,aACA,mBACA,qCACE,sBACA,OAGJ,sBACE,uBACA,WACA,cACA,aAIJ,mBACE,yBACA,uBAGF,wBACE,yBACA,uBAGF,2CACE,iBACE,gBACA,iBChHF,6BACE,sCACA,sCAEF,8BACE,uCACA,uCAEF,yDACE,uCACA,uCAEF,sDACE,sCACA,sCAEF,yDACE,yCACA,yCAEF,yDACE,yCACA,yCAEF,uDACE,uCACA,uCAKF,qCACE,gDAEF,oCACE,+CAEF,8BACE,+CAEF,iCACE,kDAEF,iCACE,kDAEF,+BACE,gDAEF,uRAIE,6CCtDJ,wBACE,WACA,yBACA,aACA,YACA,UACA,eACA,WACA,oBACA,sBACA,UACA,+BACE,WACA,WAGF,4BACE,kBACA,YACA,WAGF,4DAEE,UCxBJ,mCACE,GACE,oBAEF,KACE,qBAIJ,wBACE,kBACA,SACA,OACA,WACA,YACA,gCACA,WACA,sBACA,0DAEA,kCACE,oDAGF,oCACE,yBAGF,6BACE,QACA,aACA,uBACA,kCACA,2DAGF,6BACE,kBACA,SACA,OACA,WACA,WACA,0DAGF,+CACE,UAGF,4BACE,2CACA,WACA,YCpDJ,mBACE,WACA,YACA,sBACA,iBACA,mBACA,sDACA,iDACA,8CCJF,mCACE,oBAJA,8DAWA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gBAIJ,oCACE,IACE,UACA,0CAEF,GACE,UACA,4CAIJ,kCACE,oBA1CA,8DAiDA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,UACA,yCAEF,GACE,UACA,6CAIJ,gCACE,oBAhFA,8DAuFA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gCAIJ,iCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,sCAIJ,kCACE,oBA1HA,8DAiIA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,qCAKF,uEAEE,sCAEF,yEAEE,uCAEF,oCACE,sCAEF,uCACE,oCAKF,qEAEE,uCAEF,uEAEE,wCAEF,mCACE,qCAEF,sCACE,uCClMJ,4BACE,KACE,UACA,iCAEF,IACE,WAIJ,6BACE,KACE,UAEF,IACE,UACA,6DAEF,GACE,WAIJ,sBACE,gCAGF,qBACE,iCC5BF,4BACE,KACE,sDACA,kCACA,UAEF,IACE,uDACA,kCAEF,IACE,sDACA,UAEF,IACE,sDAEF,GACE,8BAIJ,6BACE,KACE,yDAEF,IACE,mFACA,UAEF,GACE,kFACA,WAIJ,sBACE,gCAGF,qBACE,iCCrCF,kCACE,KACE,kCACA,mBAEF,GARA,uCAaF,iCACE,KACE,mCACA,mBAEF,GAlBA,uCAuBF,+BACE,KACE,kCACA,mBAEF,GA5BA,uCAiCF,iCACE,KACE,mCACA,mBAEF,GAtCA,uCA2CF,mCACE,KA5CA,sCA+CA,GACE,kBACA,0CAIJ,kCACE,KAtDA,sCAyDA,GACE,kBACA,2CAIJ,kCACE,KAhEA,sCAmEA,GACE,kBACA,oCAIJ,gCACE,KA1EA,sCA6EA,GACE,kBACA,qCAUF,qEAEE,qCAEF,uEAEE,sCAEF,mCACE,qCAEF,sCACE,mCAKF,mEAEE,sCAxBF,kCACA,uBA0BA,qEAEE,uCA7BF,kCACA,uBA+BA,kCACE,oCAjCF,kCACA,uBAmCA,qCACE,sCArCF,kCACA,uBCtFF,0BACE,KACE,uBAEF,GACE,0BbgYJ,KACE,sBACA,WACA,6BACA,eAGF,QACE,sBACA,WACA,kBACA,aACA,8BACA,mBAEF,WACE,cACA,SAMF,kBACE,cAGF,oBACE,cACA,YAIF,MACE,gBACA,6BACA,aAGF,kBACE,0BAGF,WACE,aACA,8BACA,mBACA,kBAGF,cACE,qBACA,qBAGF,sCAEE,cACA,WAGF,SACE,cACA,WAGF,iBACE,cACA,WAGF,MACE,cAGF,KACE,cACA,WAGF,eACE,eACA,MACA,OACA,WACA,YACA,gCACA,aACA,uBACA,mBACA,UAGF,OACE,sBACA,WACA,aACA,kBACA,cACA,gBACA,WAGF,eACE,kBAGF,OACE,kBACA,MACA,QACA,eAIF,UACE,aACA,kBACA,qBAGF,cACE,qBACA,iBACA,WAIF,kBACE,aACA,kBACA,QACA,yBACA,gBACA,uCACA,UAIF,oBACE,WACA,kBACA,qBACA,cAIF,0BACE,sBAIF,kCACE","file":"style.build.css"} -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import CryptoJS from 'crypto-js' 3 | 4 | function deriveKey(password, salt) { 5 | const iterations = 10000 6 | const keyLength = 256 7 | return CryptoJS.PBKDF2(password, salt, { keySize: keyLength / 32, iterations: iterations }) 8 | } 9 | 10 | export function encrypt(data, password) { 11 | const salt = CryptoJS.lib.WordArray.random(128 / 8) 12 | const derivedKey = deriveKey(password, salt) 13 | const iv = CryptoJS.lib.WordArray.random(128 / 8) 14 | const encrypted = CryptoJS.AES.encrypt( 15 | JSON.stringify(data), 16 | derivedKey, 17 | { iv: iv } 18 | ) 19 | 20 | // Convert the salt, IV, and encrypted data to a single string 21 | return salt.toString() + iv.toString() + encrypted.toString() 22 | } 23 | 24 | export function decrypt(encryptedData, password) { 25 | const salt = CryptoJS.enc.Hex.parse(encryptedData.substring(0, 32)) 26 | const iv = CryptoJS.enc.Hex.parse(encryptedData.substring(32, 64)) 27 | const encrypted = encryptedData.substring(64) 28 | const derivedKey = deriveKey(password, salt) 29 | const decrypted = CryptoJS.AES.decrypt(encrypted, derivedKey, { iv: iv }) 30 | 31 | return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8)) 32 | } 33 | 34 | export const NO_PERMISSIONS_REQUIRED = { 35 | replaceURL: true 36 | } 37 | 38 | export const PERMISSION_NAMES = Object.fromEntries([ 39 | ['getPublicKey', 'read your public key'], 40 | ['signEvent', 'sign events using your private key'], 41 | ['nip04.encrypt', 'encrypt messages to peers'], 42 | ['nip04.decrypt', 'decrypt messages from peers'], 43 | ['nip44.encrypt', 'encrypt messages to peers'], 44 | ['nip44.decrypt', 'decrypt messages from peers'] 45 | ]) 46 | 47 | function matchConditions(conditions, event) { 48 | if (conditions?.kinds) { 49 | if (event.kind in conditions.kinds) return true 50 | else return false 51 | } 52 | 53 | return true 54 | } 55 | 56 | export async function getPermissionStatus(host, type, event) { 57 | let {policies} = await browser.storage.local.get('policies') 58 | 59 | let answers = [true, false] 60 | for (let i = 0; i < answers.length; i++) { 61 | let accept = answers[i] 62 | let {conditions} = policies?.[host]?.[accept]?.[type] || {} 63 | 64 | if (conditions) { 65 | if (type === 'signEvent') { 66 | if (matchConditions(conditions, event)) { 67 | return accept // may be true or false 68 | } else { 69 | // if this doesn't match we just continue so it will either match for the opposite answer (reject) 70 | // or it will end up returning undefined at the end 71 | continue 72 | } 73 | } else { 74 | return accept // may be true or false 75 | } 76 | } 77 | } 78 | 79 | return undefined 80 | } 81 | 82 | export async function updatePermission(host, type, accept, conditions) { 83 | let {policies = {}} = await browser.storage.local.get('policies') 84 | 85 | // if the new conditions is "match everything", override the previous 86 | if (Object.keys(conditions).length === 0) { 87 | conditions = {} 88 | } else { 89 | // if we already had a policy for this, merge the conditions 90 | let existingConditions = policies[host]?.[accept]?.[type]?.conditions 91 | if (existingConditions) { 92 | if (existingConditions.kinds && conditions.kinds) { 93 | Object.keys(existingConditions.kinds).forEach(kind => { 94 | conditions.kinds[kind] = true 95 | }) 96 | } 97 | } 98 | } 99 | 100 | // if we have a reverse policy (accept / reject) that is exactly equal to this, remove it 101 | let other = !accept 102 | let reverse = policies?.[host]?.[other]?.[type] 103 | if ( 104 | reverse && 105 | JSON.stringify(reverse.conditions) === JSON.stringify(conditions) 106 | ) { 107 | delete policies[host][other][type] 108 | } 109 | 110 | // insert our new policy 111 | policies[host] = policies[host] || {} 112 | policies[host][accept] = policies[host][accept] || {} 113 | policies[host][accept][type] = { 114 | conditions, // filter that must match the event (in case of signEvent) 115 | created_at: Math.round(Date.now() / 1000) 116 | } 117 | 118 | browser.storage.local.set({policies}) 119 | } 120 | 121 | export async function removePermissions(host, accept, type) { 122 | let {policies = {}} = await browser.storage.local.get('policies') 123 | delete policies[host]?.[accept]?.[type] 124 | browser.storage.local.set({policies}) 125 | } 126 | 127 | export async function showNotification(host, answer, type, params) { 128 | let {notifications} = await browser.storage.local.get('notifications') 129 | if (notifications) { 130 | let action = answer ? 'allowed' : 'denied' 131 | browser.notifications.create(undefined, { 132 | type: 'basic', 133 | title: `${type} ${action} for ${host}`, 134 | message: JSON.stringify( 135 | params?.event 136 | ? { 137 | kind: params.event.kind, 138 | content: params.event.content, 139 | tags: params.event.tags 140 | } 141 | : params, 142 | null, 143 | 2 144 | ), 145 | iconUrl: 'icons/48x48.png' 146 | }) 147 | } 148 | } 149 | 150 | export async function getPosition(width, height) { 151 | let left = 0 152 | let top = 0 153 | 154 | try { 155 | const lastFocused = await browser.windows.getLastFocused() 156 | 157 | if ( 158 | lastFocused && 159 | lastFocused.top !== undefined && 160 | lastFocused.left !== undefined && 161 | lastFocused.width !== undefined && 162 | lastFocused.height !== undefined 163 | ) { 164 | // Position window in the center of the lastFocused window 165 | top = Math.round(lastFocused.top + (lastFocused.height - height) / 2) 166 | left = Math.round(lastFocused.left + (lastFocused.width - width) / 2) 167 | } else { 168 | console.error('Last focused window properties are undefined.') 169 | } 170 | } catch (error) { 171 | console.error('Error getting window position:', error) 172 | } 173 | 174 | return { 175 | top, 176 | left 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /dist/assets/css/style.build.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["file:///home/pater/projects/nostrame/src/assets/css/style.scss","file:///home/pater/projects/nostrame/src/assets/css/_fonts.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_variables.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_toastContainer.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_toast.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_theme.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_closeButton.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_progressBar.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_icons.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_bounce.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_zoom.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_flip.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_slide.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_spin.scss"],"names":[],"mappings":"CAAA,4EAaA,qBACE,sBAGF,KACE,iBACA,8BAUF,KACE,SAOF,KACE,cAQF,GACE,cACA,eAWF,GACE,uBACA,SACA,iBAQF,IACE,gCACA,cAUF,EACE,+BAQF,YACE,mBACA,0BACA,iCAOF,SAEE,mBAQF,cAGE,gCACA,cAOF,MACE,cAQF,QAEE,cACA,cACA,kBACA,wBAGF,IACE,eAGF,IACE,WAUF,IACE,kBAWF,sCAKE,oBACA,eACA,iBACA,SAQF,aAEE,iBAQF,cAEE,oBAOF,gDAIE,0BAOF,wHAIE,kBACA,UAOF,4GAIE,8BAOF,SACE,2BAUF,OACE,sBACA,cACA,cACA,eACA,UACA,mBAOF,SACE,wBAOF,SACE,cAQF,6BAEE,sBACA,UAOF,kFAEE,YAQF,cACE,6BACA,oBAOF,yCACE,wBAQF,6BACE,0BACA,aAUF,QACE,cAOF,QACE,kBAUF,SACE,aAOF,SACE,aAGF,OACE,yBACA,WACA,iBACA,YACA,kBACA,eACA,kBACA,qBACA,qBACA,qCAGF,aACE,yBAIF,EACE,cACA,qBACA,0BAGF,QACE,cC9XF,WACE,sBACA,sCACA,2OAIA,mBACA,kBACA,mBAGF,iCAEE,iCACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAGF,oBACE,YAEF,mBACE,YAEF,qBACE,YAEF,oBACE,YAEF,kBACE,YAEF,qBACE,YAEF,iCACE,YAEF,oBACE,YAEF,sBACE,YAEF,kBACE,YAEF,yBACE,YAEF,6BACE,YAEF,2BACE,YAEF,oBACE,YAEF,wBACE,YAEF,sBACE,YAEF,iBACE,YAEF,kBACE,YAEF,sBACE,YAEF,iBACE,YAEF,iBACE,YAEF,iBACE,YAEF,kBACE,YAEF,oBACE,YAEF,kBACE,YAEF,kBACE,YAEF,kBACE,YAEF,sBACE,YAEF,oBACE,YAEF,uBACE,YAEF,oBACE,YAEF,mBACE,YAEF,iBACE,YAEF,yBACE,YAEF,qBACE,YAEF,0BACE,YAEF,kBACE,YAEF,kBACE,YAEF,2BACE,YAEF,mBACE,YAEF,mBACE,YAEF,kBACE,YAEF,mBACE,YAEF,wBACE,YAEF,iBACE,YAEF,uBACE,YAEF,iBACE,YAEF,kBACE,YAEF,mBACE,YAEF,kBACE,YAEF,uBACE,YC/KF,MACE,6BACA,+BACA,+BACA,kCACA,kCACA,gCACA,uDAEA,uDACA,6DACA,6DACA,yDAEA,8BACA,8BACA,kFACA,sFACA,oFACA,wFACA,kCACA,kCACA,mCACA,gCACA,mCACA,yBAEA,qCACA,iCAGA,iCACA,oCACA,oCACA,kCAEA,kCACA,6CAGA,mHAUA,wCACA,2DACA,iEACA,iEACA,6DACA,mCC1DF,2BACE,gCACA,6DACA,eACA,YACA,kCACA,sBACA,WACA,qCACE,8BACA,gCAEF,uCACE,8BACA,SACA,2BAEF,sCACE,8BACA,kCAEF,wCACE,oCACA,gCAEF,0CACE,oCACA,SACA,2BAEF,yCACE,oCACA,kCAIJ,2CACE,2BACE,YACA,UACA,+BACA,SACA,kHAGE,6BACA,wBAEF,2HAGE,mCACA,wBAEF,gCACE,iCACA,cCxDN,iBACE,OACA,kBACA,kBACA,4CACA,sBACA,mBACA,YACA,8CACA,uCACA,aACA,8BACA,4CACA,wCACA,eACA,cAEA,UACA,gBAEA,0BACE,kBACA,WACA,sDACA,yBAEA,kIACE,uBAGF,gDACE,iBAGF,kEACE,UAGF,gCACE,WACA,kBACA,OACA,QACA,0BACA,YAGF,wCACE,MAGF,wCACE,SAGF,wEACE,qBAGF,wEACE,wBAGF,iCACE,WACA,kBACA,OACA,QACA,SACA,YACA,oBACA,WAIJ,sBACE,cAEF,iCACE,eAEF,sBACE,cACA,cACA,YACA,aACA,mBACA,qCACE,sBACA,OAGJ,sBACE,uBACA,WACA,cACA,aAIJ,mBACE,yBACA,uBAGF,wBACE,yBACA,uBAGF,2CACE,iBACE,gBACA,iBChHF,6BACE,sCACA,sCAEF,8BACE,uCACA,uCAEF,yDACE,uCACA,uCAEF,sDACE,sCACA,sCAEF,yDACE,yCACA,yCAEF,yDACE,yCACA,yCAEF,uDACE,uCACA,uCAKF,qCACE,gDAEF,oCACE,+CAEF,8BACE,+CAEF,iCACE,kDAEF,iCACE,kDAEF,+BACE,gDAEF,uRAIE,6CCtDJ,wBACE,WACA,yBACA,aACA,YACA,UACA,eACA,WACA,oBACA,sBACA,UACA,+BACE,WACA,WAGF,4BACE,kBACA,YACA,WAGF,4DAEE,UCxBJ,mCACE,GACE,oBAEF,KACE,qBAIJ,wBACE,kBACA,SACA,OACA,WACA,YACA,gCACA,WACA,sBACA,0DAEA,kCACE,oDAGF,oCACE,yBAGF,6BACE,QACA,aACA,uBACA,kCACA,2DAGF,6BACE,kBACA,SACA,OACA,WACA,WACA,0DAGF,+CACE,UAGF,4BACE,2CACA,WACA,YCpDJ,mBACE,WACA,YACA,sBACA,iBACA,mBACA,sDACA,iDACA,8CCJF,mCACE,oBAJA,8DAWA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gBAIJ,oCACE,IACE,UACA,0CAEF,GACE,UACA,4CAIJ,kCACE,oBA1CA,8DAiDA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,UACA,yCAEF,GACE,UACA,6CAIJ,gCACE,oBAhFA,8DAuFA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gCAIJ,iCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,sCAIJ,kCACE,oBA1HA,8DAiIA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,qCAKF,uEAEE,sCAEF,yEAEE,uCAEF,oCACE,sCAEF,uCACE,oCAKF,qEAEE,uCAEF,uEAEE,wCAEF,mCACE,qCAEF,sCACE,uCClMJ,4BACE,KACE,UACA,iCAEF,IACE,WAIJ,6BACE,KACE,UAEF,IACE,UACA,6DAEF,GACE,WAIJ,sBACE,gCAGF,qBACE,iCC5BF,4BACE,KACE,sDACA,kCACA,UAEF,IACE,uDACA,kCAEF,IACE,sDACA,UAEF,IACE,sDAEF,GACE,8BAIJ,6BACE,KACE,yDAEF,IACE,mFACA,UAEF,GACE,kFACA,WAIJ,sBACE,gCAGF,qBACE,iCCrCF,kCACE,KACE,kCACA,mBAEF,GARA,uCAaF,iCACE,KACE,mCACA,mBAEF,GAlBA,uCAuBF,+BACE,KACE,kCACA,mBAEF,GA5BA,uCAiCF,iCACE,KACE,mCACA,mBAEF,GAtCA,uCA2CF,mCACE,KA5CA,sCA+CA,GACE,kBACA,0CAIJ,kCACE,KAtDA,sCAyDA,GACE,kBACA,2CAIJ,kCACE,KAhEA,sCAmEA,GACE,kBACA,oCAIJ,gCACE,KA1EA,sCA6EA,GACE,kBACA,qCAUF,qEAEE,qCAEF,uEAEE,sCAEF,mCACE,qCAEF,sCACE,mCAKF,mEAEE,sCAxBF,kCACA,uBA0BA,qEAEE,uCA7BF,kCACA,uBA+BA,kCACE,oCAjCF,kCACA,uBAmCA,qCACE,sCArCF,kCACA,uBCtFF,0BACE,KACE,uBAEF,GACE,0BbgYJ,KACE,sBACA,WACA,6BACA,eACA,cACA,kBACA,aACA,YAGF,QACE,yBACA,WACA,aACA,aACA,8BACA,mBACA,WACE,SAGF,UACE,WAEA,gBACE,2BAQN,SACE,kBAGF,kBACE,mBACA,aACA,kBACA,QACA,aAIA,8BACE,6BAEF,gCACE,gBAIJ,eACE,sCACA,kBACA,aAGF,wBACE,mBACA,aACA,8BACA,mBAGF,sBACE,gBACA,eACA,gBAEA,yBACE,kBACA,oCACE,gBAKN,iBACE,aACA,2BACA,mBAGF,uBACE,OACA,iBAGF,wBACE,iBAGF,sBACE,kBACA,cACA,YACA,mBACA,WAGF,SACE,wBACA,kBAGF,iBACE,qDACA,2BACA,wBACA,aACA,kBACA,MACA,OACA,WAGF,cACE,kBACA,sBACA,mBACA,YACA,aAGF,eACE,oBACA,iBACA,kBAGF,cACE,gBACA,SACA,UACA,kBACA,QACA,SACA,kBAEA,iBACE,qBACA,iBACA,WAEA,mBACE,WACA,cAQN,kBACE,cAGF,oBACE,cACA,YAIF,MACE,gBACA,6BACA,aAGF,kBACE,0BAGF,WACE,aACA,8BACA,mBACA,kBAGF,cACE,qBACA,qBAGF,+CAGE,cACA,WAGF,SACE,cACA,WAGF,iBACE,cACA,WAGF,MACE,cAGF,KACE,yBACA,WACA,cACA,iBACA,YACA,kBACA,kBACA,qBACA,qCACA,WAGF,WACE,yBAGF,eACE,eACA,MACA,OACA,WACA,YACA,gCACA,aACA,uBACA,mBACA,UAGF,OACE,sBACA,WACA,aACA,kBACA,cACA,gBACA,WAGF,eACE,kBAGF,OACE,kBACA,MACA,QACA,eAIF,UACE,aACA,kBACA,qBAGF,cACE,qBACA,iBACA,WAIF,kBACE,aACA,kBACA,QACA,yBACA,gBACA,uCACA,UAIF,oBACE,WACA,kBACA,qBACA,cAIF,0BACE,sBAIF,kCACE,cAGF,MACE,oBAGF,MACE,yBACA,eACA,SACA,OACA,WAGF,UACE,aACA,eACA,6BACA,mBACA,WACA,SACA,kBACA,kBAEA,YACE,qBACA,eACA,kBAIJ,aACE,gBACA,SACA,UAGF,aACE,cACA,WAGF,YACE,WACA,cACA,qBAGF,UACE,oBAGF,cACE,aAGF,iBACE,aACA,8BACA,uBACA,mBAGF,kCACI,aAGJ,iCACI,aACA,aACA","file":"style.build.css"} -------------------------------------------------------------------------------- /src/components/Accounts.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import { SimplePool } from 'nostr-tools/pool' 5 | import hideStringMiddle from '../helpers/hideStringMiddle' 6 | import ImportAccountModal from '../modals/ImportAccountModal' 7 | import DeriveAccountModal from '../modals/DeriveAccountModal' 8 | import MainContext from '../contexts/MainContext' 9 | 10 | const Accounts = () => { 11 | const { accounts, defaultAccount, updateDefaultAccount } = useContext(MainContext) 12 | 13 | //const [accounts, setAccounts] = useState([]) 14 | //const [defaultAccount, setDefaultAccount] = useState({ index: '', name: '', type: '' }) 15 | const [isDropdownOpen, setIsDropdownOpen] = useState(false) 16 | const [showImportAccountModal, setShowImportAccountModal] = useState(false) 17 | const [showDeriveAccount, setShowDeriveAccount] = useState(false) 18 | 19 | const pool = new SimplePool() 20 | const navigate = useNavigate() 21 | 22 | useEffect(() => { 23 | //browser.storage.onChanged.addListener(function(changes, area) { 24 | // let { newValue, oldValue } = changes.vault 25 | // if (newValue.accountDefault !== oldValue.accountDefault) { 26 | // console.log(1) 27 | // fetchData() 28 | // } 29 | //}) 30 | }, []) 31 | 32 | useEffect(() => { 33 | if (accounts.length) { 34 | fetchData() 35 | } 36 | }, [accounts]) 37 | 38 | const fetchData = async () => { 39 | const storage = await browser.storage.local.get() 40 | 41 | if (storage.isAuthenticated && !storage.isLocked) { 42 | if (!storage.vault.accountDefault) { 43 | storage.vault.accountDefault = storage.vault.accounts[0].prvKey 44 | } 45 | } 46 | } 47 | 48 | const changeDefaultAccount = async (prvKey) => { 49 | const { vault: vaultData } = await browser.storage.local.get(['vault']) 50 | vaultData.accountDefault = prvKey 51 | 52 | await browser.storage.local.set({ 53 | vault: vaultData, 54 | }) 55 | updateDefaultAccount() 56 | fetchData() 57 | toggleDropdown() 58 | navigate('/vault') 59 | } 60 | 61 | const toggleDropdown = () => { 62 | setIsDropdownOpen(!isDropdownOpen) 63 | } 64 | 65 | const lockVault = async () => { 66 | await browser.storage.local.set({ 67 | isLocked: true, 68 | vault: { 69 | accounts: [], 70 | }, 71 | password: '', 72 | }) 73 | window.location.reload() 74 | } 75 | 76 | const deriveAccountCallback = () => { 77 | setShowDeriveAccount(false) 78 | //fetchData() 79 | } 80 | 81 | const importAccountCallback = () => { 82 | setShowImportAccountModal(false) 83 | //fetchData() 84 | } 85 | 86 | return ( 87 | <> 88 |
89 | { e.preventDefault(); toggleDropdown() }}> 90 | 91 | 92 |
93 | 94 | {defaultAccount.type === "derived" && ( 95 | defaultAccount.name ? defaultAccount.name : 'Account ' + defaultAccount.index 96 | )} 97 | {defaultAccount.type === "imported" && ( 98 | defaultAccount.name ? defaultAccount.name : 'Imported ' + defaultAccount.index 99 | )} 100 | 101 |
102 |
103 | 104 | {isDropdownOpen && ( 105 |
106 | 115 | 116 | 144 | 145 | 161 |
162 | )} 163 | 164 | setShowImportAccountModal(false)} 168 | > 169 | 170 | setShowDeriveAccount(false)} 174 | > 175 |
176 | 177 | ) 178 | } 179 | export default Accounts 180 | -------------------------------------------------------------------------------- /src/background.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import {validateEvent, finalizeEvent, getPublicKey} from 'nostr-tools/pure' 3 | import * as nip19 from 'nostr-tools/nip19' 4 | import * as nip04 from 'nostr-tools/nip04' 5 | import * as nip44 from 'nostr-tools/nip44' 6 | import {Mutex} from 'async-mutex' 7 | import {LRUCache} from './utils' 8 | 9 | import { 10 | NO_PERMISSIONS_REQUIRED, 11 | getPermissionStatus, 12 | updatePermission, 13 | showNotification, 14 | getPosition 15 | } from './common' 16 | 17 | let openPrompt = null 18 | let promptMutex = new Mutex() 19 | let releasePromptMutex = () => {} 20 | let secretsCache = new LRUCache(100) 21 | let accountDefault = null 22 | let previousSk = null 23 | 24 | function getSharedSecret(sk, peer) { 25 | // Detect a key change and erase the cache if they changed their key 26 | if (previousSk !== sk) { 27 | secretsCache.clear() 28 | } 29 | 30 | let key = secretsCache.get(peer) 31 | 32 | if (!key) { 33 | key = nip44.v2.utils.getConversationKey(sk, peer) 34 | secretsCache.set(peer, key) 35 | } 36 | 37 | return key 38 | } 39 | 40 | //set the width and height of the prompt window 41 | const width = 340 42 | const height = 360 43 | 44 | // Function to handle when the extension is installed or updated 45 | browser.runtime.onInstalled.addListener(async (_, __, reason) => { 46 | if (reason === 'install') browser.runtime.openOptionsPage() 47 | 48 | await browser.storage.local.set({ 49 | "relays": [ 50 | "wss://relay.damus.io", 51 | "wss://nos.lol", 52 | "wss://nostr.bitcoiner.social", 53 | "wss://offchain.pub", 54 | ] 55 | }) 56 | }) 57 | 58 | browser.runtime.onMessage.addListener(async (message, sender) => { 59 | if (message.openSignUp) { 60 | openSignUpWindow() 61 | browser.windows.remove(sender.tab.windowId) 62 | } else { 63 | let {prompt} = message 64 | if (prompt) { 65 | handlePromptMessage(message, sender) 66 | } else { 67 | return handleContentScriptMessage(message) 68 | } 69 | } 70 | }) 71 | 72 | browser.runtime.onMessageExternal.addListener( 73 | async ({type, params}, sender) => { 74 | let extensionId = new URL(sender.url).host 75 | return handleContentScriptMessage({type, params, host: extensionId}) 76 | } 77 | ) 78 | 79 | browser.windows.onRemoved.addListener(_ => { 80 | if (openPrompt) { 81 | // calling this with a simple "no" response will not store anything, so it's fine 82 | // it will just return a failure 83 | handlePromptMessage({accept: false}, null) 84 | } 85 | }) 86 | 87 | browser.storage.onChanged.addListener((changes, area) => { 88 | for (let [key, { oldValue, newValue }] of Object.entries(changes)) { 89 | if (key === 'vault') { 90 | accountDefault = newValue.accountDefault 91 | } 92 | } 93 | }) 94 | 95 | async function handleContentScriptMessage({type, params, host}) { 96 | if (NO_PERMISSIONS_REQUIRED[type]) { 97 | // authorized, and we won't do anything with private key here, so do a separate handler 98 | switch (type) { 99 | case 'replaceURL': { 100 | let {protocol_handler: ph} = await browser.storage.local.get([ 101 | 'protocol_handler' 102 | ]) 103 | if (!ph) return false 104 | 105 | let {url} = params 106 | let raw = url.split('nostr:')[1] 107 | let {type, data} = nip19.decode(raw) 108 | let replacements = { 109 | raw, 110 | hrp: type, 111 | hex: 112 | type === 'npub' || type === 'note' 113 | ? data 114 | : type === 'nprofile' 115 | ? data.pubkey 116 | : type === 'nevent' 117 | ? data.id 118 | : null, 119 | p_or_e: {npub: 'p', note: 'e', nprofile: 'p', nevent: 'e'}[type], 120 | u_or_n: {npub: 'u', note: 'n', nprofile: 'u', nevent: 'n'}[type], 121 | relay0: type === 'nprofile' ? data.relays[0] : null, 122 | relay1: type === 'nprofile' ? data.relays[1] : null, 123 | relay2: type === 'nprofile' ? data.relays[2] : null 124 | } 125 | let result = ph 126 | Object.entries(replacements).forEach(([pattern, value]) => { 127 | result = result.replace(new RegExp(`{ *${pattern} *}`, 'g'), value) 128 | }) 129 | 130 | return result 131 | } 132 | } 133 | 134 | return 135 | } else { 136 | // acquire mutex here before reading policies 137 | releasePromptMutex = await promptMutex.acquire() 138 | 139 | let allowed = await getPermissionStatus( 140 | host, 141 | type, 142 | type === 'signEvent' ? params.event : undefined 143 | ) 144 | 145 | if (allowed === true) { 146 | // authorized, proceed 147 | releasePromptMutex() 148 | showNotification(host, allowed, type, params) 149 | } else if (allowed === false) { 150 | // denied, just refuse immediately 151 | releasePromptMutex() 152 | showNotification(host, allowed, type, params) 153 | return { 154 | error: {message: 'denied'} 155 | } 156 | } else { 157 | // ask for authorization 158 | try { 159 | let id = Math.random().toString().slice(4) 160 | let qs = new URLSearchParams({ 161 | host, 162 | id, 163 | params: JSON.stringify(params), 164 | type 165 | }) 166 | // center prompt 167 | const {top, left} = await getPosition(width, height) 168 | // prompt will be resolved with true or false 169 | let accept = await new Promise((resolve, reject) => { 170 | openPrompt = {resolve, reject} 171 | 172 | browser.windows.create({ 173 | url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`, 174 | type: 'popup', 175 | width: width, 176 | height: height, 177 | top: top, 178 | left: left 179 | }) 180 | }) 181 | 182 | // denied, stop here 183 | if (!accept) return {error: {message: 'denied'}} 184 | } catch (err) { 185 | // errored, stop here 186 | releasePromptMutex() 187 | return { 188 | error: {message: error.message, stack: error.stack} 189 | } 190 | } 191 | } 192 | } 193 | 194 | // if we're here this means it was accepted 195 | let { vault } = await browser.storage.local.get(['vault']) 196 | if (!vault || !vault.accountDefault) { 197 | return {error: {message: 'no private key found'} } 198 | } 199 | 200 | accountDefault = vault.accountDefault 201 | 202 | try { 203 | switch (type) { 204 | case 'getPublicKey': { 205 | return getPublicKey(accountDefault) 206 | } 207 | case 'signEvent': { 208 | const event = finalizeEvent(params.event, accountDefault) 209 | 210 | return validateEvent(event) 211 | ? event 212 | : {error: {message: 'invalid event'}} 213 | } 214 | case 'nip04.encrypt': { 215 | let {peer, plaintext} = params 216 | return nip04.encrypt(accountDefault, peer, plaintext) 217 | } 218 | case 'nip04.decrypt': { 219 | let {peer, ciphertext} = params 220 | return nip04.decrypt(accountDefault, peer, ciphertext) 221 | } 222 | case 'nip44.encrypt': { 223 | const {peer, plaintext} = params 224 | const key = getSharedSecret(accountDefault, peer) 225 | 226 | return nip44.v2.encrypt(plaintext, key) 227 | } 228 | case 'nip44.decrypt': { 229 | const {peer, ciphertext} = params 230 | const key = getSharedSecret(accountDefault, peer) 231 | 232 | return nip44.v2.decrypt(ciphertext, key) 233 | } 234 | } 235 | } catch (error) { 236 | return {error: {message: error.message, stack: error.stack}} 237 | } 238 | } 239 | 240 | async function handlePromptMessage({host, type, accept, conditions}, sender) { 241 | // return response 242 | openPrompt?.resolve?.(accept) 243 | 244 | // update policies 245 | if (conditions) { 246 | await updatePermission(host, type, accept, conditions) 247 | } 248 | 249 | // cleanup this 250 | openPrompt = null 251 | 252 | // release mutex here after updating policies 253 | releasePromptMutex() 254 | 255 | // close prompt 256 | if (sender) { 257 | browser.windows.remove(sender.tab.windowId) 258 | } 259 | } 260 | 261 | async function openSignUpWindow() { 262 | const {top, left} = await getPosition(width, height) 263 | 264 | browser.windows.create({ 265 | url: `${browser.runtime.getURL('signup.html')}`, 266 | type: 'popup', 267 | width: width, 268 | height: height, 269 | top: top, 270 | left: left 271 | }) 272 | } 273 | -------------------------------------------------------------------------------- /src/pages/VaultPage.jsx: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | import React, { useState, useEffect, useContext } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import getIdenticon from '../helpers/identicon' 5 | import hideStringMiddle from '../helpers/hideStringMiddle' 6 | import copyToClipboard from '../helpers/copyToClipboard' 7 | import EditAccountModal from '../modals/EditAccountModal' 8 | import AccountDetailsModal from '../modals/AccountDetailsModal' 9 | import MainContext from '../contexts/MainContext' 10 | import { encrypt, removePermissions } from '../common' 11 | 12 | const VaultPage = () => { 13 | const { accounts, defaultAccount, updateAccounts, updateDefaultAccount } = useContext(MainContext) 14 | 15 | const navigate = useNavigate() 16 | 17 | const [showEditAccountModal, setEditAccountModal] = useState(false) 18 | const [accountEditing, setAccountEditing] = useState({}) 19 | const [accountDetails, setAccountDetails] = useState({}) 20 | const [showAccountDetails, setShowAccountDetails] = useState(false) 21 | const [policies, setPermissions] = useState([]) 22 | const [accountFormat, setAccountFormat] = useState('bech32') 23 | const [loaded, setLoaded] = useState(false) 24 | 25 | useEffect(() => { 26 | updateAccounts() 27 | loadPermissions() 28 | browser.storage.onChanged.addListener(function(changes, area) { 29 | for (let [key, { oldValue, newValue }] of Object.entries(changes)) { 30 | if (key === 'isAuthenticated') { 31 | window.location.reload() 32 | } 33 | } 34 | }) 35 | }, []) 36 | 37 | useEffect(() => { 38 | if (accounts.length) { 39 | fetchData() 40 | 41 | browser.storage.onChanged.addListener(function(changes, area) { 42 | //let { newValue, oldValue } = changes.vault 43 | //if (newValue.accountDefault !== oldValue.accountDefault) { 44 | // //setLoaded(false) 45 | // //fetchData() 46 | //} 47 | }) 48 | } 49 | }, [accounts]) 50 | 51 | const fetchData = async () => { 52 | const storage = await browser.storage.local.get(['vault', 'isAuthenticated']) 53 | 54 | if (!storage.isAuthenticated) { 55 | window.location.reload() 56 | } 57 | 58 | if (!storage.vault.accountDefault) { 59 | storage.vault.accountDefault = storage.vault.accounts[0].prvKey 60 | } 61 | setLoaded(true) 62 | } 63 | 64 | async function loadPermissions() { 65 | let { policies = {} } = await browser.storage.local.get('policies') 66 | const hostData = {} 67 | 68 | Object.entries(policies).forEach(([host, accepts]) => { 69 | Object.entries(accepts).some(([accept, types]) => { 70 | if (Object.keys(types).length === 0) { 71 | return 72 | } 73 | hostData[host] = hostData[host] || [] 74 | Object.entries(types).forEach(([type, { conditions, created_at }]) => { 75 | hostData[host].push({ 76 | type, 77 | accept, 78 | conditions, 79 | created_at 80 | }) 81 | }) 82 | }) 83 | }) 84 | 85 | setPermissions(hostData) 86 | } 87 | 88 | async function handleRevoke(host, accept, type) { 89 | if ( 90 | window.confirm( 91 | `revoke all ${accept === 'true' ? 'accept' : 'deny' 92 | } ${type} policies from ${host}?` 93 | ) 94 | ) { 95 | await removePermissions(host, accept, type) 96 | //showMessage('removed policies') add toast 97 | loadPermissions() 98 | } 99 | } 100 | 101 | const editAccountCallback = async () => { 102 | setEditAccountModal(false) 103 | await updateAccounts() 104 | fetchData() 105 | } 106 | 107 | const UserIdenticon = async ( pubkey ) => { 108 | const identicon = await getIdenticon(pubkey) 109 | 110 | return `data:image/svg+xml;base64,${identicon}` 111 | } 112 | 113 | async function convertFormat(e) { 114 | e.preventDefault() 115 | setAccountFormat(accountFormat === 'bech32' ? 'hex' : 'bech32') 116 | } 117 | 118 | const deleteImportedAccount = async (prvKey) => { 119 | if (confirm("Are you sure you want to delete this account? Make sure if you have made a backup before you continue.")) { 120 | const { vault, password } = await browser.storage.local.get(['vault', 'password']) 121 | 122 | const index = vault.importedAccounts.findIndex(item => item.prvKey === prvKey) 123 | if (index !== -1) { 124 | vault.importedAccounts.splice(index, 1) 125 | 126 | vault.accountDefault = undefined 127 | 128 | const encryptedVault = encrypt(vault, password) 129 | await browser.storage.local.set({ encryptedVault, vault }) 130 | 131 | await updateAccounts() 132 | fetchData() 133 | } 134 | } 135 | } 136 | 137 | return ( 138 |
139 | <> 140 | {loaded ? ( 141 | <> 142 |
143 | { defaultAccount.banner === '' ? ( 144 |
145 | ) : ( 146 |
147 | )} 148 |
149 | 150 | 188 |
189 | 190 | {defaultAccount.type === "derived" ? ( 191 | defaultAccount.name ? defaultAccount.name : 'Account ' + defaultAccount.index 192 | ) : ( 193 | defaultAccount.name ? defaultAccount.name : 'Imported ' + defaultAccount.index 194 | )} 195 | 196 |   197 | {defaultAccount.type === "imported" && ( 198 | Imported 199 | )} 200 |
201 | {accountFormat === 'bech32' ? hideStringMiddle(defaultAccount.npub) : hideStringMiddle(defaultAccount.pubKey)} 202 |   203 | convertFormat(e)} title={accountFormat === 'bech32' ? 'Convert to hex' : 'Convert to bech32'}> 204 | 205 | 206 |   207 | copyToClipboard(e, accountFormat === 'bech32' ? defaultAccount.npub : defaultAccount.pubKey)} title="Copy"> 208 | 209 | 210 | {defaultAccount.nip05 && ( 211 | <> 212 |
213 | {defaultAccount.nip05} 214 | 215 | )} 216 | {defaultAccount.lud16 && ( 217 | <> 218 |
219 | {defaultAccount.lud16} 220 | 221 | )} 222 | {defaultAccount.about && ( 223 | <> 224 |
225 | {defaultAccount.about} 226 | 227 | )} 228 |
229 |
230 |
231 | 232 |
233 | 234 |
235 |
236 | Permissions 237 |
238 | 239 |
240 |
241 | {!!Object.values(policies).length && ( 242 | Object.entries(policies).map(([host, permissions]) => ( 243 |
244 |

{host}

245 | {permissions.map((permission, index) => ( 246 |
247 |
248 |
249 | 250 | {permission.type} 251 |   252 | {permission.accept === 'true' ? 'allow' : 'deny'} 253 |   254 | {permission.conditions.kinds 255 | ? `kinds: ${Object.keys(permission.conditions.kinds).join(', ')}` 256 | : 'always'} 257 | 258 |
259 | {new Date(permission.created_at * 1000) 260 | .toISOString() 261 | .split('.')[0] 262 | .split('T') 263 | .join(' ')} 264 |
265 | 266 |
267 | 273 |
274 |
275 |
276 | ))} 277 |
278 |
279 | )) 280 | )} 281 | {Object.values(policies).length === 0 && ( 282 |
283 | No permissions have been granted yet 284 |
285 | )} 286 |
287 |
288 |
289 | 290 | ) : ( 291 | <> 292 |
293 | Loading... 294 |
295 | 296 | )} 297 | 298 | 299 | setEditAccountModal(false)} 304 | > 305 | 306 | setShowAccountDetails(false)} 310 | > 311 |
312 | ) 313 | } 314 | 315 | export default VaultPage 316 | -------------------------------------------------------------------------------- /src/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | /* 12 | Use a more-intuitive box-sizing model. 13 | */ 14 | *, *::before, *::after { 15 | box-sizing: border-box; 16 | } 17 | 18 | html { 19 | line-height: 1.15; /* 1 */ 20 | -webkit-text-size-adjust: 100%; /* 2 */ 21 | } 22 | 23 | /* Sections 24 | ========================================================================== */ 25 | 26 | /** 27 | * Remove the margin in all browsers. 28 | */ 29 | 30 | body { 31 | margin: 0; 32 | } 33 | 34 | /** 35 | * Render the `main` element consistently in IE. 36 | */ 37 | 38 | main { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * 1. Add the correct box sizing in Firefox. 57 | * 2. Show the overflow in Edge and IE. 58 | */ 59 | 60 | hr { 61 | box-sizing: content-box; /* 1 */ 62 | height: 0; /* 1 */ 63 | overflow: visible; /* 2 */ 64 | } 65 | 66 | /** 67 | * 1. Correct the inheritance and scaling of font size in all browsers. 68 | * 2. Correct the odd `em` font sizing in all browsers. 69 | */ 70 | 71 | pre { 72 | font-family: monospace, monospace; /* 1 */ 73 | font-size: 1em; /* 2 */ 74 | } 75 | 76 | /* Text-level semantics 77 | ========================================================================== */ 78 | 79 | /** 80 | * Remove the gray background on active links in IE 10. 81 | */ 82 | 83 | a { 84 | background-color: transparent; 85 | } 86 | 87 | /** 88 | * 1. Remove the bottom border in Chrome 57- 89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 90 | */ 91 | 92 | abbr[title] { 93 | border-bottom: none; /* 1 */ 94 | text-decoration: underline; /* 2 */ 95 | text-decoration: underline dotted; /* 2 */ 96 | } 97 | 98 | /** 99 | * Add the correct font weight in Chrome, Edge, and Safari. 100 | */ 101 | 102 | b, 103 | strong { 104 | font-weight: bolder; 105 | } 106 | 107 | /** 108 | * 1. Correct the inheritance and scaling of font size in all browsers. 109 | * 2. Correct the odd `em` font sizing in all browsers. 110 | */ 111 | 112 | code, 113 | kbd, 114 | samp { 115 | font-family: monospace, monospace; /* 1 */ 116 | font-size: 1em; /* 2 */ 117 | } 118 | 119 | /** 120 | * Add the correct font size in all browsers. 121 | */ 122 | 123 | small { 124 | font-size: 80%; 125 | } 126 | 127 | /** 128 | * Prevent `sub` and `sup` elements from affecting the line height in 129 | * all browsers. 130 | */ 131 | 132 | sub, 133 | sup { 134 | font-size: 75%; 135 | line-height: 0; 136 | position: relative; 137 | vertical-align: baseline; 138 | } 139 | 140 | sub { 141 | bottom: -0.25em; 142 | } 143 | 144 | sup { 145 | top: -0.5em; 146 | } 147 | 148 | /* Embedded content 149 | ========================================================================== */ 150 | 151 | /** 152 | * Remove the border on images inside links in IE 10. 153 | */ 154 | 155 | img { 156 | border-style: none; 157 | } 158 | 159 | /* Forms 160 | ========================================================================== */ 161 | 162 | /** 163 | * 1. Change the font styles in all browsers. 164 | * 2. Remove the margin in Firefox and Safari. 165 | */ 166 | 167 | button, 168 | input, 169 | optgroup, 170 | select, 171 | textarea { 172 | font-family: inherit; /* 1 */ 173 | font-size: 100%; /* 1 */ 174 | line-height: 1.15; /* 1 */ 175 | margin: 0; /* 2 */ 176 | } 177 | 178 | /** 179 | * Show the overflow in IE. 180 | * 1. Show the overflow in Edge. 181 | */ 182 | 183 | button, 184 | input { /* 1 */ 185 | overflow: visible; 186 | } 187 | 188 | /** 189 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 190 | * 1. Remove the inheritance of text transform in Firefox. 191 | */ 192 | 193 | button, 194 | select { /* 1 */ 195 | text-transform: none; 196 | } 197 | 198 | /** 199 | * Correct the inability to style clickable types in iOS and Safari. 200 | */ 201 | 202 | button, 203 | [type="button"], 204 | [type="reset"], 205 | [type="submit"] { 206 | -webkit-appearance: button; 207 | } 208 | 209 | /** 210 | * Remove the inner border and padding in Firefox. 211 | */ 212 | 213 | button::-moz-focus-inner, 214 | [type="button"]::-moz-focus-inner, 215 | [type="reset"]::-moz-focus-inner, 216 | [type="submit"]::-moz-focus-inner { 217 | border-style: none; 218 | padding: 0; 219 | } 220 | 221 | /** 222 | * Restore the focus styles unset by the previous rule. 223 | */ 224 | 225 | button:-moz-focusring, 226 | [type="button"]:-moz-focusring, 227 | [type="reset"]:-moz-focusring, 228 | [type="submit"]:-moz-focusring { 229 | outline: 1px dotted ButtonText; 230 | } 231 | 232 | /** 233 | * Correct the padding in Firefox. 234 | */ 235 | 236 | fieldset { 237 | padding: 0.35em 0.75em 0.625em; 238 | } 239 | 240 | /** 241 | * 1. Correct the text wrapping in Edge and IE. 242 | * 2. Correct the color inheritance from `fieldset` elements in IE. 243 | * 3. Remove the padding so developers are not caught out when they zero out 244 | * `fieldset` elements in all browsers. 245 | */ 246 | 247 | legend { 248 | box-sizing: border-box; /* 1 */ 249 | color: inherit; /* 2 */ 250 | display: table; /* 1 */ 251 | max-width: 100%; /* 1 */ 252 | padding: 0; /* 3 */ 253 | white-space: normal; /* 1 */ 254 | } 255 | 256 | /** 257 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 258 | */ 259 | 260 | progress { 261 | vertical-align: baseline; 262 | } 263 | 264 | /** 265 | * Remove the default vertical scrollbar in IE 10+. 266 | */ 267 | 268 | textarea { 269 | overflow: auto; 270 | } 271 | 272 | /** 273 | * 1. Add the correct box sizing in IE 10. 274 | * 2. Remove the padding in IE 10. 275 | */ 276 | 277 | [type="checkbox"], 278 | [type="radio"] { 279 | box-sizing: border-box; /* 1 */ 280 | padding: 0; /* 2 */ 281 | } 282 | 283 | /** 284 | * Correct the cursor style of increment and decrement buttons in Chrome. 285 | */ 286 | 287 | [type="number"]::-webkit-inner-spin-button, 288 | [type="number"]::-webkit-outer-spin-button { 289 | height: auto; 290 | } 291 | 292 | /** 293 | * 1. Correct the odd appearance in Chrome and Safari. 294 | * 2. Correct the outline style in Safari. 295 | */ 296 | 297 | [type="search"] { 298 | -webkit-appearance: textfield; /* 1 */ 299 | outline-offset: -2px; /* 2 */ 300 | } 301 | 302 | /** 303 | * Remove the inner padding in Chrome and Safari on macOS. 304 | */ 305 | 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | /** 311 | * 1. Correct the inability to style clickable types in iOS and Safari. 312 | * 2. Change font properties to `inherit` in Safari. 313 | */ 314 | 315 | ::-webkit-file-upload-button { 316 | -webkit-appearance: button; /* 1 */ 317 | font: inherit; /* 2 */ 318 | } 319 | 320 | /* Interactive 321 | ========================================================================== */ 322 | 323 | /* 324 | * Add the correct display in Edge, IE 10+, and Firefox. 325 | */ 326 | 327 | details { 328 | display: block; 329 | } 330 | 331 | /* 332 | * Add the correct display in all browsers. 333 | */ 334 | 335 | summary { 336 | display: list-item; 337 | } 338 | 339 | /* Misc 340 | ========================================================================== */ 341 | 342 | /** 343 | * Add the correct display in IE 10+. 344 | */ 345 | 346 | template { 347 | display: none; 348 | } 349 | 350 | /** 351 | * Add the correct display in IE 10. 352 | */ 353 | 354 | [hidden] { 355 | display: none; 356 | } 357 | 358 | button { 359 | background-color: #9b59b6; /* Purple color */ 360 | color: #fff; /* White text */ 361 | padding: 5px 10px; /* Padding */ 362 | border: none; /* No border */ 363 | border-radius: 5px; /* Rounded corners */ 364 | cursor: pointer; /* Cursor style */ 365 | text-align: center; /* Center text */ 366 | text-decoration: none; /* Remove underline */ 367 | display: inline-block; /* Make it inline block */ 368 | transition: background-color 0.3s ease; /* Smooth transition */ 369 | } 370 | 371 | button:hover { 372 | background-color: #c39bd3; /* Lighter shade of purple on hover */ 373 | } 374 | 375 | 376 | a { 377 | color: #7d7d7d; 378 | text-decoration: none; 379 | transition: color 0.3s ease; 380 | } 381 | 382 | a:hover { 383 | color: #cdcdcd; 384 | } 385 | 386 | @import '_fonts.scss'; 387 | @import '../../../node_modules/react-toastify/scss/main.scss'; 388 | 389 | /* Set default font family and size */ 390 | body { 391 | background-color: #000; 392 | color: #fff; 393 | font-family: Arial, sans-serif; 394 | font-size: 16px; 395 | margin: 0 auto; 396 | position: relative; 397 | height: 600px; 398 | width: 357px; 399 | } 400 | 401 | .header { 402 | background-color: #9b59b6; 403 | color: #fff; 404 | padding: 10px; 405 | display: flex; 406 | justify-content: space-between; 407 | align-items: center; 408 | h1 { 409 | margin: 0; 410 | } 411 | 412 | a { 413 | color: #fff; 414 | 415 | &:hover { 416 | color: rgba(#fff, .7); 417 | } 418 | } 419 | } 420 | 421 | .header-nav { 422 | } 423 | 424 | .account { 425 | position: relative; 426 | } 427 | 428 | .account-dropdown { 429 | background: #5d5d5d; 430 | padding: 15px; 431 | position: absolute; 432 | right: 0; 433 | z-index: 1000; 434 | } 435 | 436 | .account-dropdown__item { 437 | &:hover { 438 | background: rgba(#333, .7); 439 | } 440 | &.current { 441 | background: #333; 442 | } 443 | } 444 | 445 | .account-items { 446 | border: 1px solid rgba(#fff, .6); 447 | overflow-y: scroll; 448 | height: 150px; 449 | } 450 | 451 | .account-dropdown__head { 452 | align-items: center; 453 | display: flex; 454 | justify-content: space-between; 455 | margin-bottom: 10px; 456 | } 457 | 458 | .account-dropdown-nav { 459 | list-style: none; 460 | padding-left: 0; 461 | margin-bottom: 0; 462 | 463 | > li { 464 | margin-bottom: 5px; 465 | &:last-child { 466 | margin-bottom: 0; 467 | } 468 | } 469 | } 470 | 471 | .account-profile { 472 | display: flex; 473 | flex-direction: row-reverse; 474 | align-items: center; 475 | } 476 | 477 | .account-profile__body { 478 | flex: 1; 479 | text-align: right; 480 | } 481 | 482 | .account-dropdown__item { 483 | padding: 5px 10px; 484 | } 485 | 486 | .account-profile__img { 487 | border-radius: 50%; 488 | display: block; 489 | height: 35px; 490 | margin: 0 2px 0 5px; 491 | width: 35px; 492 | } 493 | 494 | .profile { 495 | padding: 100px 10px 15px; 496 | position: relative; 497 | } 498 | 499 | .profile__banner { 500 | background: url('../../assets/img/store-banner.jpeg'); 501 | background-position: center; 502 | background-size: contain; 503 | height: 100px; 504 | position: absolute; 505 | top: 0; 506 | left: 0; 507 | width: 100%; 508 | } 509 | 510 | .profile__img { 511 | border-radius: 50%,; 512 | border: 3px solid #fff; 513 | margin-bottom: 10px; 514 | width: 100px; 515 | height: 100px; 516 | } 517 | 518 | .profile__body { 519 | line-height: 1.25rem; 520 | margin-top: -53px; 521 | position: relative; 522 | } 523 | 524 | .profile__nav { 525 | list-style: none; 526 | margin: 0; 527 | padding: 0; 528 | position: absolute; 529 | right: 0; 530 | top: 62px; 531 | text-align: center; 532 | 533 | > li { 534 | display: inline-block; 535 | margin-left: 10px; 536 | width: 16px; 537 | 538 | > a { 539 | color: #fff; 540 | display: block; 541 | } 542 | } 543 | } 544 | 545 | .Popup { 546 | } 547 | 548 | .Popup .container { 549 | margin: 0 15px; 550 | } 551 | 552 | .Options .container { 553 | margin: 0 auto; 554 | width: 600px; 555 | } 556 | 557 | 558 | .card { 559 | background: #111; 560 | border-bottom: 1px solid #222; 561 | padding: 15px; 562 | } 563 | 564 | .card:first-child { 565 | border-top: 1px solid #222; 566 | } 567 | 568 | .card-head { 569 | display: flex; 570 | justify-content: space-between; 571 | align-items: center; 572 | margin-bottom: 5px; 573 | } 574 | 575 | .break-string { 576 | word-wrap: break-word; 577 | white-space: pre-wrap; 578 | } 579 | 580 | textarea, 581 | input[type="text"], 582 | input[type="password"] { 583 | display: block; 584 | width: 100%; 585 | } 586 | 587 | textarea { 588 | display: block; 589 | width: 100%; 590 | } 591 | 592 | input[type="file"] { 593 | display: block; 594 | width: 100%; 595 | } 596 | 597 | label { 598 | display: block; 599 | } 600 | 601 | .btn { 602 | background-color: #9b59b6; /* Purple color */ 603 | color: #fff; /* White text */ 604 | display: block; 605 | padding: 5px 10px; /* Padding */ 606 | border: none; /* No border */ 607 | border-radius: 5px; /* Rounded corners */ 608 | text-align: center; /* Center text */ 609 | text-decoration: none; /* Remove underline */ 610 | transition: background-color 0.3s ease; /* Smooth transition */ 611 | width: 100%; 612 | } 613 | 614 | .btn:hover { 615 | background-color: #c39bd3; /* Lighter shade of purple on hover */ 616 | } 617 | 618 | .modal-overlay { 619 | position: fixed; 620 | top: 0; 621 | left: 0; 622 | width: 100%; 623 | height: 100%; 624 | background-color: rgba(0, 0, 0, 0.5); 625 | display: flex; 626 | justify-content: center; 627 | align-items: center; 628 | z-index: 2; 629 | } 630 | 631 | .modal { 632 | background-color: #222; 633 | color: #fff; 634 | padding: 15px; 635 | border-radius: 8px; 636 | margin: 0 15px; 637 | max-width: 340px; 638 | width: 100%; 639 | } 640 | 641 | .modal-content { 642 | position: relative; 643 | } 644 | 645 | .close { 646 | position: absolute; 647 | top: 0; 648 | right: 0; 649 | cursor: pointer; 650 | } 651 | 652 | /* Style the dropdown button */ 653 | .dropdown { 654 | display: flex; 655 | position: relative; 656 | display: inline-block; 657 | } 658 | 659 | .dropdown-btn { 660 | display: inline-block; 661 | text-align: right; 662 | width: 25px; 663 | } 664 | 665 | /* Style the dropdown content (hidden by default) */ 666 | .dropdown-content { 667 | display: none; 668 | position: absolute; 669 | right: 0; 670 | background-color: #f9f9f9; 671 | min-width: 200px; 672 | box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); 673 | z-index: 1; 674 | } 675 | 676 | /* Style the links inside the dropdown */ 677 | .dropdown-content a { 678 | color: black; 679 | padding: 10px 15px; 680 | text-decoration: none; 681 | display: block; 682 | } 683 | 684 | /* Change color of dropdown links on hover */ 685 | .dropdown-content a:hover { 686 | background-color: #ddd; 687 | } 688 | 689 | /* Show the dropdown menu on hover */ 690 | .dropdown:hover .dropdown-content { 691 | display: block; 692 | } 693 | 694 | .tabs { 695 | padding-bottom: 65px; 696 | } 697 | 698 | .foot { 699 | background-color: #5d5d5d; 700 | position: fixed; 701 | bottom: 0; 702 | left: 0; 703 | width: 100%; 704 | } 705 | 706 | .foot-nav { 707 | display: flex; 708 | font-size: 12px; 709 | justify-content: space-around; 710 | align-items: center; 711 | color: #fff; 712 | margin: 0; 713 | padding: 15px 10px; 714 | text-align: center; 715 | 716 | i { 717 | display: inline-block; 718 | font-size: 16px; 719 | margin-bottom: 5px; 720 | } 721 | } 722 | 723 | .foot-nav ul { 724 | list-style: none; 725 | margin: 0; 726 | padding: 0; 727 | } 728 | 729 | .foot-nav li { 730 | display: block; 731 | width: 100%; 732 | } 733 | 734 | .foot-nav a { 735 | color: #fff; 736 | display: block; 737 | text-decoration: none; 738 | } 739 | 740 | .tabs-nav { 741 | padding: 10px 10px 0; 742 | } 743 | 744 | .tabs-content { 745 | padding: 10px; 746 | } 747 | 748 | .permission-item { 749 | display: flex; 750 | justify-content: space-between; 751 | align-items: flex-start; 752 | margin-bottom: 16px; 753 | } 754 | 755 | .permission-item__col:first-child { 756 | flex: 0 0 70%; 757 | } 758 | 759 | .permission-item__col:last-child { 760 | flex: 0 0 30%; 761 | display: flex; 762 | justify-content: flex-end; 763 | } 764 | -------------------------------------------------------------------------------- /src/assets/css/style.build.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */*,*::before,*::after{box-sizing:border-box}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}button{background-color:#9b59b6;color:#fff;padding:5px 10px;border:none;border-radius:5px;cursor:pointer;text-align:center;text-decoration:none;display:inline-block;transition:background-color .3s ease}button:hover{background-color:#c39bd3}a{color:#7d7d7d;text-decoration:none;transition:color .3s ease}a:hover{color:#cdcdcd}@font-face{font-family:"icomoon";src:url("../fonts/icomoon.eot?rw1199");src:url("../fonts/icomoon.eot?rw1199#iefix") format("embedded-opentype"),url("../fonts/icomoon.ttf?rw1199") format("truetype"),url("../fonts/icomoon.woff?rw1199") format("woff"),url("../fonts/icomoon.svg?rw1199#icomoon") format("svg");font-weight:normal;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:"icomoon" !important;speak:never;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-dots-three-vertical:before{content:""}.icon-pencil:before{content:""}.icon-file-zip:before{content:""}.icon-copy:before{content:""}.icon-folder-plus:before{content:""}.icon-folder-download:before{content:""}.icon-folder-upload:before{content:""}.icon-qrcode:before{content:""}.icon-hour-glass:before{content:""}.icon-spinner3:before{content:""}.icon-key:before{content:""}.icon-lock:before{content:""}.icon-unlocked:before{content:""}.icon-cog:before{content:""}.icon-lab:before{content:""}.icon-bin:before{content:""}.icon-bin2:before{content:""}.icon-switch:before{content:""}.icon-list:before{content:""}.icon-tree:before{content:""}.icon-menu:before{content:""}.icon-download:before{content:""}.icon-upload:before{content:""}.icon-download3:before{content:""}.icon-sphere:before{content:""}.icon-earth:before{content:""}.icon-eye:before{content:""}.icon-eye-blocked:before{content:""}.icon-warning:before{content:""}.icon-notification:before{content:""}.icon-plus:before{content:""}.icon-info:before{content:""}.icon-cancel-circle:before{content:""}.icon-cross:before{content:""}.icon-enter:before{content:""}.icon-exit:before{content:""}.icon-loop2:before{content:""}.icon-arrow-down:before{content:""}.icon-tab:before{content:""}.icon-move-down:before{content:""}.icon-svg:before{content:""}:root{--toastify-color-light: #fff;--toastify-color-dark: #121212;--toastify-color-info: #3498db;--toastify-color-success: #07bc0c;--toastify-color-warning: #f1c40f;--toastify-color-error: #e74c3c;--toastify-color-transparent: rgba(255, 255, 255, 0.7);--toastify-icon-color-info: var(--toastify-color-info);--toastify-icon-color-success: var(--toastify-color-success);--toastify-icon-color-warning: var(--toastify-color-warning);--toastify-icon-color-error: var(--toastify-color-error);--toastify-toast-width: 320px;--toastify-toast-offset: 16px;--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));--toastify-toast-background: #fff;--toastify-toast-min-height: 64px;--toastify-toast-max-height: 800px;--toastify-toast-bd-radius: 6px;--toastify-font-family: sans-serif;--toastify-z-index: 9999;--toastify-text-color-light: #757575;--toastify-text-color-dark: #fff;--toastify-text-color-info: #fff;--toastify-text-color-success: #fff;--toastify-text-color-warning: #fff;--toastify-text-color-error: #fff;--toastify-spinner-color: #616161;--toastify-spinner-color-empty-area: #e0e0e0;--toastify-color-progress-light: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 );--toastify-color-progress-dark: #bb86fc;--toastify-color-progress-info: var(--toastify-color-info);--toastify-color-progress-success: var(--toastify-color-success);--toastify-color-progress-warning: var(--toastify-color-warning);--toastify-color-progress-error: var(--toastify-color-error);--toastify-color-progress-bgo: 0.2}.Toastify__toast-container{z-index:var(--toastify-z-index);-webkit-transform:translate3d(0, 0, var(--toastify-z-index));position:fixed;padding:4px;width:var(--toastify-toast-width);box-sizing:border-box;color:#fff}.Toastify__toast-container--top-left{top:var(--toastify-toast-top);left:var(--toastify-toast-left)}.Toastify__toast-container--top-center{top:var(--toastify-toast-top);left:50%;transform:translateX(-50%)}.Toastify__toast-container--top-right{top:var(--toastify-toast-top);right:var(--toastify-toast-right)}.Toastify__toast-container--bottom-left{bottom:var(--toastify-toast-bottom);left:var(--toastify-toast-left)}.Toastify__toast-container--bottom-center{bottom:var(--toastify-toast-bottom);left:50%;transform:translateX(-50%)}.Toastify__toast-container--bottom-right{bottom:var(--toastify-toast-bottom);right:var(--toastify-toast-right)}@media only screen and (max-width : 480px){.Toastify__toast-container{width:100vw;padding:0;left:env(safe-area-inset-left);margin:0}.Toastify__toast-container--top-left,.Toastify__toast-container--top-center,.Toastify__toast-container--top-right{top:env(safe-area-inset-top);transform:translateX(0)}.Toastify__toast-container--bottom-left,.Toastify__toast-container--bottom-center,.Toastify__toast-container--bottom-right{bottom:env(safe-area-inset-bottom);transform:translateX(0)}.Toastify__toast-container--rtl{right:env(safe-area-inset-right);left:initial}}.Toastify__toast{--y: 0;position:relative;touch-action:none;min-height:var(--toastify-toast-min-height);box-sizing:border-box;margin-bottom:1rem;padding:8px;border-radius:var(--toastify-toast-bd-radius);box-shadow:0px 4px 12px rgba(0,0,0,.1);display:flex;justify-content:space-between;max-height:var(--toastify-toast-max-height);font-family:var(--toastify-font-family);cursor:default;direction:ltr;z-index:0;overflow:hidden}.Toastify__toast--stacked{position:absolute;width:100%;transform:translate3d(0, var(--y), 0) scale(var(--s));transition:transform .3s}.Toastify__toast--stacked[data-collapsed] .Toastify__toast-body,.Toastify__toast--stacked[data-collapsed] .Toastify__close-button{transition:opacity .1s}.Toastify__toast--stacked[data-collapsed=false]{overflow:visible}.Toastify__toast--stacked[data-collapsed=true]:not(:last-child)>*{opacity:0}.Toastify__toast--stacked:after{content:"";position:absolute;left:0;right:0;height:calc(var(--g)*1px);bottom:100%}.Toastify__toast--stacked[data-pos=top]{top:0}.Toastify__toast--stacked[data-pos=bot]{bottom:0}.Toastify__toast--stacked[data-pos=bot].Toastify__toast--stacked:before{transform-origin:top}.Toastify__toast--stacked[data-pos=top].Toastify__toast--stacked:before{transform-origin:bottom}.Toastify__toast--stacked:before{content:"";position:absolute;left:0;right:0;bottom:0;height:100%;transform:scaleY(3);z-index:-1}.Toastify__toast--rtl{direction:rtl}.Toastify__toast--close-on-click{cursor:pointer}.Toastify__toast-body{margin:auto 0;flex:1 1 auto;padding:6px;display:flex;align-items:center}.Toastify__toast-body>div:last-child{word-break:break-word;flex:1}.Toastify__toast-icon{margin-inline-end:10px;width:20px;flex-shrink:0;display:flex}.Toastify--animate{animation-fill-mode:both;animation-duration:.5s}.Toastify--animate-icon{animation-fill-mode:both;animation-duration:.3s}@media only screen and (max-width : 480px){.Toastify__toast{margin-bottom:0;border-radius:0}}.Toastify__toast-theme--dark{background:var(--toastify-color-dark);color:var(--toastify-text-color-dark)}.Toastify__toast-theme--light{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--default{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--info{color:var(--toastify-text-color-info);background:var(--toastify-color-info)}.Toastify__toast-theme--colored.Toastify__toast--success{color:var(--toastify-text-color-success);background:var(--toastify-color-success)}.Toastify__toast-theme--colored.Toastify__toast--warning{color:var(--toastify-text-color-warning);background:var(--toastify-color-warning)}.Toastify__toast-theme--colored.Toastify__toast--error{color:var(--toastify-text-color-error);background:var(--toastify-color-error)}.Toastify__progress-bar-theme--light{background:var(--toastify-color-progress-light)}.Toastify__progress-bar-theme--dark{background:var(--toastify-color-progress-dark)}.Toastify__progress-bar--info{background:var(--toastify-color-progress-info)}.Toastify__progress-bar--success{background:var(--toastify-color-progress-success)}.Toastify__progress-bar--warning{background:var(--toastify-color-progress-warning)}.Toastify__progress-bar--error{background:var(--toastify-color-progress-error)}.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error{background:var(--toastify-color-transparent)}.Toastify__close-button{color:#fff;background:rgba(0,0,0,0);outline:none;border:none;padding:0;cursor:pointer;opacity:.7;transition:.3s ease;align-self:flex-start;z-index:1}.Toastify__close-button--light{color:#000;opacity:.3}.Toastify__close-button>svg{fill:currentColor;height:16px;width:14px}.Toastify__close-button:hover,.Toastify__close-button:focus{opacity:1}@keyframes Toastify__trackProgress{0%{transform:scaleX(1)}100%{transform:scaleX(0)}}.Toastify__progress-bar{position:absolute;bottom:0;left:0;width:100%;height:100%;z-index:var(--toastify-z-index);opacity:.7;transform-origin:left;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--animated{animation:Toastify__trackProgress linear 1 forwards}.Toastify__progress-bar--controlled{transition:transform .2s}.Toastify__progress-bar--rtl{right:0;left:initial;transform-origin:right;border-bottom-left-radius:initial;border-bottom-right-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp{position:absolute;bottom:0;left:0;width:100%;height:5px;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp[data-hidden=true]{opacity:0}.Toastify__progress-bar--bg{opacity:var(--toastify-color-progress-bgo);width:100%;height:100%}.Toastify__spinner{width:20px;height:20px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--toastify-spinner-color-empty-area);border-right-color:var(--toastify-spinner-color);animation:Toastify__spin .65s linear infinite}@keyframes Toastify__bounceInRight{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(3000px, 0, 0)}60%{opacity:1;transform:translate3d(-25px, 0, 0)}75%{transform:translate3d(10px, 0, 0)}90%{transform:translate3d(-5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutRight{20%{opacity:1;transform:translate3d(-20px, var(--y), 0)}to{opacity:0;transform:translate3d(2000px, var(--y), 0)}}@keyframes Toastify__bounceInLeft{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(-3000px, 0, 0)}60%{opacity:1;transform:translate3d(25px, 0, 0)}75%{transform:translate3d(-10px, 0, 0)}90%{transform:translate3d(5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutLeft{20%{opacity:1;transform:translate3d(20px, var(--y), 0)}to{opacity:0;transform:translate3d(-2000px, var(--y), 0)}}@keyframes Toastify__bounceInUp{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(0, 3000px, 0)}60%{opacity:1;transform:translate3d(0, -20px, 0)}75%{transform:translate3d(0, 10px, 0)}90%{transform:translate3d(0, -5px, 0)}to{transform:translate3d(0, 0, 0)}}@keyframes Toastify__bounceOutUp{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, -2000px, 0)}}@keyframes Toastify__bounceInDown{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(0, -3000px, 0)}60%{opacity:1;transform:translate3d(0, 25px, 0)}75%{transform:translate3d(0, -10px, 0)}90%{transform:translate3d(0, 5px, 0)}to{transform:none}}@keyframes Toastify__bounceOutDown{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, 2000px, 0)}}.Toastify__bounce-enter--top-left,.Toastify__bounce-enter--bottom-left{animation-name:Toastify__bounceInLeft}.Toastify__bounce-enter--top-right,.Toastify__bounce-enter--bottom-right{animation-name:Toastify__bounceInRight}.Toastify__bounce-enter--top-center{animation-name:Toastify__bounceInDown}.Toastify__bounce-enter--bottom-center{animation-name:Toastify__bounceInUp}.Toastify__bounce-exit--top-left,.Toastify__bounce-exit--bottom-left{animation-name:Toastify__bounceOutLeft}.Toastify__bounce-exit--top-right,.Toastify__bounce-exit--bottom-right{animation-name:Toastify__bounceOutRight}.Toastify__bounce-exit--top-center{animation-name:Toastify__bounceOutUp}.Toastify__bounce-exit--bottom-center{animation-name:Toastify__bounceOutDown}@keyframes Toastify__zoomIn{from{opacity:0;transform:scale3d(0.3, 0.3, 0.3)}50%{opacity:1}}@keyframes Toastify__zoomOut{from{opacity:1}50%{opacity:0;transform:translate3d(0, var(--y), 0) scale3d(0.3, 0.3, 0.3)}to{opacity:0}}.Toastify__zoom-enter{animation-name:Toastify__zoomIn}.Toastify__zoom-exit{animation-name:Toastify__zoomOut}@keyframes Toastify__flipIn{from{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}to{transform:perspective(400px)}}@keyframes Toastify__flipOut{from{transform:translate3d(0, var(--y), 0) perspective(400px)}30%{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, -20deg);opacity:1}to{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, 90deg);opacity:0}}.Toastify__flip-enter{animation-name:Toastify__flipIn}.Toastify__flip-exit{animation-name:Toastify__flipOut}@keyframes Toastify__slideInRight{from{transform:translate3d(110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInLeft{from{transform:translate3d(-110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInUp{from{transform:translate3d(0, 110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInDown{from{transform:translate3d(0, -110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideOutRight{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(110%, var(--y), 0)}}@keyframes Toastify__slideOutLeft{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(-110%, var(--y), 0)}}@keyframes Toastify__slideOutDown{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, 500px, 0)}}@keyframes Toastify__slideOutUp{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, -500px, 0)}}.Toastify__slide-enter--top-left,.Toastify__slide-enter--bottom-left{animation-name:Toastify__slideInLeft}.Toastify__slide-enter--top-right,.Toastify__slide-enter--bottom-right{animation-name:Toastify__slideInRight}.Toastify__slide-enter--top-center{animation-name:Toastify__slideInDown}.Toastify__slide-enter--bottom-center{animation-name:Toastify__slideInUp}.Toastify__slide-exit--top-left,.Toastify__slide-exit--bottom-left{animation-name:Toastify__slideOutLeft;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-right,.Toastify__slide-exit--bottom-right{animation-name:Toastify__slideOutRight;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-center{animation-name:Toastify__slideOutUp;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--bottom-center{animation-name:Toastify__slideOutDown;animation-timing-function:ease-in;animation-duration:.3s}@keyframes Toastify__spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}body{background-color:#000;color:#fff;font-family:Arial,sans-serif;font-size:16px}.header{background-color:#333;color:#fff;padding:10px 15px;display:flex;justify-content:space-between;align-items:center}.header>h1{color:#9b59b6;margin:0}.Popup .container{margin:0 15px}.Options .container{margin:0 auto;width:600px}.card{background:#111;border-bottom:1px solid #222;padding:15px}.card:first-child{border-top:1px solid #222}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px}.break-string{word-wrap:break-word;white-space:pre-wrap}input[type=text],input[type=password]{display:block;width:100%}textarea{display:block;width:100%}input[type=file]{display:block;width:100%}label{display:block}.btn{display:block;width:100%}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;z-index:2}.modal{background-color:#222;color:#fff;padding:15px;border-radius:8px;margin:0 15px;max-width:340px;width:100%}.modal-content{position:relative}.close{position:absolute;top:0;right:0;cursor:pointer}.dropdown{display:flex;position:relative;display:inline-block}.dropdown-btn{display:inline-block;text-align:right;width:25px}.dropdown-content{display:none;position:absolute;right:0;background-color:#f9f9f9;min-width:200px;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);z-index:1}.dropdown-content a{color:#000;padding:10px 15px;text-decoration:none;display:block}.dropdown-content a:hover{background-color:#ddd}.dropdown:hover .dropdown-content{display:block} 2 | 3 | /*# sourceMappingURL=style.build.css.map */ -------------------------------------------------------------------------------- /dist/assets/css/style.build.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */*,*::before,*::after{box-sizing:border-box}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}button{background-color:#9b59b6;color:#fff;padding:5px 10px;border:none;border-radius:5px;cursor:pointer;text-align:center;text-decoration:none;display:inline-block;transition:background-color .3s ease}button:hover{background-color:#c39bd3}a{color:#7d7d7d;text-decoration:none;transition:color .3s ease}a:hover{color:#cdcdcd}@font-face{font-family:"icomoon";src:url("../onts/icomoon.eot?z8ca6h");src:url("../fonts/icomoon.eot?z8ca6h#iefix") format("embedded-opentype"),url("../fonts/icomoon.ttf?z8ca6h") format("truetype"),url("../fonts/icomoon.woff?z8ca6h") format("woff"),url("../fonts/icomoon.svg?z8ca6h#icomoon") format("svg");font-weight:normal;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:"icomoon" !important;speak:never;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-wallet:before{content:""}.icon-money:before{content:""}.icon-wallet1:before{content:""}.icon-money1:before{content:""}.icon-cash:before{content:""}.icon-wallet2:before{content:""}.icon-dots-three-vertical:before{content:""}.icon-pencil:before{content:""}.icon-file-zip:before{content:""}.icon-copy:before{content:""}.icon-folder-plus:before{content:""}.icon-folder-download:before{content:""}.icon-folder-upload:before{content:""}.icon-qrcode:before{content:""}.icon-hour-glass:before{content:""}.icon-spinner3:before{content:""}.icon-key:before{content:""}.icon-lock:before{content:""}.icon-unlocked:before{content:""}.icon-cog:before{content:""}.icon-lab:before{content:""}.icon-bin:before{content:""}.icon-bin2:before{content:""}.icon-switch:before{content:""}.icon-list:before{content:""}.icon-tree:before{content:""}.icon-menu:before{content:""}.icon-download:before{content:""}.icon-upload:before{content:""}.icon-download3:before{content:""}.icon-sphere:before{content:""}.icon-earth:before{content:""}.icon-eye:before{content:""}.icon-eye-blocked:before{content:""}.icon-warning:before{content:""}.icon-notification:before{content:""}.icon-plus:before{content:""}.icon-info:before{content:""}.icon-cancel-circle:before{content:""}.icon-cross:before{content:""}.icon-enter:before{content:""}.icon-exit:before{content:""}.icon-loop2:before{content:""}.icon-arrow-down:before{content:""}.icon-tab:before{content:""}.icon-move-down:before{content:""}.icon-svg:before{content:""}.icon-home:before{content:""}.icon-home3:before{content:""}.icon-user:before{content:""}.icon-user-plus:before{content:""}:root{--toastify-color-light: #fff;--toastify-color-dark: #121212;--toastify-color-info: #3498db;--toastify-color-success: #07bc0c;--toastify-color-warning: #f1c40f;--toastify-color-error: #e74c3c;--toastify-color-transparent: rgba(255, 255, 255, 0.7);--toastify-icon-color-info: var(--toastify-color-info);--toastify-icon-color-success: var(--toastify-color-success);--toastify-icon-color-warning: var(--toastify-color-warning);--toastify-icon-color-error: var(--toastify-color-error);--toastify-toast-width: 320px;--toastify-toast-offset: 16px;--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));--toastify-toast-background: #fff;--toastify-toast-min-height: 64px;--toastify-toast-max-height: 800px;--toastify-toast-bd-radius: 6px;--toastify-font-family: sans-serif;--toastify-z-index: 9999;--toastify-text-color-light: #757575;--toastify-text-color-dark: #fff;--toastify-text-color-info: #fff;--toastify-text-color-success: #fff;--toastify-text-color-warning: #fff;--toastify-text-color-error: #fff;--toastify-spinner-color: #616161;--toastify-spinner-color-empty-area: #e0e0e0;--toastify-color-progress-light: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 );--toastify-color-progress-dark: #bb86fc;--toastify-color-progress-info: var(--toastify-color-info);--toastify-color-progress-success: var(--toastify-color-success);--toastify-color-progress-warning: var(--toastify-color-warning);--toastify-color-progress-error: var(--toastify-color-error);--toastify-color-progress-bgo: 0.2}.Toastify__toast-container{z-index:var(--toastify-z-index);-webkit-transform:translate3d(0, 0, var(--toastify-z-index));position:fixed;padding:4px;width:var(--toastify-toast-width);box-sizing:border-box;color:#fff}.Toastify__toast-container--top-left{top:var(--toastify-toast-top);left:var(--toastify-toast-left)}.Toastify__toast-container--top-center{top:var(--toastify-toast-top);left:50%;transform:translateX(-50%)}.Toastify__toast-container--top-right{top:var(--toastify-toast-top);right:var(--toastify-toast-right)}.Toastify__toast-container--bottom-left{bottom:var(--toastify-toast-bottom);left:var(--toastify-toast-left)}.Toastify__toast-container--bottom-center{bottom:var(--toastify-toast-bottom);left:50%;transform:translateX(-50%)}.Toastify__toast-container--bottom-right{bottom:var(--toastify-toast-bottom);right:var(--toastify-toast-right)}@media only screen and (max-width : 480px){.Toastify__toast-container{width:100vw;padding:0;left:env(safe-area-inset-left);margin:0}.Toastify__toast-container--top-left,.Toastify__toast-container--top-center,.Toastify__toast-container--top-right{top:env(safe-area-inset-top);transform:translateX(0)}.Toastify__toast-container--bottom-left,.Toastify__toast-container--bottom-center,.Toastify__toast-container--bottom-right{bottom:env(safe-area-inset-bottom);transform:translateX(0)}.Toastify__toast-container--rtl{right:env(safe-area-inset-right);left:initial}}.Toastify__toast{--y: 0;position:relative;touch-action:none;min-height:var(--toastify-toast-min-height);box-sizing:border-box;margin-bottom:1rem;padding:8px;border-radius:var(--toastify-toast-bd-radius);box-shadow:0px 4px 12px rgba(0,0,0,.1);display:flex;justify-content:space-between;max-height:var(--toastify-toast-max-height);font-family:var(--toastify-font-family);cursor:default;direction:ltr;z-index:0;overflow:hidden}.Toastify__toast--stacked{position:absolute;width:100%;transform:translate3d(0, var(--y), 0) scale(var(--s));transition:transform .3s}.Toastify__toast--stacked[data-collapsed] .Toastify__toast-body,.Toastify__toast--stacked[data-collapsed] .Toastify__close-button{transition:opacity .1s}.Toastify__toast--stacked[data-collapsed=false]{overflow:visible}.Toastify__toast--stacked[data-collapsed=true]:not(:last-child)>*{opacity:0}.Toastify__toast--stacked:after{content:"";position:absolute;left:0;right:0;height:calc(var(--g)*1px);bottom:100%}.Toastify__toast--stacked[data-pos=top]{top:0}.Toastify__toast--stacked[data-pos=bot]{bottom:0}.Toastify__toast--stacked[data-pos=bot].Toastify__toast--stacked:before{transform-origin:top}.Toastify__toast--stacked[data-pos=top].Toastify__toast--stacked:before{transform-origin:bottom}.Toastify__toast--stacked:before{content:"";position:absolute;left:0;right:0;bottom:0;height:100%;transform:scaleY(3);z-index:-1}.Toastify__toast--rtl{direction:rtl}.Toastify__toast--close-on-click{cursor:pointer}.Toastify__toast-body{margin:auto 0;flex:1 1 auto;padding:6px;display:flex;align-items:center}.Toastify__toast-body>div:last-child{word-break:break-word;flex:1}.Toastify__toast-icon{margin-inline-end:10px;width:20px;flex-shrink:0;display:flex}.Toastify--animate{animation-fill-mode:both;animation-duration:.5s}.Toastify--animate-icon{animation-fill-mode:both;animation-duration:.3s}@media only screen and (max-width : 480px){.Toastify__toast{margin-bottom:0;border-radius:0}}.Toastify__toast-theme--dark{background:var(--toastify-color-dark);color:var(--toastify-text-color-dark)}.Toastify__toast-theme--light{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--default{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--info{color:var(--toastify-text-color-info);background:var(--toastify-color-info)}.Toastify__toast-theme--colored.Toastify__toast--success{color:var(--toastify-text-color-success);background:var(--toastify-color-success)}.Toastify__toast-theme--colored.Toastify__toast--warning{color:var(--toastify-text-color-warning);background:var(--toastify-color-warning)}.Toastify__toast-theme--colored.Toastify__toast--error{color:var(--toastify-text-color-error);background:var(--toastify-color-error)}.Toastify__progress-bar-theme--light{background:var(--toastify-color-progress-light)}.Toastify__progress-bar-theme--dark{background:var(--toastify-color-progress-dark)}.Toastify__progress-bar--info{background:var(--toastify-color-progress-info)}.Toastify__progress-bar--success{background:var(--toastify-color-progress-success)}.Toastify__progress-bar--warning{background:var(--toastify-color-progress-warning)}.Toastify__progress-bar--error{background:var(--toastify-color-progress-error)}.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error{background:var(--toastify-color-transparent)}.Toastify__close-button{color:#fff;background:rgba(0,0,0,0);outline:none;border:none;padding:0;cursor:pointer;opacity:.7;transition:.3s ease;align-self:flex-start;z-index:1}.Toastify__close-button--light{color:#000;opacity:.3}.Toastify__close-button>svg{fill:currentColor;height:16px;width:14px}.Toastify__close-button:hover,.Toastify__close-button:focus{opacity:1}@keyframes Toastify__trackProgress{0%{transform:scaleX(1)}100%{transform:scaleX(0)}}.Toastify__progress-bar{position:absolute;bottom:0;left:0;width:100%;height:100%;z-index:var(--toastify-z-index);opacity:.7;transform-origin:left;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--animated{animation:Toastify__trackProgress linear 1 forwards}.Toastify__progress-bar--controlled{transition:transform .2s}.Toastify__progress-bar--rtl{right:0;left:initial;transform-origin:right;border-bottom-left-radius:initial;border-bottom-right-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp{position:absolute;bottom:0;left:0;width:100%;height:5px;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp[data-hidden=true]{opacity:0}.Toastify__progress-bar--bg{opacity:var(--toastify-color-progress-bgo);width:100%;height:100%}.Toastify__spinner{width:20px;height:20px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--toastify-spinner-color-empty-area);border-right-color:var(--toastify-spinner-color);animation:Toastify__spin .65s linear infinite}@keyframes Toastify__bounceInRight{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(3000px, 0, 0)}60%{opacity:1;transform:translate3d(-25px, 0, 0)}75%{transform:translate3d(10px, 0, 0)}90%{transform:translate3d(-5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutRight{20%{opacity:1;transform:translate3d(-20px, var(--y), 0)}to{opacity:0;transform:translate3d(2000px, var(--y), 0)}}@keyframes Toastify__bounceInLeft{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(-3000px, 0, 0)}60%{opacity:1;transform:translate3d(25px, 0, 0)}75%{transform:translate3d(-10px, 0, 0)}90%{transform:translate3d(5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutLeft{20%{opacity:1;transform:translate3d(20px, var(--y), 0)}to{opacity:0;transform:translate3d(-2000px, var(--y), 0)}}@keyframes Toastify__bounceInUp{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(0, 3000px, 0)}60%{opacity:1;transform:translate3d(0, -20px, 0)}75%{transform:translate3d(0, 10px, 0)}90%{transform:translate3d(0, -5px, 0)}to{transform:translate3d(0, 0, 0)}}@keyframes Toastify__bounceOutUp{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, -2000px, 0)}}@keyframes Toastify__bounceInDown{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(0, -3000px, 0)}60%{opacity:1;transform:translate3d(0, 25px, 0)}75%{transform:translate3d(0, -10px, 0)}90%{transform:translate3d(0, 5px, 0)}to{transform:none}}@keyframes Toastify__bounceOutDown{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, 2000px, 0)}}.Toastify__bounce-enter--top-left,.Toastify__bounce-enter--bottom-left{animation-name:Toastify__bounceInLeft}.Toastify__bounce-enter--top-right,.Toastify__bounce-enter--bottom-right{animation-name:Toastify__bounceInRight}.Toastify__bounce-enter--top-center{animation-name:Toastify__bounceInDown}.Toastify__bounce-enter--bottom-center{animation-name:Toastify__bounceInUp}.Toastify__bounce-exit--top-left,.Toastify__bounce-exit--bottom-left{animation-name:Toastify__bounceOutLeft}.Toastify__bounce-exit--top-right,.Toastify__bounce-exit--bottom-right{animation-name:Toastify__bounceOutRight}.Toastify__bounce-exit--top-center{animation-name:Toastify__bounceOutUp}.Toastify__bounce-exit--bottom-center{animation-name:Toastify__bounceOutDown}@keyframes Toastify__zoomIn{from{opacity:0;transform:scale3d(0.3, 0.3, 0.3)}50%{opacity:1}}@keyframes Toastify__zoomOut{from{opacity:1}50%{opacity:0;transform:translate3d(0, var(--y), 0) scale3d(0.3, 0.3, 0.3)}to{opacity:0}}.Toastify__zoom-enter{animation-name:Toastify__zoomIn}.Toastify__zoom-exit{animation-name:Toastify__zoomOut}@keyframes Toastify__flipIn{from{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}to{transform:perspective(400px)}}@keyframes Toastify__flipOut{from{transform:translate3d(0, var(--y), 0) perspective(400px)}30%{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, -20deg);opacity:1}to{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, 90deg);opacity:0}}.Toastify__flip-enter{animation-name:Toastify__flipIn}.Toastify__flip-exit{animation-name:Toastify__flipOut}@keyframes Toastify__slideInRight{from{transform:translate3d(110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInLeft{from{transform:translate3d(-110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInUp{from{transform:translate3d(0, 110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInDown{from{transform:translate3d(0, -110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideOutRight{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(110%, var(--y), 0)}}@keyframes Toastify__slideOutLeft{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(-110%, var(--y), 0)}}@keyframes Toastify__slideOutDown{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, 500px, 0)}}@keyframes Toastify__slideOutUp{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, -500px, 0)}}.Toastify__slide-enter--top-left,.Toastify__slide-enter--bottom-left{animation-name:Toastify__slideInLeft}.Toastify__slide-enter--top-right,.Toastify__slide-enter--bottom-right{animation-name:Toastify__slideInRight}.Toastify__slide-enter--top-center{animation-name:Toastify__slideInDown}.Toastify__slide-enter--bottom-center{animation-name:Toastify__slideInUp}.Toastify__slide-exit--top-left,.Toastify__slide-exit--bottom-left{animation-name:Toastify__slideOutLeft;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-right,.Toastify__slide-exit--bottom-right{animation-name:Toastify__slideOutRight;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-center{animation-name:Toastify__slideOutUp;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--bottom-center{animation-name:Toastify__slideOutDown;animation-timing-function:ease-in;animation-duration:.3s}@keyframes Toastify__spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}body{background-color:#000;color:#fff;font-family:Arial,sans-serif;font-size:16px;margin:0 auto;position:relative;height:600px;width:357px}.header{background-color:#9b59b6;color:#fff;padding:10px;display:flex;justify-content:space-between;align-items:center}.header h1{margin:0}.header a{color:#fff}.header a:hover{color:rgba(255,255,255,.7)}.account{position:relative}.account-dropdown{background:#5d5d5d;padding:15px;position:absolute;right:0;z-index:1000}.account-dropdown__item:hover{background:rgba(51,51,51,.7)}.account-dropdown__item.current{background:#333}.account-items{border:1px solid rgba(255,255,255,.6);overflow-y:scroll;height:150px}.account-dropdown__head{align-items:center;display:flex;justify-content:space-between;margin-bottom:10px}.account-dropdown-nav{list-style:none;padding-left:0;margin-bottom:0}.account-dropdown-nav>li{margin-bottom:5px}.account-dropdown-nav>li:last-child{margin-bottom:0}.account-profile{display:flex;flex-direction:row-reverse;align-items:center}.account-profile__body{flex:1;text-align:right}.account-dropdown__item{padding:5px 10px}.account-profile__img{border-radius:50%;display:block;height:35px;margin:0 2px 0 5px;width:35px}.profile{padding:100px 10px 15px;position:relative}.profile__banner{background:url("../../assets/img/store-banner.jpeg");background-position:center;background-size:contain;height:100px;position:absolute;top:0;left:0;width:100%}.profile__img{border-radius:50%;border:3px solid #fff;margin-bottom:10px;width:100px;height:100px}.profile__body{line-height:1.25rem;margin-top:-53px;position:relative}.profile__nav{list-style:none;margin:0;padding:0;position:absolute;right:0;top:62px;text-align:center}.profile__nav>li{display:inline-block;margin-left:10px;width:16px}.profile__nav>li>a{color:#fff;display:block}.Popup .container{margin:0 15px}.Options .container{margin:0 auto;width:600px}.card{background:#111;border-bottom:1px solid #222;padding:15px}.card:first-child{border-top:1px solid #222}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px}.break-string{word-wrap:break-word;white-space:pre-wrap}textarea,input[type=text],input[type=password]{display:block;width:100%}textarea{display:block;width:100%}input[type=file]{display:block;width:100%}label{display:block}.btn{background-color:#9b59b6;color:#fff;display:block;padding:5px 10px;border:none;border-radius:5px;text-align:center;text-decoration:none;transition:background-color .3s ease;width:100%}.btn:hover{background-color:#c39bd3}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;z-index:2}.modal{background-color:#222;color:#fff;padding:15px;border-radius:8px;margin:0 15px;max-width:340px;width:100%}.modal-content{position:relative}.close{position:absolute;top:0;right:0;cursor:pointer}.dropdown{display:flex;position:relative;display:inline-block}.dropdown-btn{display:inline-block;text-align:right;width:25px}.dropdown-content{display:none;position:absolute;right:0;background-color:#f9f9f9;min-width:200px;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);z-index:1}.dropdown-content a{color:#000;padding:10px 15px;text-decoration:none;display:block}.dropdown-content a:hover{background-color:#ddd}.dropdown:hover .dropdown-content{display:block}.tabs{padding-bottom:65px}.foot{background-color:#5d5d5d;position:fixed;bottom:0;left:0;width:100%}.foot-nav{display:flex;font-size:12px;justify-content:space-around;align-items:center;color:#fff;margin:0;padding:15px 10px;text-align:center}.foot-nav i{display:inline-block;font-size:16px;margin-bottom:5px}.foot-nav ul{list-style:none;margin:0;padding:0}.foot-nav li{display:block;width:100%}.foot-nav a{color:#fff;display:block;text-decoration:none}.tabs-nav{padding:10px 10px 0}.tabs-content{padding:10px}.permission-item{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}.permission-item__col:first-child{flex:0 0 70%}.permission-item__col:last-child{flex:0 0 30%;display:flex;justify-content:flex-end} 2 | 3 | /*# sourceMappingURL=style.build.css.map */ -------------------------------------------------------------------------------- /dist/assets/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | --------------------------------------------------------------------------------