Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b6ead1f81 | ||
|
|
38d9a9ef9f | ||
|
|
b55a3f01b6 | ||
|
b7bedf085a
|
|||
|
ff82e41012
|
|||
|
45b1fb58e9
|
@@ -45,10 +45,11 @@ This project uses **standard semver with `v` prefix** (e.g., `v0.0.8`, `v1.2.3`)
|
|||||||
6. **Create release zip files** in the `releases/` folder:
|
6. **Create release zip files** in the `releases/` folder:
|
||||||
```
|
```
|
||||||
mkdir -p releases
|
mkdir -p releases
|
||||||
|
rm -f releases/plebeian-signer-chrome-v*.zip releases/plebeian-signer-firefox-v*.zip
|
||||||
cd dist/chrome && zip -r ../../releases/plebeian-signer-chrome-vX.Y.Z.zip . && cd ../..
|
cd dist/chrome && zip -r ../../releases/plebeian-signer-chrome-vX.Y.Z.zip . && cd ../..
|
||||||
cd dist/firefox && zip -r ../../releases/plebeian-signer-firefox-vX.Y.Z.zip . && cd ../..
|
cd dist/firefox && zip -r ../../releases/plebeian-signer-firefox-vX.Y.Z.zip . && cd ../..
|
||||||
```
|
```
|
||||||
Replace `vX.Y.Z` with the actual version number.
|
Replace `vX.Y.Z` with the actual version number. Old zip files are deleted to keep only the latest release.
|
||||||
|
|
||||||
7. **Compose a commit message** following this format:
|
7. **Compose a commit message** following this format:
|
||||||
- First line: 72 chars max, imperative mood summary (e.g., "Release v0.0.8")
|
- First line: 72 chars max, imperative mood summary (e.g., "Release v0.0.8")
|
||||||
|
|||||||
112
docs/store/PRIVACY_POLICY.md
Normal file
112
docs/store/PRIVACY_POLICY.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Plebeian Signer Privacy Policy
|
||||||
|
|
||||||
|
**Last Updated:** December 20, 2025
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Plebeian Signer is a browser extension for managing Nostr identities and signing cryptographic events. This privacy policy explains how we handle your data.
|
||||||
|
|
||||||
|
## Data Collection
|
||||||
|
|
||||||
|
**We do not collect any personal data.**
|
||||||
|
|
||||||
|
Plebeian Signer operates entirely locally within your browser. We do not:
|
||||||
|
- Collect analytics or telemetry
|
||||||
|
- Track your usage or behavior
|
||||||
|
- Send your data to any external servers
|
||||||
|
- Use cookies or tracking technologies
|
||||||
|
- Share any information with third parties
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
All data is stored locally in your browser using the browser's built-in storage APIs:
|
||||||
|
|
||||||
|
### What We Store Locally
|
||||||
|
|
||||||
|
1. **Encrypted Vault Data**
|
||||||
|
- Your Nostr private keys (encrypted with Argon2id + AES-256-GCM)
|
||||||
|
- Identity nicknames and metadata
|
||||||
|
- Relay configurations
|
||||||
|
- Site permissions
|
||||||
|
|
||||||
|
2. **Session Data**
|
||||||
|
- Temporary decryption keys (cleared when browser closes or vault locks)
|
||||||
|
- Cached profile metadata
|
||||||
|
|
||||||
|
3. **Extension Settings**
|
||||||
|
- Sync preferences
|
||||||
|
- Reckless mode settings
|
||||||
|
- Whitelisted hosts
|
||||||
|
|
||||||
|
### Encryption
|
||||||
|
|
||||||
|
Your private keys are never stored in plaintext. The vault uses:
|
||||||
|
- **Argon2id** for password-based key derivation (256MB memory, 4 threads, 8 iterations)
|
||||||
|
- **AES-256-GCM** for authenticated encryption
|
||||||
|
- **Random salt and IV** generated for each vault
|
||||||
|
|
||||||
|
## Network Communications
|
||||||
|
|
||||||
|
Plebeian Signer makes the following network requests:
|
||||||
|
|
||||||
|
1. **Nostr Relay Connections**
|
||||||
|
- To fetch your profile metadata (kind 0 events)
|
||||||
|
- To fetch relay lists (kind 10002 events)
|
||||||
|
- Only connects to relays you have configured
|
||||||
|
|
||||||
|
2. **NIP-05 Verification**
|
||||||
|
- Fetches `.well-known/nostr.json` from domains in NIP-05 identifiers
|
||||||
|
- Used only to verify identity claims
|
||||||
|
|
||||||
|
**We do not operate any servers.** All relay connections are made directly to the Nostr network.
|
||||||
|
|
||||||
|
## Permissions Explained
|
||||||
|
|
||||||
|
The extension requests these browser permissions:
|
||||||
|
|
||||||
|
- **`storage`**: To save your encrypted vault and settings
|
||||||
|
- **`activeTab`**: To inject the NIP-07 interface into web pages
|
||||||
|
- **`scripting`**: To enable communication between pages and the extension
|
||||||
|
|
||||||
|
## Data Sharing
|
||||||
|
|
||||||
|
We do not share any data with third parties. The extension:
|
||||||
|
- Has no backend servers
|
||||||
|
- Does not use analytics services
|
||||||
|
- Does not include advertising
|
||||||
|
- Does not sell or monetize your data in any way
|
||||||
|
|
||||||
|
## Your Control
|
||||||
|
|
||||||
|
You have full control over your data:
|
||||||
|
- **Export**: You can export your encrypted vault at any time
|
||||||
|
- **Delete**: Use the "Reset Extension" feature to delete all local data
|
||||||
|
- **Lock**: Lock your vault to clear session data immediately
|
||||||
|
|
||||||
|
## Open Source
|
||||||
|
|
||||||
|
Plebeian Signer is open source software. You can audit the code yourself:
|
||||||
|
- Repository: https://git.mleku.dev/mleku/plebeian-signer
|
||||||
|
|
||||||
|
## Children's Privacy
|
||||||
|
|
||||||
|
This extension is not intended for children under 13 years of age. We do not knowingly collect any information from children.
|
||||||
|
|
||||||
|
## Changes to This Policy
|
||||||
|
|
||||||
|
If we make changes to this privacy policy, we will update the "Last Updated" date at the top of this document. Significant changes will be noted in the extension's release notes.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For privacy-related questions or concerns, please open an issue on our repository:
|
||||||
|
https://git.mleku.dev/mleku/plebeian-signer/issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- All data stays in your browser
|
||||||
|
- Private keys are encrypted with strong cryptography
|
||||||
|
- No analytics, tracking, or data collection
|
||||||
|
- No external servers (except Nostr relays you configure)
|
||||||
|
- Fully open source and auditable
|
||||||
293
docs/store/PUBLISHING_GUIDE.md
Normal file
293
docs/store/PUBLISHING_GUIDE.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Extension Store Publishing Guide
|
||||||
|
|
||||||
|
This guide walks you through publishing Plebeian Signer to the Chrome Web Store and Firefox Add-ons.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Assets You Need to Create](#assets-you-need-to-create)
|
||||||
|
2. [Chrome Web Store](#chrome-web-store)
|
||||||
|
3. [Firefox Add-ons](#firefox-add-ons)
|
||||||
|
4. [Ongoing Maintenance](#ongoing-maintenance)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assets You Need to Create
|
||||||
|
|
||||||
|
Before submitting to either store, prepare these assets:
|
||||||
|
|
||||||
|
### Screenshots (Required for both stores)
|
||||||
|
|
||||||
|
Create 3-5 screenshots showing the extension in action:
|
||||||
|
|
||||||
|
1. **Main popup view** - Show the identity card with profile info
|
||||||
|
2. **Permission prompt** - Show a signing request popup
|
||||||
|
3. **Identity management** - Show the identity list/switching
|
||||||
|
4. **Permissions page** - Show the permissions management
|
||||||
|
5. **Settings page** - Show vault settings and options
|
||||||
|
|
||||||
|
**Specifications:**
|
||||||
|
- Chrome: 1280x800 or 640x400 pixels (PNG or JPEG)
|
||||||
|
- Firefox: 1280x800 recommended (PNG or JPEG)
|
||||||
|
|
||||||
|
**Tips:**
|
||||||
|
- Use a clean browser profile
|
||||||
|
- Show realistic data (not "test" or placeholder text)
|
||||||
|
- Capture the full popup or relevant UI area
|
||||||
|
- Consider adding captions/annotations
|
||||||
|
|
||||||
|
### Promotional Images (Chrome only)
|
||||||
|
|
||||||
|
Chrome Web Store uses promotional tiles:
|
||||||
|
|
||||||
|
| Size | Name | Required |
|
||||||
|
|------|------|----------|
|
||||||
|
| 440x280 | Small promo tile | Optional but recommended |
|
||||||
|
| 920x680 | Large promo tile | Optional |
|
||||||
|
| 1400x560 | Marquee promo tile | Optional |
|
||||||
|
|
||||||
|
**Design tips:**
|
||||||
|
- Include the extension icon/logo
|
||||||
|
- Add a tagline like "Secure Nostr Identity Manager"
|
||||||
|
- Use brand colors
|
||||||
|
- Keep text minimal and readable
|
||||||
|
|
||||||
|
### Icon (Already exists)
|
||||||
|
|
||||||
|
You already have icons in the extension:
|
||||||
|
- `icon-48.png` - 48x48
|
||||||
|
- `icon-128.png` - 128x128
|
||||||
|
|
||||||
|
Chrome also wants a 128x128 icon for the store listing (can use the same one).
|
||||||
|
|
||||||
|
### Privacy Policy URL
|
||||||
|
|
||||||
|
You need to host the privacy policy at a public URL. Options:
|
||||||
|
|
||||||
|
1. **GitHub/Gitea Pages** - Host `PRIVACY_POLICY.md` as a webpage
|
||||||
|
2. **Simple webpage** - Create a basic HTML page
|
||||||
|
3. **Gist** - Create a public GitHub gist
|
||||||
|
|
||||||
|
Example URL format: `https://git.mleku.dev/mleku/plebeian-signer/src/branch/main/docs/store/PRIVACY_POLICY.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chrome Web Store
|
||||||
|
|
||||||
|
### Step 1: Create Developer Account
|
||||||
|
|
||||||
|
1. Go to https://chrome.google.com/webstore/devconsole
|
||||||
|
2. Sign in with a Google account
|
||||||
|
3. Pay the one-time $5 USD registration fee
|
||||||
|
4. Accept the developer agreement
|
||||||
|
|
||||||
|
### Step 2: Create New Item
|
||||||
|
|
||||||
|
1. Click **"New Item"** button
|
||||||
|
2. Upload `releases/plebeian-signer-chrome-v1.0.5.zip`
|
||||||
|
3. Wait for the upload to process
|
||||||
|
|
||||||
|
### Step 3: Fill Store Listing
|
||||||
|
|
||||||
|
**Product Details:**
|
||||||
|
- **Name:** Plebeian Signer
|
||||||
|
- **Summary:** Copy from `STORE_DESCRIPTION.md` (short description, 132 chars max)
|
||||||
|
- **Description:** Copy from `STORE_DESCRIPTION.md` (full description)
|
||||||
|
- **Category:** Productivity
|
||||||
|
- **Language:** English
|
||||||
|
|
||||||
|
**Graphic Assets:**
|
||||||
|
- Upload your screenshots (at least 1 required, up to 5)
|
||||||
|
- Upload promotional tiles if you have them
|
||||||
|
|
||||||
|
**Additional Fields:**
|
||||||
|
- **Official URL:** `https://git.mleku.dev/mleku/plebeian-signer`
|
||||||
|
- **Support URL:** `https://git.mleku.dev/mleku/plebeian-signer/issues`
|
||||||
|
|
||||||
|
### Step 4: Privacy Tab
|
||||||
|
|
||||||
|
- **Single Purpose:** "Manage Nostr identities and sign cryptographic events for web applications"
|
||||||
|
- **Permission Justifications:**
|
||||||
|
- `storage`: "Store encrypted vault containing user's Nostr identities and extension settings"
|
||||||
|
- `activeTab`: "Inject NIP-07 interface into the active tab when user visits Nostr applications"
|
||||||
|
- `scripting`: "Enable communication between web pages and the extension for signing requests"
|
||||||
|
- **Data Usage:** Check "I do not sell or transfer user data to third parties"
|
||||||
|
- **Privacy Policy URL:** Your hosted privacy policy URL
|
||||||
|
|
||||||
|
### Step 5: Distribution
|
||||||
|
|
||||||
|
- **Visibility:** Public
|
||||||
|
- **Distribution:** All regions (or select specific ones)
|
||||||
|
|
||||||
|
### Step 6: Submit for Review
|
||||||
|
|
||||||
|
1. Review all sections show green checkmarks
|
||||||
|
2. Click **"Submit for Review"**
|
||||||
|
3. Wait 1-3 business days (can take longer for first submission)
|
||||||
|
|
||||||
|
### Chrome Review Notes
|
||||||
|
|
||||||
|
Google may ask about:
|
||||||
|
- Why you need each permission
|
||||||
|
- How you handle user data
|
||||||
|
- Your identity/organization
|
||||||
|
|
||||||
|
Be prepared to respond to reviewer questions via the dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Firefox Add-ons
|
||||||
|
|
||||||
|
### Step 1: Create Developer Account
|
||||||
|
|
||||||
|
1. Go to https://addons.mozilla.org/developers/
|
||||||
|
2. Sign in with a Firefox account (create one if needed)
|
||||||
|
3. No fee required
|
||||||
|
|
||||||
|
### Step 2: Submit New Add-on
|
||||||
|
|
||||||
|
1. Click **"Submit a New Add-on"**
|
||||||
|
2. Select **"On this site"** for hosting
|
||||||
|
3. Upload `releases/plebeian-signer-firefox-v1.0.5.zip`
|
||||||
|
4. Wait for automated validation
|
||||||
|
|
||||||
|
### Step 3: Source Code Submission
|
||||||
|
|
||||||
|
Firefox may request source code because the extension uses bundled/minified JavaScript.
|
||||||
|
|
||||||
|
**If prompted:**
|
||||||
|
1. Create a source code zip (exclude `node_modules`):
|
||||||
|
```bash
|
||||||
|
cd /home/mleku/src/git.mleku.dev/mleku/plebeian-signer
|
||||||
|
zip -r plebeian-signer-source.zip . -x "node_modules/*" -x "dist/*" -x ".git/*"
|
||||||
|
```
|
||||||
|
2. Upload this zip when asked
|
||||||
|
3. Include build instructions (point to CLAUDE.md or add a note):
|
||||||
|
```
|
||||||
|
Build Instructions:
|
||||||
|
1. npm ci
|
||||||
|
2. npm run build:firefox
|
||||||
|
3. Output is in dist/firefox/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Fill Listing Details
|
||||||
|
|
||||||
|
**Basic Information:**
|
||||||
|
- **Name:** Plebeian Signer
|
||||||
|
- **Add-on URL:** `plebeian-signer` (creates addons.mozilla.org/addon/plebeian-signer)
|
||||||
|
- **Summary:** Copy short description from `STORE_DESCRIPTION.md`
|
||||||
|
- **Description:** Copy full description (supports some HTML/Markdown)
|
||||||
|
- **Categories:** Privacy & Security
|
||||||
|
|
||||||
|
**Additional Details:**
|
||||||
|
- **Homepage:** `https://git.mleku.dev/mleku/plebeian-signer`
|
||||||
|
- **Support URL:** `https://git.mleku.dev/mleku/plebeian-signer/issues`
|
||||||
|
- **License:** Select appropriate license
|
||||||
|
- **Privacy Policy:** Paste URL to hosted privacy policy
|
||||||
|
|
||||||
|
**Media:**
|
||||||
|
- **Icon:** Already in the extension manifest
|
||||||
|
- **Screenshots:** Upload your screenshots
|
||||||
|
|
||||||
|
### Step 5: Submit for Review
|
||||||
|
|
||||||
|
1. Ensure all required fields are complete
|
||||||
|
2. Click **"Submit Version"**
|
||||||
|
3. Wait for review (usually hours to a few days)
|
||||||
|
|
||||||
|
### Firefox Review Notes
|
||||||
|
|
||||||
|
Firefox reviewers are generally faster but thorough. They may:
|
||||||
|
- Ask for source code (see Step 3)
|
||||||
|
- Question specific code patterns
|
||||||
|
- Request changes for policy compliance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ongoing Maintenance
|
||||||
|
|
||||||
|
### Updating the Extension
|
||||||
|
|
||||||
|
**For new releases:**
|
||||||
|
|
||||||
|
1. Build new version: `/release patch` (or `minor`/`major`)
|
||||||
|
2. Upload the new zip to each store
|
||||||
|
3. Add release notes describing changes
|
||||||
|
4. Submit for review
|
||||||
|
|
||||||
|
**Chrome:**
|
||||||
|
- Go to Developer Dashboard → Your extension → Package → Upload new package
|
||||||
|
|
||||||
|
**Firefox:**
|
||||||
|
- Go to Developer Hub → Your extension → Upload a New Version
|
||||||
|
|
||||||
|
### Responding to Reviews
|
||||||
|
|
||||||
|
Both stores may contact you with:
|
||||||
|
- Policy violation notices
|
||||||
|
- User reports
|
||||||
|
- Review questions
|
||||||
|
|
||||||
|
Monitor your developer email and respond promptly.
|
||||||
|
|
||||||
|
### Version Numbering
|
||||||
|
|
||||||
|
Both stores extract the version from `manifest.json`. Your current setup with `v1.0.5` in `package.json` feeds into the manifests correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
### Before First Submission
|
||||||
|
|
||||||
|
- [ ] Create 3-5 screenshots
|
||||||
|
- [ ] Create promotional images (Chrome, optional but recommended)
|
||||||
|
- [ ] Host privacy policy at a public URL
|
||||||
|
- [ ] Test the extension zip by loading it unpacked
|
||||||
|
- [ ] Prepare source code zip for Firefox
|
||||||
|
|
||||||
|
### Chrome Web Store
|
||||||
|
|
||||||
|
- [ ] Register developer account ($5)
|
||||||
|
- [ ] Upload extension zip
|
||||||
|
- [ ] Fill all required listing fields
|
||||||
|
- [ ] Add screenshots
|
||||||
|
- [ ] Add privacy policy URL
|
||||||
|
- [ ] Justify all permissions
|
||||||
|
- [ ] Submit for review
|
||||||
|
|
||||||
|
### Firefox Add-ons
|
||||||
|
|
||||||
|
- [ ] Register developer account (free)
|
||||||
|
- [ ] Upload extension zip
|
||||||
|
- [ ] Upload source code if requested
|
||||||
|
- [ ] Fill all required listing fields
|
||||||
|
- [ ] Add screenshots
|
||||||
|
- [ ] Add privacy policy URL
|
||||||
|
- [ ] Submit for review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Helpful Links
|
||||||
|
|
||||||
|
- Chrome Developer Dashboard: https://chrome.google.com/webstore/devconsole
|
||||||
|
- Chrome Publishing Docs: https://developer.chrome.com/docs/webstore/publish/
|
||||||
|
- Firefox Developer Hub: https://addons.mozilla.org/developers/
|
||||||
|
- Firefox Extension Workshop: https://extensionworkshop.com/documentation/publish/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estimated Timeline
|
||||||
|
|
||||||
|
| Task | Time |
|
||||||
|
|------|------|
|
||||||
|
| Create screenshots | 30 min - 1 hour |
|
||||||
|
| Create promotional images | 1-2 hours (optional) |
|
||||||
|
| Host privacy policy | 15 min |
|
||||||
|
| Chrome submission | 30 min |
|
||||||
|
| Chrome review | 1-3 business days |
|
||||||
|
| Firefox submission | 30 min |
|
||||||
|
| Firefox review | Hours to 2 days |
|
||||||
|
|
||||||
|
**Total:** You can have both submissions done in an afternoon, with approvals coming within a week.
|
||||||
88
docs/store/STORE_DESCRIPTION.md
Normal file
88
docs/store/STORE_DESCRIPTION.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Plebeian Signer - Store Description
|
||||||
|
|
||||||
|
Use this content for Chrome Web Store and Firefox Add-ons listings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Short Description (132 characters max for Chrome)
|
||||||
|
|
||||||
|
Secure Nostr identity manager. Sign events without exposing private keys. Multi-identity support with NIP-07 compatibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full Description
|
||||||
|
|
||||||
|
**Plebeian Signer** is a secure browser extension for managing your Nostr identities and signing events without exposing your private keys to web applications.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
**Multi-Identity Management**
|
||||||
|
- Create and manage multiple Nostr identities from a single extension
|
||||||
|
- Easily switch between identities with one click
|
||||||
|
- Import existing keys or generate new ones
|
||||||
|
|
||||||
|
**Bank-Grade Security**
|
||||||
|
- Private keys never leave the extension
|
||||||
|
- Vault encrypted with Argon2id + AES-256-GCM (the same algorithms used by password managers)
|
||||||
|
- Automatic vault locking for protection
|
||||||
|
|
||||||
|
**NIP-07 Compatible**
|
||||||
|
- Works with all Nostr web applications that support NIP-07
|
||||||
|
- Supports NIP-04 and NIP-44 encryption/decryption
|
||||||
|
- Relay configuration per identity
|
||||||
|
|
||||||
|
**Permission Control**
|
||||||
|
- Fine-grained permission management per application
|
||||||
|
- Approve or deny signing requests on a per-site basis
|
||||||
|
- Optional "Reckless Mode" for trusted applications
|
||||||
|
- Whitelist trusted hosts for automatic approval
|
||||||
|
|
||||||
|
**User-Friendly Interface**
|
||||||
|
- Clean, intuitive design
|
||||||
|
- Profile metadata display with avatar and banner
|
||||||
|
- NIP-05 verification support
|
||||||
|
- Bookmark your favorite Nostr apps
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. Create a password-protected vault
|
||||||
|
2. Add your Nostr identities (import existing or generate new)
|
||||||
|
3. Visit any NIP-07 compatible Nostr application
|
||||||
|
4. Approve signing requests through the extension popup
|
||||||
|
|
||||||
|
### Privacy First
|
||||||
|
|
||||||
|
Plebeian Signer is open source and respects your privacy:
|
||||||
|
- No telemetry or analytics
|
||||||
|
- No external servers (except for profile metadata from Nostr relays)
|
||||||
|
- All cryptographic operations happen locally in your browser
|
||||||
|
- Your private keys are encrypted and never transmitted
|
||||||
|
|
||||||
|
### Supported NIPs
|
||||||
|
|
||||||
|
- NIP-07: Browser Extension for Nostr
|
||||||
|
- NIP-04: Encrypted Direct Messages
|
||||||
|
- NIP-44: Versioned Encryption
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
- Source Code: https://git.mleku.dev/mleku/plebeian-signer
|
||||||
|
- Report Issues: https://git.mleku.dev/mleku/plebeian-signer/issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Category Suggestions
|
||||||
|
|
||||||
|
**Chrome Web Store:**
|
||||||
|
- Primary: Productivity
|
||||||
|
- Secondary: Developer Tools
|
||||||
|
|
||||||
|
**Firefox Add-ons:**
|
||||||
|
- Primary: Privacy & Security
|
||||||
|
- Secondary: Other
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tags/Keywords
|
||||||
|
|
||||||
|
nostr, nip-07, signing, identity, privacy, encryption, decentralized, keys, wallet, security
|
||||||
129
docs/store/publishing.md
Normal file
129
docs/store/publishing.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Publishing Checklist
|
||||||
|
|
||||||
|
Developer accounts are set up. This document covers the remaining steps.
|
||||||
|
|
||||||
|
## Privacy Policy URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.mleku.dev/mleku/plebeian-signer/src/branch/main/docs/store/PRIVACY_POLICY.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots Needed
|
||||||
|
|
||||||
|
Take 3-5 screenshots (1280x800 or 640x400 PNG/JPEG):
|
||||||
|
|
||||||
|
1. **Identity view** - Main popup showing profile card with avatar/banner
|
||||||
|
2. **Permission prompt** - A signing request popup from a Nostr app
|
||||||
|
3. **Identity list** - Multiple identities with switching UI
|
||||||
|
4. **Permissions page** - Managing site permissions
|
||||||
|
5. **Settings** - Vault/reckless mode settings
|
||||||
|
|
||||||
|
**Tips:**
|
||||||
|
- Load the extension in a clean browser profile
|
||||||
|
- Use real-looking test data, not "test123"
|
||||||
|
- Crop to show just the popup/relevant UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chrome Web Store Submission
|
||||||
|
|
||||||
|
1. Go to https://chrome.google.com/webstore/devconsole
|
||||||
|
2. Click **"New Item"**
|
||||||
|
3. Upload: `releases/plebeian-signer-chrome-v1.0.5.zip`
|
||||||
|
|
||||||
|
### Store Listing Tab
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Name | Plebeian Signer |
|
||||||
|
| Summary | Secure Nostr identity manager. Sign events without exposing private keys. Multi-identity support with NIP-07 compatibility. |
|
||||||
|
| Description | Copy from `docs/store/STORE_DESCRIPTION.md` (full description section) |
|
||||||
|
| Category | Productivity |
|
||||||
|
| Language | English |
|
||||||
|
|
||||||
|
Upload your screenshots.
|
||||||
|
|
||||||
|
### Privacy Tab
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Single Purpose | Manage Nostr identities and sign cryptographic events for web applications |
|
||||||
|
| Privacy Policy URL | `https://git.mleku.dev/mleku/plebeian-signer/src/branch/main/docs/store/PRIVACY_POLICY.md` |
|
||||||
|
|
||||||
|
**Permission Justifications:**
|
||||||
|
|
||||||
|
| Permission | Justification |
|
||||||
|
|------------|---------------|
|
||||||
|
| storage | Store encrypted vault containing user's Nostr identities and extension settings |
|
||||||
|
| activeTab | Inject NIP-07 interface into the active tab when user visits Nostr applications |
|
||||||
|
| scripting | Enable communication between web pages and the extension for signing requests |
|
||||||
|
|
||||||
|
Check: "I do not sell or transfer user data to third parties"
|
||||||
|
|
||||||
|
### Distribution Tab
|
||||||
|
|
||||||
|
- Visibility: Public
|
||||||
|
- Regions: All
|
||||||
|
|
||||||
|
Click **"Submit for Review"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Firefox Add-ons Submission
|
||||||
|
|
||||||
|
1. Go to https://addons.mozilla.org/developers/
|
||||||
|
2. Click **"Submit a New Add-on"**
|
||||||
|
3. Select **"On this site"**
|
||||||
|
4. Upload: `releases/plebeian-signer-firefox-v1.0.5.zip`
|
||||||
|
|
||||||
|
### If Asked for Source Code
|
||||||
|
|
||||||
|
Run this to create source zip:
|
||||||
|
```bash
|
||||||
|
cd /home/mleku/src/git.mleku.dev/mleku/plebeian-signer
|
||||||
|
zip -r plebeian-signer-source.zip . -x "node_modules/*" -x "dist/*" -x ".git/*" -x "releases/*"
|
||||||
|
```
|
||||||
|
|
||||||
|
Build instructions to provide:
|
||||||
|
```
|
||||||
|
1. npm ci
|
||||||
|
2. npm run build:firefox
|
||||||
|
3. Output is in dist/firefox/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listing Details
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Name | Plebeian Signer |
|
||||||
|
| Add-on URL | plebeian-signer |
|
||||||
|
| Summary | Secure Nostr identity manager. Sign events without exposing private keys. Multi-identity support with NIP-07 compatibility. |
|
||||||
|
| Description | Copy from `docs/store/STORE_DESCRIPTION.md` |
|
||||||
|
| Categories | Privacy & Security |
|
||||||
|
| Homepage | `https://git.mleku.dev/mleku/plebeian-signer` |
|
||||||
|
| Support URL | `https://git.mleku.dev/mleku/plebeian-signer/issues` |
|
||||||
|
| Privacy Policy | `https://git.mleku.dev/mleku/plebeian-signer/src/branch/main/docs/store/PRIVACY_POLICY.md` |
|
||||||
|
|
||||||
|
Upload your screenshots.
|
||||||
|
|
||||||
|
Click **"Submit Version"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Submission
|
||||||
|
|
||||||
|
- **Chrome:** 1-3 business days review
|
||||||
|
- **Firefox:** Hours to 2 days review
|
||||||
|
|
||||||
|
Check your email for reviewer questions. Both dashboards show review status.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating Later
|
||||||
|
|
||||||
|
When you release a new version:
|
||||||
|
|
||||||
|
1. Run `/release patch` (or minor/major)
|
||||||
|
2. Chrome: Dashboard → Your extension → Package → Upload new package
|
||||||
|
3. Firefox: Developer Hub → Your extension → Upload a New Version
|
||||||
|
4. Add release notes, submit for review
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "plebeian-signer",
|
"name": "plebeian-signer",
|
||||||
"version": "v1.0.3",
|
"version": "v1.0.6",
|
||||||
"custom": {
|
"custom": {
|
||||||
"chrome": {
|
"chrome": {
|
||||||
"version": "v1.0.3"
|
"version": "v1.0.6"
|
||||||
},
|
},
|
||||||
"firefox": {
|
"firefox": {
|
||||||
"version": "v1.0.3"
|
"version": "v1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
||||||
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
||||||
"version": "1.0.3",
|
"version": "1.0.6",
|
||||||
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { IdentityComponent } from './components/home/identity/identity.component
|
|||||||
import { InfoComponent } from './components/home/info/info.component';
|
import { InfoComponent } from './components/home/info/info.component';
|
||||||
import { SettingsComponent } from './components/home/settings/settings.component';
|
import { SettingsComponent } from './components/home/settings/settings.component';
|
||||||
import { LogsComponent } from './components/home/logs/logs.component';
|
import { LogsComponent } from './components/home/logs/logs.component';
|
||||||
|
import { BookmarksComponent } from './components/home/bookmarks/bookmarks.component';
|
||||||
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
|
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
|
||||||
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
||||||
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
||||||
@@ -71,6 +72,10 @@ export const routes: Routes = [
|
|||||||
path: 'logs',
|
path: 'logs',
|
||||||
component: LogsComponent,
|
component: LogsComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'bookmarks',
|
||||||
|
component: BookmarksComponent,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-banner">
|
<div class="info-banner">
|
||||||
<i class="bi bi-info-circle"></i>
|
<span class="emoji">💡</span>
|
||||||
<span>These relays are fetched from your NIP-65 relay list (kind 10002). To update your relay list, use a Nostr client that supports NIP-65.</span>
|
<span>These relays are fetched from your NIP-65 relay list (kind 10002). To update your relay list, use a Nostr client that supports NIP-65.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
|
||||||
|
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
|
||||||
|
<div class="bookmarks-header">
|
||||||
|
<span class="bookmarks-title">Bookmarks</span>
|
||||||
|
<button class="btn btn-primary btn-sm" (click)="onBookmarkThisPage()">
|
||||||
|
<span class="emoji">🔖</span> Bookmark This Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bookmarks-container">
|
||||||
|
@if (isLoading) {
|
||||||
|
<div class="loading-state">Loading...</div>
|
||||||
|
} @else if (bookmarks.length === 0) {
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="sam-text-muted">
|
||||||
|
No bookmarks yet. Click "Bookmark This Page" to add the current page.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
@for (bookmark of bookmarks; track bookmark.id) {
|
||||||
|
<div class="bookmark-item" (click)="openBookmark(bookmark)">
|
||||||
|
<div class="bookmark-info">
|
||||||
|
<span class="bookmark-title">{{ bookmark.title }}</span>
|
||||||
|
<span class="bookmark-url">{{ getDomain(bookmark.url) }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="remove-btn"
|
||||||
|
title="Remove bookmark"
|
||||||
|
(click)="onRemoveBookmark(bookmark); $event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<span class="emoji">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--size);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state,
|
||||||
|
.loading-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-h);
|
||||||
|
padding: var(--size-h) var(--size);
|
||||||
|
margin-bottom: var(--size-hh);
|
||||||
|
background: var(--background-light);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-light-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-url {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--destructive);
|
||||||
|
color: var(--destructive-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { Bookmark, LoggerService, SignerMetaData } from '@common';
|
||||||
|
import { ChromeMetaHandler } from '../../../common/data/chrome-meta-handler';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bookmarks',
|
||||||
|
templateUrl: './bookmarks.component.html',
|
||||||
|
styleUrl: './bookmarks.component.scss',
|
||||||
|
imports: [],
|
||||||
|
})
|
||||||
|
export class BookmarksComponent implements OnInit {
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
readonly #metaHandler = new ChromeMetaHandler();
|
||||||
|
|
||||||
|
bookmarks: Bookmark[] = [];
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.loadBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBookmarks() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const metaData = await this.#metaHandler.loadFullData() as SignerMetaData;
|
||||||
|
this.#metaHandler.setFullData(metaData);
|
||||||
|
this.bookmarks = this.#metaHandler.getBookmarks();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load bookmarks:', error);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBookmarkThisPage() {
|
||||||
|
try {
|
||||||
|
// Get the current tab URL and title
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab?.url || !tab?.title) {
|
||||||
|
console.error('Could not get current tab info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already bookmarked
|
||||||
|
if (this.bookmarks.some(b => b.url === tab.url)) {
|
||||||
|
console.log('Page already bookmarked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBookmark: Bookmark = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
url: tab.url,
|
||||||
|
title: tab.title,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bookmarks = [newBookmark, ...this.bookmarks];
|
||||||
|
await this.saveBookmarks();
|
||||||
|
this.#logger.logBookmarkAdded(newBookmark.url, newBookmark.title);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to bookmark page:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRemoveBookmark(bookmark: Bookmark) {
|
||||||
|
this.bookmarks = this.bookmarks.filter(b => b.id !== bookmark.id);
|
||||||
|
await this.saveBookmarks();
|
||||||
|
this.#logger.logBookmarkRemoved(bookmark.url, bookmark.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBookmarks() {
|
||||||
|
try {
|
||||||
|
await this.#metaHandler.setBookmarks(this.bookmarks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save bookmarks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openBookmark(bookmark: Bookmark) {
|
||||||
|
chrome.tabs.create({ url: bookmark.url });
|
||||||
|
}
|
||||||
|
|
||||||
|
getDomain(url: string): string {
|
||||||
|
try {
|
||||||
|
return new URL(url).hostname;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
title="Your selected identity"
|
title="Your selected identity"
|
||||||
>
|
>
|
||||||
<i class="bi bi-person-circle"></i>
|
<span class="emoji">👤</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
title="Identities"
|
title="Identities"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people-fill"></i>
|
<span class="emoji">👥</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -27,14 +27,22 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
title="Settings"
|
title="Settings"
|
||||||
>
|
>
|
||||||
<i class="bi bi-gear"></i>
|
<span class="emoji">⚙️</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
|
<a class="tab" routerLink="/home/bookmarks" routerLinkActive="active" title="Bookmarks">
|
||||||
<i class="bi bi-info-circle"></i>
|
<span class="emoji">🔖</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="tab" routerLink="/home/logs" routerLinkActive="active" title="Logs">
|
<a class="tab" routerLink="/home/logs" routerLinkActive="active" title="Logs">
|
||||||
<span style="font-size: 1.2rem">🪵</span>
|
<span class="emoji">🪵</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
|
||||||
|
<span class="emoji">💡</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="tab" (click)="onClickLock()" title="Lock">
|
||||||
|
<span class="emoji">🔒</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
height: 60px;
|
height: 40px;
|
||||||
min-height: 60px;
|
min-height: 40px;
|
||||||
background: var(--background-light);
|
background: var(--background-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
a {
|
a, button {
|
||||||
all: unset;
|
all: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
|
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
border-top: 3px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
border-top: 3px solid var(--primary);
|
border-top: 2px solid var(--primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterModule, RouterOutlet } from '@angular/router';
|
import { Router, RouterModule, RouterOutlet } from '@angular/router';
|
||||||
|
import { LoggerService, StorageService } from '@common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -7,4 +8,14 @@ import { RouterModule, RouterOutlet } from '@angular/router';
|
|||||||
styleUrl: './home.component.scss',
|
styleUrl: './home.component.scss',
|
||||||
imports: [RouterOutlet, RouterModule],
|
imports: [RouterOutlet, RouterModule],
|
||||||
})
|
})
|
||||||
export class HomeComponent {}
|
export class HomeComponent {
|
||||||
|
readonly #storage = inject(StorageService);
|
||||||
|
readonly #router = inject(Router);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
|
async onClickLock() {
|
||||||
|
this.#logger.logVaultLock();
|
||||||
|
await this.#storage.lockVault();
|
||||||
|
this.#router.navigateByUrl('/vault-login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
title="Manage whitelisted apps"
|
title="Manage whitelisted apps"
|
||||||
(click)="onClickWhitelistedApps()"
|
(click)="onClickWhitelistedApps()"
|
||||||
>
|
>
|
||||||
<i class="bi bi-gear"></i>
|
<span class="emoji">⚙️</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
/>
|
/>
|
||||||
<span class="name">{{ getDisplayName(identity) }}</span>
|
<span class="name">{{ getDisplayName(identity) }}</span>
|
||||||
<lib-icon-button
|
<lib-icon-button
|
||||||
icon="gear"
|
icon="⚙️"
|
||||||
title="Identity settings"
|
title="Identity settings"
|
||||||
(click)="onClickEditIdentity(identity.id, $event)"
|
(click)="onClickEditIdentity(identity.id, $event)"
|
||||||
></lib-icon-button>
|
></lib-icon-button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, inject, OnInit } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
Identity_DECRYPTED,
|
Identity_DECRYPTED,
|
||||||
|
LoggerService,
|
||||||
NostrHelper,
|
NostrHelper,
|
||||||
ProfileMetadata,
|
ProfileMetadata,
|
||||||
ProfileMetadataService,
|
ProfileMetadataService,
|
||||||
@@ -29,6 +30,7 @@ export class IdentityComponent implements OnInit {
|
|||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.#loadData();
|
this.#loadData();
|
||||||
@@ -136,13 +138,16 @@ export class IdentityComponent implements OnInit {
|
|||||||
const result = await validateNip05(nip05, pubkey);
|
const result = await validateNip05(nip05, pubkey);
|
||||||
this.nip05isValidated = result.valid;
|
this.nip05isValidated = result.valid;
|
||||||
|
|
||||||
if (!result.valid) {
|
if (result.valid) {
|
||||||
console.log('NIP-05 validation failed:', result.error);
|
this.#logger.logNip05ValidationSuccess(nip05, pubkey);
|
||||||
|
} else {
|
||||||
|
this.#logger.logNip05ValidationError(nip05, result.error ?? 'Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validating = false;
|
this.validating = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('NIP-05 validation failed:', error);
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.#logger.logNip05ValidationError(nip05, errorMsg);
|
||||||
this.nip05isValidated = false;
|
this.nip05isValidated = false;
|
||||||
this.validating = false;
|
this.validating = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<div class="logs-header">
|
<div class="logs-header">
|
||||||
<span class="logs-title">Logs</span>
|
<span class="logs-title">Logs</span>
|
||||||
<button class="btn btn-sm btn-secondary" (click)="onClear()">Clear Log</button>
|
<div class="logs-actions">
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="onRefresh()">Refresh</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="onClear()">Clear</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="logs-container">
|
<div class="logs-container">
|
||||||
@if (logs.length === 0) {
|
@if (logs.length === 0) {
|
||||||
<div class="logs-empty">No logs yet</div>
|
<div class="logs-empty">No activity logged yet</div>
|
||||||
}
|
}
|
||||||
@for (log of logs; track log.timestamp) {
|
@for (log of logs; track log.timestamp) {
|
||||||
<div class="log-entry" [class]="getLevelClass(log.level)">
|
<div class="log-entry" [class]="getLevelClass(log.level)">
|
||||||
|
<span class="log-icon emoji">{{ log.icon }}</span>
|
||||||
<span class="log-time">{{ log.timestamp | date:'HH:mm:ss' }}</span>
|
<span class="log-time">{{ log.timestamp | date:'HH:mm:ss' }}</span>
|
||||||
<span class="log-level">{{ log.level }}</span>
|
|
||||||
<span class="log-message">{{ log.message }}</span>
|
<span class="log-message">{{ log.message }}</span>
|
||||||
@if (log.data) {
|
|
||||||
<pre class="log-data">{{ log.data | json }}</pre>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-title {
|
.logs-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -34,15 +39,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
.log-entry {
|
||||||
font-family: monospace;
|
font-family: var(--font-sans);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 4px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
|
|
||||||
&.log-error {
|
&.log-error {
|
||||||
background: rgba(220, 53, 69, 0.15);
|
background: rgba(220, 53, 69, 0.15);
|
||||||
@@ -65,29 +69,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.log-time {
|
.log-time {
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
font-size: 10px;
|
||||||
|
|
||||||
.log-level {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
width: 40px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-message {
|
.log-message {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-data {
|
|
||||||
width: 100%;
|
|
||||||
margin: 4px 0 0 0;
|
|
||||||
padding: 4px;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { LoggerService, LogEntry } from '@common';
|
import { LoggerService, LogEntry } from '@common';
|
||||||
import { DatePipe, JsonPipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-logs',
|
selector: 'app-logs',
|
||||||
templateUrl: './logs.component.html',
|
templateUrl: './logs.component.html',
|
||||||
styleUrl: './logs.component.scss',
|
styleUrl: './logs.component.scss',
|
||||||
imports: [DatePipe, JsonPipe],
|
imports: [DatePipe],
|
||||||
})
|
})
|
||||||
export class LogsComponent {
|
export class LogsComponent implements OnInit {
|
||||||
readonly #logger = inject(LoggerService);
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
get logs(): LogEntry[] {
|
get logs(): LogEntry[] {
|
||||||
return this.#logger.logs;
|
return this.#logger.logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClear() {
|
ngOnInit() {
|
||||||
this.#logger.clear();
|
// Refresh logs from storage to get background script logs
|
||||||
|
this.#logger.refreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRefresh() {
|
||||||
|
await this.#logger.refreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClear() {
|
||||||
|
await this.#logger.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
getLevelClass(level: LogEntry['level']): string {
|
getLevelClass(level: LogEntry['level']): string {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
BrowserSyncFlow,
|
BrowserSyncFlow,
|
||||||
ConfirmComponent,
|
ConfirmComponent,
|
||||||
DateHelper,
|
DateHelper,
|
||||||
|
LoggerService,
|
||||||
NavComponent,
|
NavComponent,
|
||||||
StartupService,
|
StartupService,
|
||||||
StorageService,
|
StorageService,
|
||||||
@@ -21,6 +22,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
readonly #startup = inject(StartupService);
|
readonly #startup = inject(StartupService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const vault = JSON.stringify(
|
const vault = JSON.stringify(
|
||||||
@@ -44,6 +46,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
async onResetExtension() {
|
async onResetExtension() {
|
||||||
try {
|
try {
|
||||||
|
this.#logger.logVaultReset();
|
||||||
await this.#storage.resetExtension();
|
await this.#storage.resetExtension();
|
||||||
this.#startup.startOver(getNewStorageServiceConfig());
|
this.#startup.startOver(getNewStorageServiceConfig());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,6 +72,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
await this.#storage.deleteVault(true);
|
await this.#storage.deleteVault(true);
|
||||||
await this.#storage.importVault(vault);
|
await this.#storage.importVault(vault);
|
||||||
|
this.#logger.logVaultImport(file.name);
|
||||||
this.#storage.isInitialized = false;
|
this.#storage.isInitialized = false;
|
||||||
this.#startup.startOver(getNewStorageServiceConfig());
|
this.#startup.startOver(getNewStorageServiceConfig());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -84,6 +88,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
const fileName = `Plebeian Signer Chrome - Vault Export - ${dateTimeString}.json`;
|
const fileName = `Plebeian Signer Chrome - Vault Export - ${dateTimeString}.json`;
|
||||||
|
|
||||||
this.#downloadJson(jsonVault, fileName);
|
this.#downloadJson(jsonVault, fileName);
|
||||||
|
this.#logger.logVaultExport(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
#downloadJson(jsonString: string, fileName: string) {
|
#downloadJson(jsonString: string, fileName: string) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, inject, ViewChild } from '@angular/core';
|
import { Component, inject, ViewChild } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NavComponent, StorageService, DerivingModalComponent } from '@common';
|
import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-new',
|
selector: 'app-new',
|
||||||
@@ -16,6 +16,7 @@ export class NewComponent extends NavComponent {
|
|||||||
|
|
||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
toggleType(element: HTMLInputElement) {
|
toggleType(element: HTMLInputElement) {
|
||||||
if (element.type === 'password') {
|
if (element.type === 'password') {
|
||||||
@@ -35,6 +36,7 @@ export class NewComponent extends NavComponent {
|
|||||||
try {
|
try {
|
||||||
await this.#storage.createNewVault(this.password);
|
await this.#storage.createNewVault(this.password);
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
|
this.#logger.logVaultCreated();
|
||||||
this.#router.navigateByUrl('/home/identities');
|
this.#router.navigateByUrl('/home/identities');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
|||||||
import {
|
import {
|
||||||
ConfirmComponent,
|
ConfirmComponent,
|
||||||
DerivingModalComponent,
|
DerivingModalComponent,
|
||||||
|
LoggerService,
|
||||||
NostrHelper,
|
NostrHelper,
|
||||||
ProfileMetadataService,
|
ProfileMetadataService,
|
||||||
StartupService,
|
StartupService,
|
||||||
@@ -28,6 +29,7 @@ export class VaultLoginComponent implements AfterViewInit {
|
|||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
readonly #startup = inject(StartupService);
|
readonly #startup = inject(StartupService);
|
||||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.passwordInput.nativeElement.focus();
|
this.passwordInput.nativeElement.focus();
|
||||||
@@ -69,6 +71,7 @@ export class VaultLoginComponent implements AfterViewInit {
|
|||||||
// Unlock succeeded - hide modal and navigate
|
// Unlock succeeded - hide modal and navigate
|
||||||
console.log('[login] Hiding modal and navigating');
|
console.log('[login] Hiding modal and navigating');
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
|
this.#logger.logVaultUnlock();
|
||||||
|
|
||||||
// Fetch profile metadata for all identities in the background
|
// Fetch profile metadata for all identities in the background
|
||||||
this.#fetchAllProfiles();
|
this.#fetchAllProfiles();
|
||||||
@@ -102,6 +105,7 @@ export class VaultLoginComponent implements AfterViewInit {
|
|||||||
|
|
||||||
async onClickResetExtension() {
|
async onClickResetExtension() {
|
||||||
try {
|
try {
|
||||||
|
this.#logger.logVaultReset();
|
||||||
await this.#storage.resetExtension();
|
await this.#storage.resetExtension();
|
||||||
this.#startup.startOver(getNewStorageServiceConfig());
|
this.#startup.startOver(getNewStorageServiceConfig());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { NostrHelper } from '@common';
|
import {
|
||||||
|
backgroundLogNip07Action,
|
||||||
|
backgroundLogPermissionStored,
|
||||||
|
NostrHelper,
|
||||||
|
} from '@common';
|
||||||
import {
|
import {
|
||||||
BackgroundRequestMessage,
|
BackgroundRequestMessage,
|
||||||
checkPermissions,
|
checkPermissions,
|
||||||
@@ -109,17 +113,28 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
|||||||
});
|
});
|
||||||
debug(response);
|
debug(response);
|
||||||
if (response === 'approve' || response === 'reject') {
|
if (response === 'approve' || response === 'reject') {
|
||||||
|
const policy = response === 'approve' ? 'allow' : 'deny';
|
||||||
await storePermission(
|
await storePermission(
|
||||||
browserSessionData,
|
browserSessionData,
|
||||||
currentIdentity,
|
currentIdentity,
|
||||||
req.host,
|
req.host,
|
||||||
req.method,
|
req.method,
|
||||||
response === 'approve' ? 'allow' : 'deny',
|
policy,
|
||||||
|
req.params?.kind
|
||||||
|
);
|
||||||
|
await backgroundLogPermissionStored(
|
||||||
|
req.host,
|
||||||
|
req.method,
|
||||||
|
policy,
|
||||||
req.params?.kind
|
req.params?.kind
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['reject', 'reject-once'].includes(response)) {
|
if (['reject', 'reject-once'].includes(response)) {
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, false, false, {
|
||||||
|
kind: req.params?.kind,
|
||||||
|
peerPubkey: req.params?.peerPubkey,
|
||||||
|
});
|
||||||
throw new Error('Permission denied');
|
throw new Error('Permission denied');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -128,46 +143,71 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relays: Relays = {};
|
const relays: Relays = {};
|
||||||
|
let result: any;
|
||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case 'getPublicKey':
|
case 'getPublicKey':
|
||||||
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
result = NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'signEvent':
|
case 'signEvent':
|
||||||
return signEvent(req.params, currentIdentity.privkey);
|
result = signEvent(req.params, currentIdentity.privkey);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
kind: req.params?.kind,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'getRelays':
|
case 'getRelays':
|
||||||
browserSessionData.relays.forEach((x) => {
|
browserSessionData.relays.forEach((x) => {
|
||||||
relays[x.url] = { read: x.read, write: x.write };
|
relays[x.url] = { read: x.read, write: x.write };
|
||||||
});
|
});
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||||
return relays;
|
return relays;
|
||||||
|
|
||||||
case 'nip04.encrypt':
|
case 'nip04.encrypt':
|
||||||
return await nip04Encrypt(
|
result = await nip04Encrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.plaintext
|
req.params.plaintext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'nip44.encrypt':
|
case 'nip44.encrypt':
|
||||||
return await nip44Encrypt(
|
result = await nip44Encrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.plaintext
|
req.params.plaintext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'nip04.decrypt':
|
case 'nip04.decrypt':
|
||||||
return await nip04Decrypt(
|
result = await nip04Decrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.ciphertext
|
req.params.ciphertext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'nip44.decrypt':
|
case 'nip44.decrypt':
|
||||||
return await nip44Decrypt(
|
result = await nip44Decrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.ciphertext
|
req.params.ciphertext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Not supported request method '${req.method}'.`);
|
throw new Error(`Not supported request method '${req.method}'.`);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
<div class="icon-button">
|
<div class="icon-button">
|
||||||
<i [class]="'bi bi-' + icon"></i>
|
@if (isEmoji) {
|
||||||
|
<span class="emoji">{{ icon }}</span>
|
||||||
|
} @else {
|
||||||
|
<i [class]="'bi bi-' + icon"></i>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ import { Component, Input } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class IconButtonComponent {
|
export class IconButtonComponent {
|
||||||
@Input({ required: true }) icon!: string;
|
@Input({ required: true }) icon!: string;
|
||||||
|
|
||||||
|
get isEmoji(): boolean {
|
||||||
|
// Check if the icon is an emoji (starts with a non-ASCII character)
|
||||||
|
return this.icon.length > 0 && this.icon.charCodeAt(0) > 255;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,37 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
declare const chrome: any;
|
||||||
|
|
||||||
|
export type LogCategory =
|
||||||
|
| 'nip07'
|
||||||
|
| 'permission'
|
||||||
|
| 'vault'
|
||||||
|
| 'profile'
|
||||||
|
| 'bookmark'
|
||||||
|
| 'system';
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
level: 'log' | 'warn' | 'error' | 'debug';
|
level: 'log' | 'warn' | 'error' | 'debug';
|
||||||
|
category: LogCategory;
|
||||||
|
icon: string;
|
||||||
message: string;
|
message: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serializable format for storage
|
||||||
|
interface StoredLogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: 'log' | 'warn' | 'error' | 'debug';
|
||||||
|
category: LogCategory;
|
||||||
|
icon: string;
|
||||||
|
message: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOGS_STORAGE_KEY = 'extensionLogs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@@ -20,46 +44,351 @@ export class LoggerService {
|
|||||||
return this.#logs;
|
return this.#logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(namespace: string): void {
|
async initialize(namespace: string): Promise<void> {
|
||||||
this.#namespace = namespace;
|
this.#namespace = namespace;
|
||||||
|
await this.#loadLogsFromStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #loadLogsFromStorage(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||||
|
const result = await chrome.storage.session.get(LOGS_STORAGE_KEY);
|
||||||
|
if (result[LOGS_STORAGE_KEY]) {
|
||||||
|
// Convert stored format back to LogEntry with Date objects
|
||||||
|
this.#logs = (result[LOGS_STORAGE_KEY] as StoredLogEntry[]).map(
|
||||||
|
(entry) => ({
|
||||||
|
...entry,
|
||||||
|
timestamp: new Date(entry.timestamp),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs from storage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #saveLogsToStorage(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
||||||
|
// Convert Date to ISO string for storage
|
||||||
|
const storedLogs: StoredLogEntry[] = this.#logs.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
timestamp: entry.timestamp.toISOString(),
|
||||||
|
}));
|
||||||
|
await chrome.storage.session.set({ [LOGS_STORAGE_KEY]: storedLogs });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save logs to storage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshLogs(): Promise<void> {
|
||||||
|
await this.#loadLogsFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Generic logging methods
|
||||||
|
// ============================================
|
||||||
|
|
||||||
log(value: any, data?: any) {
|
log(value: any, data?: any) {
|
||||||
this.#assureInitialized();
|
this.#assureInitialized();
|
||||||
this.#addLog('log', value, data);
|
this.#addLog('log', 'system', '📝', value, data);
|
||||||
const nowString = new Date().toLocaleString();
|
this.#consoleLog('log', value);
|
||||||
console.log(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(value: any, data?: any) {
|
warn(value: any, data?: any) {
|
||||||
this.#assureInitialized();
|
this.#assureInitialized();
|
||||||
this.#addLog('warn', value, data);
|
this.#addLog('warn', 'system', '⚠️', value, data);
|
||||||
const nowString = new Date().toLocaleString();
|
this.#consoleLog('warn', value);
|
||||||
console.warn(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error(value: any, data?: any) {
|
error(value: any, data?: any) {
|
||||||
this.#assureInitialized();
|
this.#assureInitialized();
|
||||||
this.#addLog('error', value, data);
|
this.#addLog('error', 'system', '❌', value, data);
|
||||||
const nowString = new Date().toLocaleString();
|
this.#consoleLog('error', value);
|
||||||
console.error(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(value: any, data?: any) {
|
debug(value: any, data?: any) {
|
||||||
this.#assureInitialized();
|
this.#assureInitialized();
|
||||||
this.#addLog('debug', value, data);
|
this.#addLog('debug', 'system', '🔍', value, data);
|
||||||
const nowString = new Date().toLocaleString();
|
this.#consoleLog('debug', value);
|
||||||
console.debug(`[${this.#namespace} - ${nowString}]`, JSON.stringify(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
// ============================================
|
||||||
|
// NIP-07 Action Logging
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
logNip07Action(
|
||||||
|
method: string,
|
||||||
|
host: string,
|
||||||
|
approved: boolean,
|
||||||
|
autoApproved: boolean,
|
||||||
|
details?: { kind?: number; peerPubkey?: string }
|
||||||
|
) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
const approvalType = autoApproved ? 'auto-approved' : approved ? 'approved' : 'denied';
|
||||||
|
const icon = approved ? '✅' : '🚫';
|
||||||
|
|
||||||
|
let message = `${method} from ${host} - ${approvalType}`;
|
||||||
|
if (details?.kind !== undefined) {
|
||||||
|
message += ` (kind: ${details.kind})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#addLog('log', 'nip07', icon, message, {
|
||||||
|
method,
|
||||||
|
host,
|
||||||
|
approved,
|
||||||
|
autoApproved,
|
||||||
|
...details,
|
||||||
|
});
|
||||||
|
this.#consoleLog('log', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip07GetPublicKey(host: string, approved: boolean, autoApproved: boolean) {
|
||||||
|
this.logNip07Action('getPublicKey', host, approved, autoApproved);
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip07SignEvent(
|
||||||
|
host: string,
|
||||||
|
kind: number,
|
||||||
|
approved: boolean,
|
||||||
|
autoApproved: boolean
|
||||||
|
) {
|
||||||
|
this.logNip07Action('signEvent', host, approved, autoApproved, { kind });
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip07Encrypt(
|
||||||
|
method: 'nip04.encrypt' | 'nip44.encrypt',
|
||||||
|
host: string,
|
||||||
|
approved: boolean,
|
||||||
|
autoApproved: boolean,
|
||||||
|
peerPubkey?: string
|
||||||
|
) {
|
||||||
|
this.logNip07Action(method, host, approved, autoApproved, { peerPubkey });
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip07Decrypt(
|
||||||
|
method: 'nip04.decrypt' | 'nip44.decrypt',
|
||||||
|
host: string,
|
||||||
|
approved: boolean,
|
||||||
|
autoApproved: boolean,
|
||||||
|
peerPubkey?: string
|
||||||
|
) {
|
||||||
|
this.logNip07Action(method, host, approved, autoApproved, { peerPubkey });
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip07GetRelays(host: string, approved: boolean, autoApproved: boolean) {
|
||||||
|
this.logNip07Action('getRelays', host, approved, autoApproved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Permission Logging
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
logPermissionStored(
|
||||||
|
host: string,
|
||||||
|
method: string,
|
||||||
|
policy: string,
|
||||||
|
kind?: number
|
||||||
|
) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
const icon = policy === 'allow' ? '🔓' : '🔒';
|
||||||
|
let message = `Permission stored: ${method} for ${host} - ${policy}`;
|
||||||
|
if (kind !== undefined) {
|
||||||
|
message += ` (kind: ${kind})`;
|
||||||
|
}
|
||||||
|
this.#addLog('log', 'permission', icon, message, { host, method, policy, kind });
|
||||||
|
this.#consoleLog('log', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
logPermissionDeleted(host: string, method: string, kind?: number) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
let message = `Permission deleted: ${method} for ${host}`;
|
||||||
|
if (kind !== undefined) {
|
||||||
|
message += ` (kind: ${kind})`;
|
||||||
|
}
|
||||||
|
this.#addLog('log', 'permission', '🗑️', message, { host, method, kind });
|
||||||
|
this.#consoleLog('log', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Vault Operations Logging
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
logVaultUnlock() {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'vault', '🔓', 'Vault unlocked', undefined);
|
||||||
|
this.#consoleLog('log', 'Vault unlocked');
|
||||||
|
}
|
||||||
|
|
||||||
|
logVaultLock() {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'vault', '🔒', 'Vault locked', undefined);
|
||||||
|
this.#consoleLog('log', 'Vault locked');
|
||||||
|
}
|
||||||
|
|
||||||
|
logVaultCreated() {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'vault', '🆕', 'Vault created', undefined);
|
||||||
|
this.#consoleLog('log', 'Vault created');
|
||||||
|
}
|
||||||
|
|
||||||
|
logVaultExport(fileName: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'vault', '📤', `Vault exported: ${fileName}`, { fileName });
|
||||||
|
this.#consoleLog('log', `Vault exported: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logVaultImport(fileName: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'vault', '📥', `Vault imported: ${fileName}`, { fileName });
|
||||||
|
this.#consoleLog('log', `Vault imported: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logVaultReset() {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('warn', 'vault', '🗑️', 'Extension reset', undefined);
|
||||||
|
this.#consoleLog('warn', 'Extension reset');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Profile Operations Logging
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
logProfileFetchError(pubkey: string, error: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
const shortPubkey = pubkey.substring(0, 8) + '...';
|
||||||
|
this.#addLog('error', 'profile', '👤', `Failed to fetch profile for ${shortPubkey}: ${error}`, {
|
||||||
|
pubkey,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
this.#consoleLog('error', `Failed to fetch profile for ${shortPubkey}: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logProfileParseError(pubkey: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
const shortPubkey = pubkey.substring(0, 8) + '...';
|
||||||
|
this.#addLog('error', 'profile', '👤', `Failed to parse profile content for ${shortPubkey}`, {
|
||||||
|
pubkey,
|
||||||
|
});
|
||||||
|
this.#consoleLog('error', `Failed to parse profile content for ${shortPubkey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip05ValidationError(nip05: string, error: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('error', 'profile', '🔗', `NIP-05 validation failed for ${nip05}: ${error}`, {
|
||||||
|
nip05,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
this.#consoleLog('error', `NIP-05 validation failed for ${nip05}: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logNip05ValidationSuccess(nip05: string, pubkey: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
const shortPubkey = pubkey.substring(0, 8) + '...';
|
||||||
|
this.#addLog('log', 'profile', '✓', `NIP-05 verified: ${nip05} → ${shortPubkey}`, {
|
||||||
|
nip05,
|
||||||
|
pubkey,
|
||||||
|
});
|
||||||
|
this.#consoleLog('log', `NIP-05 verified: ${nip05} → ${shortPubkey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logProfileEdit(identityNick: string, field: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'profile', '✏️', `Profile edited: ${identityNick} - ${field}`, {
|
||||||
|
identityNick,
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
this.#consoleLog('log', `Profile edited: ${identityNick} - ${field}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logIdentityCreated(nick: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'profile', '🆕', `Identity created: ${nick}`, { nick });
|
||||||
|
this.#consoleLog('log', `Identity created: ${nick}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logIdentityDeleted(nick: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('warn', 'profile', '🗑️', `Identity deleted: ${nick}`, { nick });
|
||||||
|
this.#consoleLog('warn', `Identity deleted: ${nick}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logIdentitySelected(nick: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'profile', '👆', `Identity selected: ${nick}`, { nick });
|
||||||
|
this.#consoleLog('log', `Identity selected: ${nick}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Bookmark Operations Logging
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
logBookmarkAdded(url: string, title: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'bookmark', '🔖', `Bookmark added: ${title}`, { url, title });
|
||||||
|
this.#consoleLog('log', `Bookmark added: ${title}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logBookmarkRemoved(url: string, title: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('log', 'bookmark', '🗑️', `Bookmark removed: ${title}`, { url, title });
|
||||||
|
this.#consoleLog('log', `Bookmark removed: ${title}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// System/Error Logging
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
logRelayFetchError(identityNick: string, error: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('error', 'system', '📡', `Failed to fetch relays for ${identityNick}: ${error}`, {
|
||||||
|
identityNick,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
this.#consoleLog('error', `Failed to fetch relays for ${identityNick}: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logStorageError(operation: string, error: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('error', 'system', '💾', `Storage error (${operation}): ${error}`, {
|
||||||
|
operation,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
this.#consoleLog('error', `Storage error (${operation}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logCryptoError(operation: string, error: string) {
|
||||||
|
this.#assureInitialized();
|
||||||
|
this.#addLog('error', 'system', '🔐', `Crypto error (${operation}): ${error}`, {
|
||||||
|
operation,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
this.#consoleLog('error', `Crypto error (${operation}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Internal methods
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
this.#logs = [];
|
this.#logs = [];
|
||||||
|
await this.#saveLogsToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
#addLog(level: LogEntry['level'], message: any, data?: any) {
|
#addLog(
|
||||||
|
level: LogEntry['level'],
|
||||||
|
category: LogCategory,
|
||||||
|
icon: string,
|
||||||
|
message: any,
|
||||||
|
data?: any
|
||||||
|
) {
|
||||||
const entry: LogEntry = {
|
const entry: LogEntry = {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level,
|
level,
|
||||||
|
category,
|
||||||
|
icon,
|
||||||
message: typeof message === 'string' ? message : JSON.stringify(message),
|
message: typeof message === 'string' ? message : JSON.stringify(message),
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
@@ -69,6 +398,27 @@ export class LoggerService {
|
|||||||
if (this.#logs.length > this.#maxLogs) {
|
if (this.#logs.length > this.#maxLogs) {
|
||||||
this.#logs.pop();
|
this.#logs.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save to storage asynchronously (don't block)
|
||||||
|
this.#saveLogsToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
#consoleLog(level: 'log' | 'warn' | 'error' | 'debug', message: string) {
|
||||||
|
const nowString = new Date().toLocaleString();
|
||||||
|
const formattedMsg = `[${this.#namespace} - ${nowString}] ${message}`;
|
||||||
|
switch (level) {
|
||||||
|
case 'warn':
|
||||||
|
console.warn(formattedMsg);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error(formattedMsg);
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
console.debug(formattedMsg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(formattedMsg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#assureInitialized() {
|
#assureInitialized() {
|
||||||
@@ -79,3 +429,87 @@ export class LoggerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Standalone functions for background script
|
||||||
|
// (Background script runs in different context without Angular DI)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export async function backgroundLog(
|
||||||
|
category: LogCategory,
|
||||||
|
icon: string,
|
||||||
|
level: LogEntry['level'],
|
||||||
|
message: string,
|
||||||
|
data?: any
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (typeof chrome === 'undefined' || !chrome.storage?.session) {
|
||||||
|
console.log(`[Background] ${message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await chrome.storage.session.get(LOGS_STORAGE_KEY);
|
||||||
|
const existingLogs: StoredLogEntry[] = result[LOGS_STORAGE_KEY] || [];
|
||||||
|
|
||||||
|
const newEntry: StoredLogEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level,
|
||||||
|
category,
|
||||||
|
icon,
|
||||||
|
message,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedLogs = [newEntry, ...existingLogs].slice(0, 500);
|
||||||
|
await chrome.storage.session.set({ [LOGS_STORAGE_KEY]: updatedLogs });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add background log:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function backgroundLogNip07Action(
|
||||||
|
method: string,
|
||||||
|
host: string,
|
||||||
|
approved: boolean,
|
||||||
|
autoApproved: boolean,
|
||||||
|
details?: { kind?: number; peerPubkey?: string }
|
||||||
|
): Promise<void> {
|
||||||
|
const approvalType = autoApproved
|
||||||
|
? 'auto-approved'
|
||||||
|
: approved
|
||||||
|
? 'approved'
|
||||||
|
: 'denied';
|
||||||
|
const icon = approved ? '✅' : '🚫';
|
||||||
|
|
||||||
|
let message = `${method} from ${host} - ${approvalType}`;
|
||||||
|
if (details?.kind !== undefined) {
|
||||||
|
message += ` (kind: ${details.kind})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await backgroundLog('nip07', icon, 'log', message, {
|
||||||
|
method,
|
||||||
|
host,
|
||||||
|
approved,
|
||||||
|
autoApproved,
|
||||||
|
...details,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function backgroundLogPermissionStored(
|
||||||
|
host: string,
|
||||||
|
method: string,
|
||||||
|
policy: string,
|
||||||
|
kind?: number
|
||||||
|
): Promise<void> {
|
||||||
|
const icon = policy === 'allow' ? '🔓' : '🔒';
|
||||||
|
let message = `Permission stored: ${method} for ${host} - ${policy}`;
|
||||||
|
if (kind !== undefined) {
|
||||||
|
message += ` (kind: ${kind})`;
|
||||||
|
}
|
||||||
|
await backgroundLog('permission', icon, 'log', message, {
|
||||||
|
host,
|
||||||
|
method,
|
||||||
|
policy,
|
||||||
|
kind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { SimplePool } from 'nostr-tools/pool';
|
import { SimplePool } from 'nostr-tools/pool';
|
||||||
import { FALLBACK_PROFILE_RELAYS } from '../../constants/fallback-relays';
|
import { FALLBACK_PROFILE_RELAYS } from '../../constants/fallback-relays';
|
||||||
import { ProfileMetadata, ProfileMetadataCache } from '../storage/types';
|
import { ProfileMetadata, ProfileMetadataCache } from '../storage/types';
|
||||||
|
import { LoggerService } from '../logger/logger.service';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
declare const chrome: any;
|
declare const chrome: any;
|
||||||
@@ -14,6 +15,7 @@ const STORAGE_KEY = 'profileMetadataCache';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ProfileMetadataService {
|
export class ProfileMetadataService {
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
#cache: ProfileMetadataCache = {};
|
#cache: ProfileMetadataCache = {};
|
||||||
#pool: SimplePool | null = null;
|
#pool: SimplePool | null = null;
|
||||||
#fetchPromises = new Map<string, Promise<ProfileMetadata | null>>();
|
#fetchPromises = new Map<string, Promise<ProfileMetadata | null>>();
|
||||||
@@ -52,7 +54,8 @@ export class ProfileMetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load profile cache from storage:', error);
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.#logger.logStorageError('load profile cache', errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +68,8 @@ export class ProfileMetadataService {
|
|||||||
await chrome.storage.session.set({ [STORAGE_KEY]: this.#cache });
|
await chrome.storage.session.set({ [STORAGE_KEY]: this.#cache });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save profile cache to storage:', error);
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.#logger.logStorageError('save profile cache', errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +213,7 @@ export class ProfileMetadataService {
|
|||||||
this.#cache[pubkey] = profile;
|
this.#cache[pubkey] = profile;
|
||||||
results.set(pubkey, profile);
|
results.set(pubkey, profile);
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`Failed to parse profile for ${pubkey}`);
|
this.#logger.logProfileParseError(pubkey);
|
||||||
results.set(pubkey, null);
|
results.set(pubkey, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +229,8 @@ export class ProfileMetadataService {
|
|||||||
await this.#saveCacheToStorage();
|
await this.#saveCacheToStorage();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch profiles:', error);
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.#logger.logProfileFetchError('multiple', errorMsg);
|
||||||
// Set null for all unfetched pubkeys on error
|
// Set null for all unfetched pubkeys on error
|
||||||
for (const pubkey of uncachedPubkeys) {
|
for (const pubkey of uncachedPubkeys) {
|
||||||
if (!results.has(pubkey)) {
|
if (!results.has(pubkey)) {
|
||||||
@@ -283,11 +288,12 @@ export class ProfileMetadataService {
|
|||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`Failed to parse profile content for ${pubkey}`);
|
this.#logger.logProfileParseError(pubkey);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch profile for ${pubkey}:`, error);
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.#logger.logProfileFetchError(pubkey, errorMsg);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export abstract class BrowserSessionHandler {
|
|||||||
this.#browserSessionData = JSON.parse(JSON.stringify(data));
|
this.#browserSessionData = JSON.parse(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearInMemoryData() {
|
||||||
|
this.#browserSessionData = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist the full data to the session data storage.
|
* Persist the full data to the session data storage.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { BrowserSyncFlow, SignerMetaData } from './types';
|
import { Bookmark, BrowserSyncFlow, SignerMetaData } from './types';
|
||||||
|
|
||||||
export abstract class SignerMetaHandler {
|
export abstract class SignerMetaHandler {
|
||||||
get signerMetaData(): SignerMetaData | undefined {
|
get signerMetaData(): SignerMetaData | undefined {
|
||||||
@@ -8,7 +8,7 @@ export abstract class SignerMetaHandler {
|
|||||||
|
|
||||||
#signerMetaData?: SignerMetaData;
|
#signerMetaData?: SignerMetaData;
|
||||||
|
|
||||||
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'recklessMode', 'whitelistedHosts'];
|
readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'recklessMode', 'whitelistedHosts', 'bookmarks'];
|
||||||
/**
|
/**
|
||||||
* Load the full data from the storage. If the storage is used for storing
|
* Load the full data from the storage. If the storage is used for storing
|
||||||
* other data (e.g. browser sync data when the user decided to NOT sync),
|
* other data (e.g. browser sync data when the user decided to NOT sync),
|
||||||
@@ -89,4 +89,26 @@ export abstract class SignerMetaHandler {
|
|||||||
|
|
||||||
await this.saveFullData(this.#signerMetaData);
|
await this.saveFullData(this.#signerMetaData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the bookmarks array and immediately saves it.
|
||||||
|
*/
|
||||||
|
async setBookmarks(bookmarks: Bookmark[]): Promise<void> {
|
||||||
|
if (!this.#signerMetaData) {
|
||||||
|
this.#signerMetaData = {
|
||||||
|
bookmarks,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.#signerMetaData.bookmarks = bookmarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveFullData(this.#signerMetaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current bookmarks.
|
||||||
|
*/
|
||||||
|
getBookmarks(): Bookmark[] {
|
||||||
|
return this.#signerMetaData?.bookmarks ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ export class StorageService {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async lockVault(): Promise<void> {
|
||||||
|
this.assureIsInitialized();
|
||||||
|
await this.getBrowserSessionHandler().clearData();
|
||||||
|
this.getBrowserSessionHandler().clearInMemoryData();
|
||||||
|
// Note: We don't set isInitialized = false here because the sync data
|
||||||
|
// (encrypted vault) is still loaded and we need it to unlock again
|
||||||
|
}
|
||||||
|
|
||||||
async unlockVault(password: string): Promise<void> {
|
async unlockVault(password: string): Promise<void> {
|
||||||
await unlockVault.call(this, password);
|
await unlockVault.call(this, password);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ export const SIGNER_META_DATA_KEY = {
|
|||||||
vaultSnapshots: 'vaultSnapshots',
|
vaultSnapshots: 'vaultSnapshots',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bookmark entry for storing user bookmarks
|
||||||
|
*/
|
||||||
|
export interface Bookmark {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SignerMetaData {
|
export interface SignerMetaData {
|
||||||
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Signer sync, 3 = Custom sync (bring your own sync))
|
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Signer sync, 3 = Custom sync (bring your own sync))
|
||||||
|
|
||||||
@@ -104,6 +114,9 @@ export interface SignerMetaData {
|
|||||||
|
|
||||||
// Whitelisted hosts: auto-approve all actions from these hosts
|
// Whitelisted hosts: auto-approve all actions from these hosts
|
||||||
whitelistedHosts?: string[];
|
whitelistedHosts?: string[];
|
||||||
|
|
||||||
|
// User bookmarks
|
||||||
|
bookmarks?: Bookmark[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -84,3 +84,11 @@ h2.font-heading {
|
|||||||
h3.font-heading {
|
h3.font-heading {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emoji styling
|
||||||
|
.emoji {
|
||||||
|
font-family: var(--font-emoji);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
--font-sans: 'IBM Plex Mono', monospace;
|
--font-sans: 'IBM Plex Mono', monospace;
|
||||||
--font-heading: 'reglisse', sans-serif;
|
--font-heading: 'reglisse', sans-serif;
|
||||||
--font-theylive: 'theylive', sans-serif;
|
--font-theylive: 'theylive', sans-serif;
|
||||||
|
--font-emoji: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Android Emoji', sans-serif;
|
||||||
|
|
||||||
// Border radius (from market)
|
// Border radius (from market)
|
||||||
--radius: 0.25rem;
|
--radius: 0.25rem;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Plebeian Signer",
|
"name": "Plebeian Signer",
|
||||||
"description": "Nostr Identity Manager & Signer",
|
"description": "Nostr Identity Manager & Signer",
|
||||||
"version": "1.0.3",
|
"version": "1.0.6",
|
||||||
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { IdentityComponent } from './components/home/identity/identity.component
|
|||||||
import { InfoComponent } from './components/home/info/info.component';
|
import { InfoComponent } from './components/home/info/info.component';
|
||||||
import { SettingsComponent } from './components/home/settings/settings.component';
|
import { SettingsComponent } from './components/home/settings/settings.component';
|
||||||
import { LogsComponent } from './components/home/logs/logs.component';
|
import { LogsComponent } from './components/home/logs/logs.component';
|
||||||
|
import { BookmarksComponent } from './components/home/bookmarks/bookmarks.component';
|
||||||
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
|
import { NewIdentityComponent } from './components/new-identity/new-identity.component';
|
||||||
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
import { EditIdentityComponent } from './components/edit-identity/edit-identity.component';
|
||||||
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
import { HomeComponent as EditIdentityHomeComponent } from './components/edit-identity/home/home.component';
|
||||||
@@ -71,6 +72,10 @@ export const routes: Routes = [
|
|||||||
path: 'logs',
|
path: 'logs',
|
||||||
component: LogsComponent,
|
component: LogsComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'bookmarks',
|
||||||
|
component: BookmarksComponent,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-banner">
|
<div class="info-banner">
|
||||||
<i class="bi bi-info-circle"></i>
|
<span class="emoji">💡</span>
|
||||||
<span>These relays are fetched from your NIP-65 relay list (kind 10002). To update your relay list, use a Nostr client that supports NIP-65.</span>
|
<span>These relays are fetched from your NIP-65 relay list (kind 10002). To update your relay list, use a Nostr client that supports NIP-65.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
|
||||||
|
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
|
||||||
|
<div class="bookmarks-header">
|
||||||
|
<span class="bookmarks-title">Bookmarks</span>
|
||||||
|
<button class="btn btn-primary btn-sm" (click)="onBookmarkThisPage()">
|
||||||
|
<span class="emoji">🔖</span> Bookmark This Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bookmarks-container">
|
||||||
|
@if (isLoading) {
|
||||||
|
<div class="loading-state">Loading...</div>
|
||||||
|
} @else if (bookmarks.length === 0) {
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="sam-text-muted">
|
||||||
|
No bookmarks yet. Click "Bookmark This Page" to add the current page.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
@for (bookmark of bookmarks; track bookmark.id) {
|
||||||
|
<div class="bookmark-item" (click)="openBookmark(bookmark)">
|
||||||
|
<div class="bookmark-info">
|
||||||
|
<span class="bookmark-title">{{ bookmark.title }}</span>
|
||||||
|
<span class="bookmark-url">{{ getDomain(bookmark.url) }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="remove-btn"
|
||||||
|
title="Remove bookmark"
|
||||||
|
(click)="onRemoveBookmark(bookmark); $event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<span class="emoji">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--size);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--size);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state,
|
||||||
|
.loading-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-h);
|
||||||
|
padding: var(--size-h) var(--size);
|
||||||
|
margin-bottom: var(--size-hh);
|
||||||
|
background: var(--background-light);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-light-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-url {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--destructive);
|
||||||
|
color: var(--destructive-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { Bookmark, LoggerService, SignerMetaData } from '@common';
|
||||||
|
import { FirefoxMetaHandler } from '../../../common/data/firefox-meta-handler';
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bookmarks',
|
||||||
|
templateUrl: './bookmarks.component.html',
|
||||||
|
styleUrl: './bookmarks.component.scss',
|
||||||
|
imports: [],
|
||||||
|
})
|
||||||
|
export class BookmarksComponent implements OnInit {
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
readonly #metaHandler = new FirefoxMetaHandler();
|
||||||
|
|
||||||
|
bookmarks: Bookmark[] = [];
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.loadBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBookmarks() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const metaData = await this.#metaHandler.loadFullData() as SignerMetaData;
|
||||||
|
this.#metaHandler.setFullData(metaData);
|
||||||
|
this.bookmarks = this.#metaHandler.getBookmarks();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load bookmarks:', error);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBookmarkThisPage() {
|
||||||
|
try {
|
||||||
|
// Get the current tab URL and title
|
||||||
|
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab?.url || !tab?.title) {
|
||||||
|
console.error('Could not get current tab info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already bookmarked
|
||||||
|
if (this.bookmarks.some(b => b.url === tab.url)) {
|
||||||
|
console.log('Page already bookmarked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBookmark: Bookmark = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
url: tab.url,
|
||||||
|
title: tab.title,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bookmarks = [newBookmark, ...this.bookmarks];
|
||||||
|
await this.saveBookmarks();
|
||||||
|
this.#logger.logBookmarkAdded(newBookmark.url, newBookmark.title);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to bookmark page:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRemoveBookmark(bookmark: Bookmark) {
|
||||||
|
this.bookmarks = this.bookmarks.filter(b => b.id !== bookmark.id);
|
||||||
|
await this.saveBookmarks();
|
||||||
|
this.#logger.logBookmarkRemoved(bookmark.url, bookmark.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBookmarks() {
|
||||||
|
try {
|
||||||
|
await this.#metaHandler.setBookmarks(this.bookmarks);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save bookmarks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openBookmark(bookmark: Bookmark) {
|
||||||
|
browser.tabs.create({ url: bookmark.url });
|
||||||
|
}
|
||||||
|
|
||||||
|
getDomain(url: string): string {
|
||||||
|
try {
|
||||||
|
return new URL(url).hostname;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
title="Your selected identity"
|
title="Your selected identity"
|
||||||
>
|
>
|
||||||
<i class="bi bi-person-circle"></i>
|
<span class="emoji">👤</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
title="Identities"
|
title="Identities"
|
||||||
>
|
>
|
||||||
<i class="bi bi-people-fill"></i>
|
<span class="emoji">👥</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -27,14 +27,22 @@
|
|||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
title="Settings"
|
title="Settings"
|
||||||
>
|
>
|
||||||
<i class="bi bi-gear"></i>
|
<span class="emoji">⚙️</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
|
<a class="tab" routerLink="/home/bookmarks" routerLinkActive="active" title="Bookmarks">
|
||||||
<i class="bi bi-info-circle"></i>
|
<span class="emoji">🔖</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="tab" routerLink="/home/logs" routerLinkActive="active" title="Logs">
|
<a class="tab" routerLink="/home/logs" routerLinkActive="active" title="Logs">
|
||||||
<span style="font-size: 1.2rem">🪵</span>
|
<span class="emoji">🪵</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="tab" routerLink="/home/info" routerLinkActive="active" title="Info">
|
||||||
|
<span class="emoji">💡</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="tab" (click)="onClickLock()" title="Lock">
|
||||||
|
<span class="emoji">🔒</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
height: 60px;
|
height: 40px;
|
||||||
min-height: 60px;
|
min-height: 40px;
|
||||||
background: var(--background-light);
|
background: var(--background-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
a {
|
a, button {
|
||||||
all: unset;
|
all: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
|
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
border-top: 3px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
border-top: 3px solid var(--primary);
|
border-top: 2px solid var(--primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterModule, RouterOutlet } from '@angular/router';
|
import { Router, RouterModule, RouterOutlet } from '@angular/router';
|
||||||
|
import { LoggerService, StorageService } from '@common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@@ -7,4 +8,14 @@ import { RouterModule, RouterOutlet } from '@angular/router';
|
|||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
styleUrl: './home.component.scss',
|
styleUrl: './home.component.scss',
|
||||||
})
|
})
|
||||||
export class HomeComponent {}
|
export class HomeComponent {
|
||||||
|
readonly #storage = inject(StorageService);
|
||||||
|
readonly #router = inject(Router);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
|
async onClickLock() {
|
||||||
|
this.#logger.logVaultLock();
|
||||||
|
await this.#storage.lockVault();
|
||||||
|
this.#router.navigateByUrl('/vault-login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
title="Manage whitelisted apps"
|
title="Manage whitelisted apps"
|
||||||
(click)="onClickWhitelistedApps()"
|
(click)="onClickWhitelistedApps()"
|
||||||
>
|
>
|
||||||
<i class="bi bi-gear"></i>
|
<span class="emoji">⚙️</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
/>
|
/>
|
||||||
<span class="name">{{ getDisplayName(identity) }}</span>
|
<span class="name">{{ getDisplayName(identity) }}</span>
|
||||||
<lib-icon-button
|
<lib-icon-button
|
||||||
icon="gear"
|
icon="⚙️"
|
||||||
title="Identity settings"
|
title="Identity settings"
|
||||||
(click)="onClickEditIdentity(identity.id, $event)"
|
(click)="onClickEditIdentity(identity.id, $event)"
|
||||||
></lib-icon-button>
|
></lib-icon-button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, inject, OnInit } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
Identity_DECRYPTED,
|
Identity_DECRYPTED,
|
||||||
|
LoggerService,
|
||||||
NostrHelper,
|
NostrHelper,
|
||||||
ProfileMetadata,
|
ProfileMetadata,
|
||||||
ProfileMetadataService,
|
ProfileMetadataService,
|
||||||
@@ -29,6 +30,7 @@ export class IdentityComponent implements OnInit {
|
|||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.#loadData();
|
this.#loadData();
|
||||||
@@ -136,13 +138,16 @@ export class IdentityComponent implements OnInit {
|
|||||||
const result = await validateNip05(nip05, pubkey);
|
const result = await validateNip05(nip05, pubkey);
|
||||||
this.nip05isValidated = result.valid;
|
this.nip05isValidated = result.valid;
|
||||||
|
|
||||||
if (!result.valid) {
|
if (result.valid) {
|
||||||
console.log('NIP-05 validation failed:', result.error);
|
this.#logger.logNip05ValidationSuccess(nip05, pubkey);
|
||||||
|
} else {
|
||||||
|
this.#logger.logNip05ValidationError(nip05, result.error ?? 'Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validating = false;
|
this.validating = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('NIP-05 validation failed:', error);
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.#logger.logNip05ValidationError(nip05, errorMsg);
|
||||||
this.nip05isValidated = false;
|
this.nip05isValidated = false;
|
||||||
this.validating = false;
|
this.validating = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<div class="logs-header">
|
<div class="logs-header">
|
||||||
<span class="logs-title">Logs</span>
|
<span class="logs-title">Logs</span>
|
||||||
<button class="btn btn-sm btn-secondary" (click)="onClear()">Clear Log</button>
|
<div class="logs-actions">
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="onRefresh()">Refresh</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="onClear()">Clear</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="logs-container">
|
<div class="logs-container">
|
||||||
@if (logs.length === 0) {
|
@if (logs.length === 0) {
|
||||||
<div class="logs-empty">No logs yet</div>
|
<div class="logs-empty">No activity logged yet</div>
|
||||||
}
|
}
|
||||||
@for (log of logs; track log.timestamp) {
|
@for (log of logs; track log.timestamp) {
|
||||||
<div class="log-entry" [class]="getLevelClass(log.level)">
|
<div class="log-entry" [class]="getLevelClass(log.level)">
|
||||||
|
<span class="log-icon emoji">{{ log.icon }}</span>
|
||||||
<span class="log-time">{{ log.timestamp | date:'HH:mm:ss' }}</span>
|
<span class="log-time">{{ log.timestamp | date:'HH:mm:ss' }}</span>
|
||||||
<span class="log-level">{{ log.level }}</span>
|
|
||||||
<span class="log-message">{{ log.message }}</span>
|
<span class="log-message">{{ log.message }}</span>
|
||||||
@if (log.data) {
|
|
||||||
<pre class="log-data">{{ log.data | json }}</pre>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-title {
|
.logs-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -34,15 +39,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
.log-entry {
|
||||||
font-family: monospace;
|
font-family: var(--font-sans);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 4px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
|
|
||||||
&.log-error {
|
&.log-error {
|
||||||
background: rgba(220, 53, 69, 0.15);
|
background: rgba(220, 53, 69, 0.15);
|
||||||
@@ -65,29 +69,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.log-time {
|
.log-time {
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
font-size: 10px;
|
||||||
|
|
||||||
.log-level {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
width: 40px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-message {
|
.log-message {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-data {
|
|
||||||
width: 100%;
|
|
||||||
margin: 4px 0 0 0;
|
|
||||||
padding: 4px;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
import { LoggerService, LogEntry } from '@common';
|
import { LoggerService, LogEntry } from '@common';
|
||||||
import { DatePipe, JsonPipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-logs',
|
selector: 'app-logs',
|
||||||
templateUrl: './logs.component.html',
|
templateUrl: './logs.component.html',
|
||||||
styleUrl: './logs.component.scss',
|
styleUrl: './logs.component.scss',
|
||||||
imports: [DatePipe, JsonPipe],
|
imports: [DatePipe],
|
||||||
})
|
})
|
||||||
export class LogsComponent {
|
export class LogsComponent implements OnInit {
|
||||||
readonly #logger = inject(LoggerService);
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
get logs(): LogEntry[] {
|
get logs(): LogEntry[] {
|
||||||
return this.#logger.logs;
|
return this.#logger.logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClear() {
|
ngOnInit() {
|
||||||
this.#logger.clear();
|
// Refresh logs from storage to get background script logs
|
||||||
|
this.#logger.refreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRefresh() {
|
||||||
|
await this.#logger.refreshLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClear() {
|
||||||
|
await this.#logger.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
getLevelClass(level: LogEntry['level']): string {
|
getLevelClass(level: LogEntry['level']): string {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
BrowserSyncFlow,
|
BrowserSyncFlow,
|
||||||
ConfirmComponent,
|
ConfirmComponent,
|
||||||
DateHelper,
|
DateHelper,
|
||||||
|
LoggerService,
|
||||||
NavComponent,
|
NavComponent,
|
||||||
StartupService,
|
StartupService,
|
||||||
StorageService,
|
StorageService,
|
||||||
@@ -20,6 +21,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
readonly #startup = inject(StartupService);
|
readonly #startup = inject(StartupService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const vault = JSON.stringify(
|
const vault = JSON.stringify(
|
||||||
@@ -43,6 +45,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
|
|
||||||
async onResetExtension() {
|
async onResetExtension() {
|
||||||
try {
|
try {
|
||||||
|
this.#logger.logVaultReset();
|
||||||
await this.#storage.resetExtension();
|
await this.#storage.resetExtension();
|
||||||
this.#startup.startOver(getNewStorageServiceConfig());
|
this.#startup.startOver(getNewStorageServiceConfig());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -58,6 +61,7 @@ export class SettingsComponent extends NavComponent implements OnInit {
|
|||||||
const fileName = `Plebeian Signer Firefox - Vault Export - ${dateTimeString}.json`;
|
const fileName = `Plebeian Signer Firefox - Vault Export - ${dateTimeString}.json`;
|
||||||
|
|
||||||
this.#downloadJson(jsonVault, fileName);
|
this.#downloadJson(jsonVault, fileName);
|
||||||
|
this.#logger.logVaultExport(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
#downloadJson(jsonString: string, fileName: string) {
|
#downloadJson(jsonString: string, fileName: string) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, inject, ViewChild } from '@angular/core';
|
import { Component, inject, ViewChild } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NavComponent, StorageService, DerivingModalComponent } from '@common';
|
import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-new',
|
selector: 'app-new',
|
||||||
@@ -16,6 +16,7 @@ export class NewComponent extends NavComponent {
|
|||||||
|
|
||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
toggleType(element: HTMLInputElement) {
|
toggleType(element: HTMLInputElement) {
|
||||||
if (element.type === 'password') {
|
if (element.type === 'password') {
|
||||||
@@ -35,6 +36,7 @@ export class NewComponent extends NavComponent {
|
|||||||
try {
|
try {
|
||||||
await this.#storage.createNewVault(this.password);
|
await this.#storage.createNewVault(this.password);
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
|
this.#logger.logVaultCreated();
|
||||||
this.#router.navigateByUrl('/home/identities');
|
this.#router.navigateByUrl('/home/identities');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
|||||||
import {
|
import {
|
||||||
ConfirmComponent,
|
ConfirmComponent,
|
||||||
DerivingModalComponent,
|
DerivingModalComponent,
|
||||||
|
LoggerService,
|
||||||
NostrHelper,
|
NostrHelper,
|
||||||
ProfileMetadataService,
|
ProfileMetadataService,
|
||||||
StartupService,
|
StartupService,
|
||||||
@@ -28,6 +29,7 @@ export class VaultLoginComponent implements AfterViewInit {
|
|||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
readonly #startup = inject(StartupService);
|
readonly #startup = inject(StartupService);
|
||||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||||
|
readonly #logger = inject(LoggerService);
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.passwordInput.nativeElement.focus();
|
this.passwordInput.nativeElement.focus();
|
||||||
@@ -69,6 +71,7 @@ export class VaultLoginComponent implements AfterViewInit {
|
|||||||
// Unlock succeeded - hide modal and navigate
|
// Unlock succeeded - hide modal and navigate
|
||||||
console.log('[login] Hiding modal and navigating');
|
console.log('[login] Hiding modal and navigating');
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
|
this.#logger.logVaultUnlock();
|
||||||
|
|
||||||
// Fetch profile metadata for all identities in the background
|
// Fetch profile metadata for all identities in the background
|
||||||
this.#fetchAllProfiles();
|
this.#fetchAllProfiles();
|
||||||
@@ -102,6 +105,7 @@ export class VaultLoginComponent implements AfterViewInit {
|
|||||||
|
|
||||||
async onClickResetExtension() {
|
async onClickResetExtension() {
|
||||||
try {
|
try {
|
||||||
|
this.#logger.logVaultReset();
|
||||||
await this.#storage.resetExtension();
|
await this.#storage.resetExtension();
|
||||||
this.#startup.startOver(getNewStorageServiceConfig());
|
this.#startup.startOver(getNewStorageServiceConfig());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { NostrHelper } from '@common';
|
import {
|
||||||
|
backgroundLogNip07Action,
|
||||||
|
backgroundLogPermissionStored,
|
||||||
|
NostrHelper,
|
||||||
|
} from '@common';
|
||||||
import {
|
import {
|
||||||
BackgroundRequestMessage,
|
BackgroundRequestMessage,
|
||||||
checkPermissions,
|
checkPermissions,
|
||||||
@@ -109,17 +113,28 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
|||||||
});
|
});
|
||||||
debug(response);
|
debug(response);
|
||||||
if (response === 'approve' || response === 'reject') {
|
if (response === 'approve' || response === 'reject') {
|
||||||
|
const policy = response === 'approve' ? 'allow' : 'deny';
|
||||||
await storePermission(
|
await storePermission(
|
||||||
browserSessionData,
|
browserSessionData,
|
||||||
currentIdentity,
|
currentIdentity,
|
||||||
req.host,
|
req.host,
|
||||||
req.method,
|
req.method,
|
||||||
response === 'approve' ? 'allow' : 'deny',
|
policy,
|
||||||
|
req.params?.kind
|
||||||
|
);
|
||||||
|
await backgroundLogPermissionStored(
|
||||||
|
req.host,
|
||||||
|
req.method,
|
||||||
|
policy,
|
||||||
req.params?.kind
|
req.params?.kind
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['reject', 'reject-once'].includes(response)) {
|
if (['reject', 'reject-once'].includes(response)) {
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, false, false, {
|
||||||
|
kind: req.params?.kind,
|
||||||
|
peerPubkey: req.params?.peerPubkey,
|
||||||
|
});
|
||||||
throw new Error('Permission denied');
|
throw new Error('Permission denied');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -128,46 +143,71 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relays: Relays = {};
|
const relays: Relays = {};
|
||||||
|
let result: any;
|
||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case 'getPublicKey':
|
case 'getPublicKey':
|
||||||
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
result = NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'signEvent':
|
case 'signEvent':
|
||||||
return signEvent(req.params, currentIdentity.privkey);
|
result = signEvent(req.params, currentIdentity.privkey);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
kind: req.params?.kind,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'getRelays':
|
case 'getRelays':
|
||||||
browserSessionData.relays.forEach((x) => {
|
browserSessionData.relays.forEach((x) => {
|
||||||
relays[x.url] = { read: x.read, write: x.write };
|
relays[x.url] = { read: x.read, write: x.write };
|
||||||
});
|
});
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove);
|
||||||
return relays;
|
return relays;
|
||||||
|
|
||||||
case 'nip04.encrypt':
|
case 'nip04.encrypt':
|
||||||
return await nip04Encrypt(
|
result = await nip04Encrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.plaintext
|
req.params.plaintext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'nip44.encrypt':
|
case 'nip44.encrypt':
|
||||||
return await nip44Encrypt(
|
result = await nip44Encrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.plaintext
|
req.params.plaintext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'nip04.decrypt':
|
case 'nip04.decrypt':
|
||||||
return await nip04Decrypt(
|
result = await nip04Decrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.ciphertext
|
req.params.ciphertext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
case 'nip44.decrypt':
|
case 'nip44.decrypt':
|
||||||
return await nip44Decrypt(
|
result = await nip44Decrypt(
|
||||||
currentIdentity.privkey,
|
currentIdentity.privkey,
|
||||||
req.params.peerPubkey,
|
req.params.peerPubkey,
|
||||||
req.params.ciphertext
|
req.params.ciphertext
|
||||||
);
|
);
|
||||||
|
await backgroundLogNip07Action(req.method, req.host, true, recklessApprove, {
|
||||||
|
peerPubkey: req.params.peerPubkey,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Not supported request method '${req.method}'.`);
|
throw new Error(`Not supported request method '${req.method}'.`);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user