diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..31ca4d2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,46 @@ +{ + "root": true, + "env": { + "browser": true, + "es2021": true, + "webextensions": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "@typescript-eslint", + "react", + "react-hooks" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "no-console": ["warn", { "allow": ["warn", "error"] }] + }, + "settings": { + "react": { + "version": "detect" + } + }, + "ignorePatterns": [ + "dist/", + "node_modules/", + "*.config.js", + "*.config.ts" + ] +} + diff --git a/README.md b/README.md index c2330da..405045b 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,15 @@ npm run build | `Alt+D` | Toggle Drawing | Enable/disable drawing mode | | `Alt+S` | Toggle Sidebar | Open/close the side panel | | `Alt+A` | Open Annotations | Jump to annotations tab | +| `Alt+Shift+A` | Toggle Annotations | Enable/disable annotation button | +| `Shift+?` | Show Shortcuts | Display keyboard shortcuts help | +| `Ctrl+Z` / `Cmd+Z` | Undo | Undo last action (in drawing mode) | +| `Ctrl+Y` / `Cmd+Shift+Z` | Redo | Redo last action (in drawing mode) | +| `Escape` | Close/Exit | Close modal or exit drawing mode | -*On Mac, use `Option` instead of `Alt`* +*On Mac, use `Option` instead of `Alt` and `Cmd` instead of `Ctrl`* + +**Tip:** Press `Shift+?` in the popup or sidepanel to see all available shortcuts! ## 📖 Usage Guide diff --git a/docs/DEPENDENCY_UPGRADE_PLAN.md b/docs/DEPENDENCY_UPGRADE_PLAN.md new file mode 100644 index 0000000..ea8e8e9 --- /dev/null +++ b/docs/DEPENDENCY_UPGRADE_PLAN.md @@ -0,0 +1,111 @@ +# Dependency Upgrade Plan + +This document outlines the planned dependency upgrades for post-launch maintenance. + +## Current Status + +### Production Dependencies +- `@synonymdev/pubky`: `^0.5.4` (pinned) +- `dompurify`: `^3.3.1` +- `react`: `^18.3.1` +- `react-dom`: `^18.3.1` + +### Dev Dependencies +- `vite`: `^5.3.3` +- `vitest`: `^1.3.1` +- `typescript`: `^5.5.3` +- `@playwright/test`: `^1.40.0` + +## Upgrade Priorities + +### High Priority (Post-Launch) + +1. **@synonymdev/pubky** (when stable) + - Current: `^0.5.4` + - Target: Latest stable (check changelog for breaking changes) + - Risk: Medium - Core SDK, test thoroughly + - Timeline: After 1-2 months of stable production use + +2. **React** (when 19.x stable) + - Current: `^18.3.1` + - Target: `^19.0.0` (when released) + - Risk: Medium - May require code changes + - Timeline: Wait for ecosystem adoption + +### Medium Priority (Quarterly) + +3. **Vite** (when 6.x stable) + - Current: `^5.3.3` + - Target: `^6.0.0` (when released) + - Risk: Medium - Build tool, test build process + - Timeline: After Vite 6 stable release + +4. **TypeScript** (quarterly) + - Current: `^5.5.3` + - Target: Latest 5.x (avoid 6.x until stable) + - Risk: Low - Usually backward compatible + - Timeline: Every 3 months + +5. **Playwright** (quarterly) + - Current: `^1.40.0` + - Target: Latest 1.x + - Risk: Low - Test framework, update tests if needed + - Timeline: Every 3 months + +### Low Priority (As Needed) + +6. **DOMPurify** (security updates) + - Current: `^3.3.1` + - Target: Latest 3.x + - Risk: Low - Security library, patch updates only + - Timeline: When security patches released + +7. **Vitest** (quarterly) + - Current: `^1.3.1` + - Target: Latest 1.x + - Risk: Low - Test framework + - Timeline: Every 3 months + +## Upgrade Process + +1. **Create feature branch**: `chore/upgrade-{package}-{version}` +2. **Update package.json**: Change version range +3. **Run `npm install`**: Install new version +4. **Run `npm run typecheck`**: Check for type errors +5. **Run `npm run lint`**: Check for linting issues +6. **Run `npm test`**: Run unit tests +7. **Run `npm run test:e2e`**: Run E2E tests +8. **Manual testing**: Test critical user flows +9. **Update CHANGELOG.md**: Document upgrade +10. **Create PR**: Get review before merging + +## Breaking Changes Tracking + +### @synonymdev/pubky +- Monitor: https://github.com/synonymdev/pubky/releases +- Watch for: API changes, Client constructor changes, auth flow changes + +### React 19 +- Monitor: https://react.dev/blog +- Watch for: Hook changes, component API changes, concurrent features + +### Vite 6 +- Monitor: https://vitejs.dev/blog +- Watch for: Config changes, plugin API changes, build output changes + +## Security Updates + +For security-related updates: +1. Create hotfix branch immediately +2. Test critical paths only +3. Deploy to production ASAP +4. Full testing in follow-up PR + +## Notes + +- Always test in development environment first +- Keep backup of working package-lock.json +- Document any breaking changes in code comments +- Update type definitions if needed +- Check peer dependency requirements + diff --git a/package.json b/package.json index aa1a4fa..d6f275d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "test:ui": "vitest --ui", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", - "test:e2e:errors": "node run-e2e-with-error-capture.js" + "test:e2e:errors": "node run-e2e-with-error-capture.js", + "typecheck": "tsc --noEmit", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix" }, "dependencies": { "@synonymdev/pubky": "^0.5.4", @@ -41,10 +44,15 @@ "@types/qrcode": "^1.5.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", "@vitejs/plugin-react": "^4.3.1", "@vitest/coverage-v8": "^1.3.1", "@vitest/ui": "^1.3.1", "autoprefixer": "^10.4.19", + "eslint": "^9.39.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", "jsdom": "^24.0.0", "postcss": "^8.4.39", "tailwindcss": "^3.4.4", diff --git a/src/popup/components/ProfileEditor.tsx b/src/popup/components/ProfileEditor.tsx index c4d0fdb..71501c2 100644 --- a/src/popup/components/ProfileEditor.tsx +++ b/src/popup/components/ProfileEditor.tsx @@ -3,6 +3,7 @@ import { storage, ProfileData } from '../../utils/storage'; import { profileManager } from '../../utils/profile-manager'; import { logger } from '../../utils/logger'; import { validateProfile, VALIDATION_LIMITS } from '../../utils/validation'; +import { exportRecoveryFile, validatePassphrase } from '../../utils/recovery-file'; import ProgressBar from './ProgressBar'; import ImageCropper from './ImageCropper'; @@ -538,8 +539,121 @@ export function ProfileEditor() {

* Your profile will be saved as profile.json and a generated index.html on your homeserver

+ + {/* Recovery File Export */} +
+

Key Backup

+

+ Export a recovery file to backup your keys. Store it securely - you'll need it if you lose access to your device. +

+ +
+ {/* Recovery File Modal */} + {showRecoveryModal && ( +
+
+

Export Recovery File

+

+ Enter a strong passphrase to encrypt your recovery file. You'll need this passphrase to restore your keys. +

+ +
+
+ + { + setRecoveryPassphrase(e.target.value); + setRecoveryError(null); + }} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-yellow-500" + placeholder="Enter passphrase (min 8 characters)" + /> +
+ +
+ + { + setRecoveryPassphraseConfirm(e.target.value); + setRecoveryError(null); + }} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-yellow-500" + placeholder="Confirm passphrase" + /> +
+ + {recoveryError && ( +
{recoveryError}
+ )} + +
+ + +
+
+
+
+ )} + {/* Image Cropper Modal */} {showCropper && imageFileToCrop && ( { + try { + logger.info('RecoveryFile', 'Creating recovery file'); + + // Import SDK function + const { createRecoveryFile } = await import('@synonymdev/pubky'); + + // Create encrypted recovery file + const recoveryData = await createRecoveryFile(passphrase); + + // Create blob and download + const blob = new Blob([recoveryData], { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + + // Generate filename with timestamp + const timestamp = new Date().toISOString().split('T')[0]; + const filename = `pubky-recovery-${timestamp}.recovery`; + + // Trigger download + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up + URL.revokeObjectURL(url); + + logger.info('RecoveryFile', 'Recovery file exported successfully', { filename }); + } catch (error) { + logger.error('RecoveryFile', 'Failed to export recovery file', error as Error); + throw error; + } +} + +/** + * Validate passphrase strength + * @param passphrase - Passphrase to validate + * @returns Object with isValid flag and error message if invalid + */ +export function validatePassphrase(passphrase: string): { isValid: boolean; error?: string } { + if (!passphrase || passphrase.length < 8) { + return { isValid: false, error: 'Passphrase must be at least 8 characters long' }; + } + + if (passphrase.length > 128) { + return { isValid: false, error: 'Passphrase must be less than 128 characters' }; + } + + // Check for at least one number and one letter + const hasNumber = /\d/.test(passphrase); + const hasLetter = /[a-zA-Z]/.test(passphrase); + + if (!hasNumber || !hasLetter) { + return { + isValid: false, + error: 'Passphrase should contain both letters and numbers for better security' + }; + } + + return { isValid: true }; +} + diff --git a/vite.config.ts b/vite.config.ts index 749028e..508160f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -74,6 +74,13 @@ export default defineConfig({ if (id.includes('pubky-app-specs')) { return 'vendor-pubky-specs'; } + // Lazy load heavy dependencies + if (id.includes('qrcode')) { + return 'vendor-qrcode'; + } + if (id.includes('react-image-crop')) { + return 'vendor-image-crop'; + } return 'vendor-other'; } },