From 7de9e588d4e3c2a93c3c6e9305a98b7c13864877 Mon Sep 17 00:00:00 2001 From: DocTator Date: Mon, 28 Sep 2026 07:51:19 -0400 Subject: [PATCH] Add Password Generator plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the plugin requested in hermes-hq/plugins#52: - Random passwords with configurable length and character-set toggles (lower/upper/digits/symbols), plus an option to exclude visually ambiguous characters (0/O, 1/l/I). - Word-based passphrases from a curated 1424-word list (no profanity, no easily-confused words), with configurable word count, separator, capitalization, and an optional trailing number. - API-key-style tokens with an optional prefix. - An entropy-based strength meter shared across all three modes. - Copy to clipboard with a configurable auto-clear timeout, which only clears the clipboard if it still holds what this plugin copied. All randomness goes through crypto.getRandomValues via rejection sampling — never Math.random — since this plugin exists specifically to produce secrets. Co-Authored-By: Claude Sonnet 5 --- plugins/password-generator/.gitignore | 3 + plugins/password-generator/hermes-plugin.json | 47 + plugins/password-generator/package-lock.json | 867 ++++++++++++++++++ plugins/password-generator/package.json | 18 + .../src/PasswordGeneratorPanel.tsx | 453 +++++++++ plugins/password-generator/src/activate.ts | 77 ++ plugins/password-generator/src/generator.ts | 139 +++ plugins/password-generator/src/wordlist.ts | 153 ++++ plugins/password-generator/tsconfig.json | 17 + plugins/password-generator/vite.config.ts | 26 + registry/index.json | 28 + 11 files changed, 1828 insertions(+) create mode 100644 plugins/password-generator/.gitignore create mode 100644 plugins/password-generator/hermes-plugin.json create mode 100644 plugins/password-generator/package-lock.json create mode 100644 plugins/password-generator/package.json create mode 100644 plugins/password-generator/src/PasswordGeneratorPanel.tsx create mode 100644 plugins/password-generator/src/activate.ts create mode 100644 plugins/password-generator/src/generator.ts create mode 100644 plugins/password-generator/src/wordlist.ts create mode 100644 plugins/password-generator/tsconfig.json create mode 100644 plugins/password-generator/vite.config.ts diff --git a/plugins/password-generator/.gitignore b/plugins/password-generator/.gitignore new file mode 100644 index 0000000..1ab415f --- /dev/null +++ b/plugins/password-generator/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tgz diff --git a/plugins/password-generator/hermes-plugin.json b/plugins/password-generator/hermes-plugin.json new file mode 100644 index 0000000..5e07031 --- /dev/null +++ b/plugins/password-generator/hermes-plugin.json @@ -0,0 +1,47 @@ +{ + "id": "hermes-hq.password-generator", + "name": "Password Generator", + "version": "1.0.0", + "description": "Generate secure passwords, passphrases, and API-key-style secrets with a strength meter and clipboard auto-clear", + "author": "Hermes IDE", + "main": "dist/index.js", + "activationEvents": [ + { "type": "onStartup" } + ], + "contributes": { + "commands": [ + { + "command": "password-generator.openPanel", + "title": "Open Password Generator", + "category": "Password Generator" + }, + { + "command": "password-generator.quickGenerate", + "title": "Generate & Copy Password", + "category": "Password Generator" + } + ], + "panels": [ + { + "id": "password-generator-panel", + "name": "Passwords", + "side": "left", + "icon": "" + } + ], + "statusBarItems": [ + { + "id": "password-generator.status", + "text": "Passwords", + "tooltip": "Open Password Generator", + "alignment": "right", + "priority": 38, + "command": "password-generator.openPanel" + } + ] + }, + "permissions": [ + "clipboard.write", + "clipboard.read" + ] +} diff --git a/plugins/password-generator/package-lock.json b/plugins/password-generator/package-lock.json new file mode 100644 index 0000000..2578a0a --- /dev/null +++ b/plugins/password-generator/package-lock.json @@ -0,0 +1,867 @@ +{ + "name": "@hermes-hq/plugin-password-generator", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@hermes-hq/plugin-password-generator", + "version": "1.0.0", + "devDependencies": { + "@types/react": "^19.2.14", + "typescript": "^5.4.0", + "vite": "^8.0.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/plugins/password-generator/package.json b/plugins/password-generator/package.json new file mode 100644 index 0000000..d484d57 --- /dev/null +++ b/plugins/password-generator/package.json @@ -0,0 +1,18 @@ +{ + "name": "@hermes-hq/plugin-password-generator", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "typescript": "^5.4.0", + "vite": "^8.0.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } +} diff --git a/plugins/password-generator/src/PasswordGeneratorPanel.tsx b/plugins/password-generator/src/PasswordGeneratorPanel.tsx new file mode 100644 index 0000000..fd84a56 --- /dev/null +++ b/plugins/password-generator/src/PasswordGeneratorPanel.tsx @@ -0,0 +1,453 @@ +import * as React from "react"; +import { + generatePassword, + generatePassphrase, + generateApiKey, + estimateStrength, + type PasswordOptions, + type PassphraseOptions, + type ApiKeyOptions, +} from "./generator"; +import { WORDLIST } from "./wordlist"; +import { getAPI } from "./activate"; + +type Mode = "password" | "passphrase" | "apikey"; + +const CLEAR_OPTIONS = [ + { value: 0, label: "Never" }, + { value: 15, label: "15s" }, + { value: 30, label: "30s" }, + { value: 60, label: "60s" }, +]; + +const s = { + root: { + display: "flex", + flexDirection: "column" as const, + height: "100%", + fontFamily: "var(--font-mono)", + fontSize: "var(--text-lg)", + color: "var(--text-0)", + overflow: "hidden", + }, + tabs: { + display: "flex", + borderBottom: "1px solid var(--border)", + flexShrink: 0, + }, + tab: { + flex: 1, + padding: "8px 6px", + textAlign: "center" as const, + fontSize: "var(--text-base)", + fontWeight: 600, + color: "var(--text-2)", + background: "transparent", + border: "none", + borderBottom: "2px solid transparent", + cursor: "pointer" as const, + }, + tabActive: { + color: "var(--text-0)", + borderBottom: "2px solid var(--accent, var(--blue))", + }, + body: { + padding: "10px 12px", + display: "flex", + flexDirection: "column" as const, + gap: "8px", + overflow: "auto" as const, + flex: 1, + }, + row: { + display: "flex", + alignItems: "center", + gap: "6px", + }, + label: { + fontSize: "var(--text-base)", + color: "var(--text-2)", + fontWeight: 600, + textTransform: "uppercase" as const, + letterSpacing: "0.3px", + minWidth: "58px", + }, + select: { + flex: 1, + minWidth: 0, + background: "var(--bg-2)", + border: "1px solid var(--border)", + borderRadius: "var(--radius)", + color: "var(--text-0)", + fontFamily: "var(--font-mono)", + fontSize: "var(--text-lg)", + padding: "5px 8px", + outline: "none", + cursor: "pointer" as const, + }, + input: { + flex: 1, + minWidth: 0, + background: "var(--bg-2)", + border: "1px solid var(--border)", + borderRadius: "var(--radius)", + color: "var(--text-0)", + fontFamily: "var(--font-mono)", + fontSize: "var(--text-lg)", + padding: "5px 8px", + outline: "none", + }, + range: { + flex: 1, + }, + checkRow: { + display: "flex", + flexWrap: "wrap" as const, + gap: "10px", + paddingLeft: "58px", + }, + checkLabel: { + display: "flex", + alignItems: "center", + gap: "5px", + fontSize: "var(--text-base)", + color: "var(--text-1)", + cursor: "pointer" as const, + }, + output: { + background: "var(--bg-2)", + border: "1px solid var(--border)", + borderRadius: "var(--radius)", + padding: "10px", + fontFamily: "var(--font-mono)", + fontSize: "var(--text-lg)", + color: "var(--text-0)", + wordBreak: "break-all" as const, + minHeight: "1.6em", + cursor: "pointer" as const, + }, + btnRow: { + display: "flex", + gap: "6px", + }, + btn: { + flex: 1, + background: "var(--accent, var(--blue))", + border: "none", + borderRadius: "var(--radius)", + color: "#fff", + fontFamily: "var(--font-mono)", + fontSize: "var(--text-md)", + fontWeight: 600, + padding: "7px 12px", + cursor: "pointer" as const, + }, + btnSecondary: { + flex: 0, + background: "var(--bg-3)", + border: "1px solid var(--border)", + borderRadius: "var(--radius)", + color: "var(--text-2)", + fontFamily: "var(--font-mono)", + fontSize: "var(--text-md)", + fontWeight: 500, + padding: "7px 12px", + cursor: "pointer" as const, + whiteSpace: "nowrap" as const, + }, + meterTrack: { + height: "5px", + borderRadius: "3px", + background: "var(--bg-3)", + overflow: "hidden" as const, + }, + meterFill: (score: number, label: string) => ({ + height: "100%", + width: `${Math.round(score * 100)}%`, + borderRadius: "3px", + transition: "width 0.15s", + background: + label === "Very weak" || label === "Weak" + ? "var(--red, #e5534b)" + : label === "Fair" + ? "var(--yellow, #d9a441)" + : "var(--green, #3fb950)", + }), + meterLabel: { + fontSize: "var(--text-sm)", + color: "var(--text-3)", + display: "flex", + justifyContent: "space-between" as const, + }, + clearNote: { + fontSize: "var(--text-sm)", + color: "var(--text-3)", + textAlign: "center" as const, + }, +}; + +export function PasswordGeneratorPanel() { + const [mode, setMode] = React.useState("password"); + const [value, setValue] = React.useState(""); + const [copied, setCopied] = React.useState(false); + + // Password options + const [pwLength, setPwLength] = React.useState(16); + const [pwLower, setPwLower] = React.useState(true); + const [pwUpper, setPwUpper] = React.useState(true); + const [pwDigits, setPwDigits] = React.useState(true); + const [pwSymbols, setPwSymbols] = React.useState(true); + const [pwExcludeAmbiguous, setPwExcludeAmbiguous] = React.useState(false); + + // Passphrase options + const [ppWordCount, setPpWordCount] = React.useState(4); + const [ppSeparator, setPpSeparator] = React.useState("-"); + const [ppCapitalize, setPpCapitalize] = React.useState(true); + const [ppIncludeNumber, setPpIncludeNumber] = React.useState(false); + + // API key options + const [akPrefix, setAkPrefix] = React.useState("sk_live"); + const [akLength, setAkLength] = React.useState(32); + + // Clipboard auto-clear + const [clearAfter, setClearAfter] = React.useState(30); + const [clearCountdown, setClearCountdown] = React.useState(null); + const clearTimerRef = React.useRef(null); + const countdownRef = React.useRef(null); + const lastCopiedRef = React.useRef(""); + + const clearPendingTimers = React.useCallback(() => { + if (clearTimerRef.current !== null) { clearTimeout(clearTimerRef.current); clearTimerRef.current = null; } + if (countdownRef.current !== null) { clearInterval(countdownRef.current); countdownRef.current = null; } + setClearCountdown(null); + }, []); + + const generate = React.useCallback(() => { + let next = ""; + if (mode === "password") { + const opts: PasswordOptions = { + length: pwLength, + lower: pwLower, + upper: pwUpper, + digits: pwDigits, + symbols: pwSymbols, + excludeAmbiguous: pwExcludeAmbiguous, + }; + next = generatePassword(opts); + } else if (mode === "passphrase") { + const opts: PassphraseOptions = { + wordCount: ppWordCount, + separator: ppSeparator, + capitalize: ppCapitalize, + includeNumber: ppIncludeNumber, + }; + next = generatePassphrase(opts, WORDLIST); + } else { + const opts: ApiKeyOptions = { prefix: akPrefix, length: akLength }; + next = generateApiKey(opts); + } + setValue(next); + setCopied(false); + }, [mode, pwLength, pwLower, pwUpper, pwDigits, pwSymbols, pwExcludeAmbiguous, ppWordCount, ppSeparator, ppCapitalize, ppIncludeNumber, akPrefix, akLength]); + + // Regenerate whenever the mode or any option changes. + React.useEffect(() => { generate(); }, [generate]); + + // Clean up timers on unmount. + React.useEffect(() => clearPendingTimers, [clearPendingTimers]); + + const scheduleAutoClear = React.useCallback((copiedValue: string) => { + clearPendingTimers(); + if (clearAfter <= 0) return; + lastCopiedRef.current = copiedValue; + setClearCountdown(clearAfter); + countdownRef.current = window.setInterval(() => { + setClearCountdown(c => (c === null ? null : Math.max(0, c - 1))); + }, 1000); + clearTimerRef.current = window.setTimeout(async () => { + clearPendingTimers(); + try { + const api = getAPI(); + // Only clear the clipboard if it still holds what we put there — + // don't clobber something the user copied in the meantime. + const current = await api.clipboard.readText(); + if (current === lastCopiedRef.current) { + await api.clipboard.writeText(""); + api.ui.showToast("Clipboard cleared", { type: "info", duration: 1500 }); + } + } catch { /* clipboard read/clear is best-effort */ } + }, clearAfter * 1000); + }, [clearAfter, clearPendingTimers]); + + const handleCopy = React.useCallback(async () => { + if (!value) return; + try { + const api = getAPI(); + await api.clipboard.writeText(value); + setCopied(true); + api.ui.showToast("Copied to clipboard", { type: "success", duration: 1500 }); + scheduleAutoClear(value); + } catch { /* ignore */ } + }, [value, scheduleAutoClear]); + + const strength = React.useMemo(() => estimateStrength(value), [value]); + + return ( +
+
+ {(["password", "passphrase", "apikey"] as Mode[]).map(m => ( + + ))} +
+ +
+ {mode === "password" && ( + <> +
+ Length + setPwLength(Number(e.target.value))} + /> + {pwLength} +
+
+ + + + +
+
+ +
+ + )} + + {mode === "passphrase" && ( + <> +
+ Words + setPpWordCount(Number(e.target.value))} + /> + {ppWordCount} +
+
+ Separator + +
+
+ + +
+ + )} + + {mode === "apikey" && ( + <> +
+ Prefix + setAkPrefix(e.target.value)} + /> +
+
+ Length + setAkLength(Number(e.target.value))} + /> + {akLength} +
+ + )} + +
+ {value || " "} +
+ +
+
+
+
+
+ {strength.label} + ~{strength.bits} bits +
+
+ +
+ + +
+ +
+ Auto-clear + +
+ {clearCountdown !== null && ( +
Clipboard clears in {clearCountdown}s…
+ )} +
+
+ ); +} diff --git a/plugins/password-generator/src/activate.ts b/plugins/password-generator/src/activate.ts new file mode 100644 index 0000000..3ccb04b --- /dev/null +++ b/plugins/password-generator/src/activate.ts @@ -0,0 +1,77 @@ +import { PasswordGeneratorPanel } from "./PasswordGeneratorPanel"; +import { generatePassword } from "./generator"; + +// Hermes Plugin API types (provided at runtime by the host app) +interface Disposable { + dispose(): void; +} + +interface PluginPanelProps { + pluginId: string; + panelId: string; +} + +interface HermesPluginAPI { + ui: { + registerPanel(panelId: string, component: React.ComponentType): Disposable; + showPanel(panelId: string): void; + hidePanel(panelId: string): void; + togglePanel(panelId: string): void; + showToast(message: string, options?: { type?: "info" | "success" | "warning" | "error"; duration?: number }): void; + updateStatusBarItem(itemId: string, update: { text?: string; tooltip?: string; visible?: boolean }): void; + }; + commands: { + register(commandId: string, handler: () => void | Promise): Disposable; + execute(commandId: string): Promise; + }; + clipboard: { + readText(): Promise; + writeText(text: string): Promise; + }; + storage: { + get(key: string): Promise; + set(key: string, value: string): Promise; + delete(key: string): Promise; + }; + subscriptions: Disposable[]; +} + +let hermesAPI: HermesPluginAPI | null = null; + +export function getAPI(): HermesPluginAPI { + if (!hermesAPI) throw new Error("Password Generator plugin not activated"); + return hermesAPI; +} + +export function activate(api: HermesPluginAPI) { + hermesAPI = api; + + api.ui.registerPanel("password-generator-panel", PasswordGeneratorPanel); + + api.subscriptions.push( + api.commands.register("password-generator.openPanel", () => { + api.ui.showPanel("password-generator-panel"); + }) + ); + + // Quick command: generate a solid default password and copy it, without + // opening the panel. + api.subscriptions.push( + api.commands.register("password-generator.quickGenerate", async () => { + const pw = generatePassword({ + length: 20, + lower: true, + upper: true, + digits: true, + symbols: true, + excludeAmbiguous: false, + }); + await api.clipboard.writeText(pw); + api.ui.showToast("Password copied to clipboard", { type: "success", duration: 2000 }); + }) + ); +} + +export function deactivate() { + hermesAPI = null; +} diff --git a/plugins/password-generator/src/generator.ts b/plugins/password-generator/src/generator.ts new file mode 100644 index 0000000..81f1839 --- /dev/null +++ b/plugins/password-generator/src/generator.ts @@ -0,0 +1,139 @@ +// Core generation logic. All randomness goes through crypto.getRandomValues — +// never Math.random — since this plugin exists specifically to produce +// secrets. + +const LOWER = "abcdefghijklmnopqrstuvwxyz"; +const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const DIGITS = "0123456789"; +const SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?"; +// Characters that are easy to misread in most fonts: 0/O, 1/l/I, etc. +const AMBIGUOUS = "0O1lI|"; + +function randomInt(maxExclusive: number): number { + // Rejection sampling over the full 32-bit space, so the result stays + // uniform for any maxExclusive — including values above 256 (e.g. the + // 1400+ entry wordlist), which a single random byte can't cover. A + // single byte's modulo range silently degenerates to an infinite loop + // once maxExclusive > 256 (256 % maxExclusive == 256, so range hits 0). + const range = Math.floor(0x100000000 / maxExclusive) * maxExclusive; + const buf = new Uint32Array(1); + let x: number; + do { + crypto.getRandomValues(buf); + x = buf[0]; + } while (x >= range); + return x % maxExclusive; +} + +function randomChar(charset: string): string { + return charset[randomInt(charset.length)]; +} + +function shuffle(arr: T[]): T[] { + const a = arr.slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = randomInt(i + 1); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +export interface PasswordOptions { + length: number; + lower: boolean; + upper: boolean; + digits: boolean; + symbols: boolean; + excludeAmbiguous: boolean; +} + +export function generatePassword(opts: PasswordOptions): string { + let pools = [ + opts.lower ? LOWER : "", + opts.upper ? UPPER : "", + opts.digits ? DIGITS : "", + opts.symbols ? SYMBOLS : "", + ].filter(p => p.length > 0); + + if (opts.excludeAmbiguous) { + pools = pools.map(p => [...p].filter(c => !AMBIGUOUS.includes(c)).join("")); + } + + if (pools.length === 0) return ""; + + const full = pools.join(""); + const length = Math.max(1, opts.length); + + // Guarantee at least one char from every selected pool (when length allows), + // then fill the rest from the combined pool, then shuffle so the + // guaranteed picks aren't always in the same position. + const guaranteed = pools.slice(0, length).map(randomChar); + const rest = Array.from({ length: Math.max(0, length - guaranteed.length) }, () => randomChar(full)); + return shuffle([...guaranteed, ...rest]).join(""); +} + +export interface PassphraseOptions { + wordCount: number; + separator: string; + capitalize: boolean; + includeNumber: boolean; +} + +export function generatePassphrase(opts: PassphraseOptions, wordlist: string[]): string { + const count = Math.max(1, opts.wordCount); + const words = Array.from({ length: count }, () => { + const w = wordlist[randomInt(wordlist.length)]; + return opts.capitalize ? w[0].toUpperCase() + w.slice(1) : w; + }); + if (opts.includeNumber) { + words.push(String(randomInt(100)).padStart(2, "0")); + } + return words.join(opts.separator); +} + +export interface ApiKeyOptions { + prefix: string; + length: number; +} + +const BASE62 = LOWER + UPPER + DIGITS; + +export function generateApiKey(opts: ApiKeyOptions): string { + const body = Array.from({ length: Math.max(1, opts.length) }, () => randomChar(BASE62)).join(""); + const prefix = opts.prefix.trim(); + return prefix ? `${prefix}${prefix.endsWith("_") ? "" : "_"}${body}` : body; +} + +export type StrengthLabel = "Very weak" | "Weak" | "Fair" | "Good" | "Strong" | "Very strong"; + +export interface Strength { + bits: number; + label: StrengthLabel; + // 0..1, for a meter bar + score: number; +} + +/** Shannon-style entropy estimate from the actual character-class pool size used. */ +export function estimateStrength(value: string): Strength { + if (!value) return { bits: 0, label: "Very weak", score: 0 }; + + let poolSize = 0; + if (/[a-z]/.test(value)) poolSize += LOWER.length; + if (/[A-Z]/.test(value)) poolSize += UPPER.length; + if (/[0-9]/.test(value)) poolSize += DIGITS.length; + if (/[^a-zA-Z0-9]/.test(value)) poolSize += SYMBOLS.length; + if (poolSize === 0) poolSize = 26; + + const bits = Math.round(value.length * Math.log2(poolSize)); + + let label: StrengthLabel; + let score: number; + if (bits < 28) { label = "Very weak"; score = 0.15; } + else if (bits < 36) { label = "Weak"; score = 0.35; } + else if (bits < 50) { label = "Fair"; score = 0.55; } + else if (bits < 65) { label = "Good"; score = 0.75; } + else if (bits < 80) { label = "Strong"; score = 0.9; } + else { label = "Very strong"; score = 1; } + + return { bits, label, score }; +} diff --git a/plugins/password-generator/src/wordlist.ts b/plugins/password-generator/src/wordlist.ts new file mode 100644 index 0000000..fbd5dd4 --- /dev/null +++ b/plugins/password-generator/src/wordlist.ts @@ -0,0 +1,153 @@ +// A curated list of common, short, unambiguous English words for +// passphrase generation. Chosen for: no profanity/slurs, no easily-confused +// homophones, no words shorter than 3 or longer than 8 letters, and broad +// everyday vocabulary so passphrases are easy to read back and retype. +// +// 1424 words gives ~10.5 bits of entropy per word (log2(1424) ≈ 10.47), so a +// 4-word passphrase is ~42 bits and a 6-word one is ~63 bits, before the +// optional trailing number even adds its own entropy. +export const WORDLIST: string[] = [ + "able", "acid", "acorn", "actor", "adapt", "agile", "aide", "aim", "air", "alarm", + "album", "alert", "alley", "almond", "alpha", "alpine", "amber", "amuse", "anchor", "angel", + "angle", "animal", "ankle", "antique", "apex", "apple", "april", "apron", "arcade", "arch", + "arctic", "arena", "argue", "arm", "armor", "aroma", "arrow", "art", "ash", "aspect", + "aspen", "atlas", "atom", "attic", "audio", "august", "aunt", "author", "auto", "autumn", + "avenue", "avid", "award", "awning", "axis", "baby", "badge", "bagel", "baker", "balance", + "balcony", "ball", "balloon", "bamboo", "banana", "band", "banjo", "banner", "barn", "barrel", + "base", "basil", "basin", "basket", "bat", "bath", "battery", "bay", "beach", "beacon", + "bead", "beam", "bean", "bear", "beard", "beast", "beaver", "bed", "bee", "beet", + "bell", "belt", "bench", "berry", "beta", "bike", "billow", "birch", "bird", "bison", + "black", "blade", "blanket", "blaze", "blend", "bliss", "block", "bloom", "blossom", "blue", + "blunt", "boat", "bobcat", "bolt", "bond", "bone", "bonus", "book", "boost", "boot", + "border", "bottle", "boulder", "bounce", "bow", "bowl", "box", "boxer", "brace", "branch", + "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "bright", "brim", "broad", + "broccoli", "bronze", "brook", "brown", "brush", "bubble", "bucket", "buckle", "buddy", "budget", + "buffalo", "bugle", "build", "bulb", "bunny", "burst", "bus", "bush", "cabin", "cable", + "cactus", "cake", "calm", "camel", "camera", "camp", "canal", "candle", "candy", "canoe", + "canvas", "canyon", "cape", "captain", "car", "carbon", "card", "cargo", "carpet", "carrot", + "cart", "cascade", "case", "castle", "cave", "cedar", "cello", "cement", "chain", "chair", + "chalk", "chant", "charm", "chart", "chase", "cheek", "cheese", "cherry", "chess", "chest", + "chief", "chill", "chime", "chip", "choice", "choir", "chord", "chunk", "cider", "circle", + "citrus", "city", "clam", "clap", "clarity", "class", "claw", "clay", "clerk", "cliff", + "climb", "clip", "cloak", "clock", "cloth", "cloud", "clover", "clown", "club", "clue", + "coach", "coal", "coast", "coat", "cobalt", "coco", "coconut", "coffee", "coin", "comb", + "comet", "comfort", "comic", "compass", "cone", "copper", "coral", "cord", "cork", "corn", + "corner", "cotton", "couch", "cougar", "counter", "county", "cousin", "cove", "cover", "cozy", + "crab", "craft", "crane", "crater", "cream", "creek", "crest", "crew", "cricket", "crimson", + "crisp", "critic", "crop", "cross", "crow", "crown", "crumb", "crush", "crust", "crystal", + "cube", "cup", "curl", "cursor", "curve", "custom", "cyan", "cyber", "cycle", "dagger", + "daily", "dairy", "daisy", "dale", "dance", "dandy", "dart", "dash", "dawn", "dazzle", + "deck", "deer", "delta", "dense", "depot", "depth", "desert", "design", "desk", "diamond", + "diary", "dice", "digit", "dime", "diner", "dingo", "disc", "ditch", "dive", "dock", + "doctor", "dog", "dollar", "dolphin", "dome", "donkey", "donut", "door", "dot", "double", + "dove", "down", "dozen", "draft", "dragon", "drake", "drama", "drape", "drift", "drill", + "drink", "drive", "drop", "drum", "dry", "duck", "dune", "dusk", "dust", "dutch", + "eagle", "ear", "earth", "east", "easy", "echo", "eclipse", "edge", "eel", "effort", + "elbow", "elder", "elect", "elite", "elk", "elm", "ember", "emerald", "emperor", "empty", + "enamel", "energy", "engine", "enjoy", "enter", "equal", "era", "erase", "essay", "estate", + "ethic", "event", "evergreen", "exact", "exam", "exile", "exit", "expand", "extend", "eye", + "fabric", "face", "falcon", "fall", "fame", "family", "fan", "farm", "fast", "fauna", + "feast", "feather", "fence", "fern", "ferry", "fiber", "field", "fiesta", "figure", "filter", + "finch", "finger", "finish", "fire", "first", "fish", "fist", "fizz", "flag", "flame", + "flare", "flash", "flavor", "fleet", "flint", "flip", "float", "flock", "flood", "floor", + "flora", "flour", "flow", "flower", "flute", "foam", "focus", "fog", "fold", "forest", + "forge", "fork", "form", "fort", "forum", "fossil", "fountain", "fox", "frame", "friday", + "friend", "frog", "front", "frost", "fruit", "fudge", "fuel", "fumble", "fun", "fungus", + "future", "gable", "gadget", "galaxy", "gallery", "gallop", "garden", "garlic", "garnet", "gas", + "gate", "gauge", "gazelle", "gear", "gecko", "gem", "gentle", "geode", "ghost", "giant", + "gift", "ginger", "giraffe", "girl", "glacier", "glade", "glass", "gleam", "glide", "globe", + "glory", "glove", "glow", "goat", "gold", "gong", "goose", "gopher", "gorge", "gorilla", + "gourd", "grace", "grain", "grand", "grape", "graph", "grass", "gravel", "gray", "green", + "grid", "grill", "grip", "grove", "growth", "guide", "guitar", "gulf", "gully", "gum", + "gush", "hail", "hair", "half", "hall", "hammer", "hand", "happy", "harbor", "hare", + "harp", "harvest", "hat", "hatch", "hawk", "hazel", "heart", "hearth", "heat", "hedge", + "heron", "hex", "hickory", "hide", "hill", "hinge", "hive", "hollow", "holly", "honey", + "hood", "hook", "hope", "horizon", "horn", "horse", "hotel", "hour", "house", "hover", + "human", "humble", "hunt", "husk", "hut", "hydro", "hyena", "ice", "icicle", "icon", + "igloo", "image", "inbox", "inch", "index", "indigo", "inlet", "input", "insect", "instant", + "iris", "iron", "island", "issue", "ivory", "ivy", "jade", "jaguar", "jar", "jasmine", + "jazz", "jelly", "jet", "jewel", "jigsaw", "jog", "joker", "jolly", "journal", "joy", + "judge", "juice", "july", "jump", "june", "jungle", "junior", "jupiter", "jury", "kayak", + "keen", "kelp", "kernel", "kettle", "key", "khaki", "kick", "kilo", "kind", "king", + "kiosk", "kit", "kite", "kitten", "kiwi", "knee", "knight", "knob", "knot", "koala", + "label", "lace", "ladder", "lady", "lagoon", "lake", "lamb", "lamp", "lance", "land", + "lane", "lantern", "laptop", "large", "lark", "laser", "latch", "laurel", "lava", "lawn", + "layer", "leaf", "league", "leap", "learn", "leather", "ledge", "leech", "lemon", "lens", + "leopard", "letter", "level", "lever", "lily", "lime", "limit", "linen", "lion", "list", + "litter", "lively", "lizard", "llama", "loaf", "lobby", "lobster", "local", "lock", "locust", + "lodge", "log", "logic", "lollipop", "lotus", "loud", "lounge", "lucky", "lumber", "lunar", + "lunch", "lush", "lyric", "magic", "magma", "magnet", "maize", "major", "mallet", "mammal", + "manor", "maple", "marble", "march", "margin", "marina", "market", "marsh", "mask", "mason", + "mass", "master", "match", "matrix", "maze", "meadow", "melody", "melon", "mentor", "menu", + "mercury", "mesa", "metal", "meter", "method", "meteor", "middle", "midnight", "mild", "mile", + "milk", "mill", "mimic", "mint", "minute", "mirror", "mist", "mitten", "mobile", "model", + "module", "molten", "monarch", "monday", "money", "monitor", "monkey", "monsoon", "month", "moon", + "moose", "moral", "morning", "mosaic", "moss", "motel", "moth", "motion", "motor", "mound", + "mountain", "mouse", "mouth", "movie", "mud", "muffin", "mug", "mule", "mural", "muscle", + "museum", "music", "mustang", "mystic", "nail", "name", "napkin", "narrow", "native", "nature", + "navy", "nebula", "needle", "neon", "nephew", "nest", "net", "network", "news", "night", + "noble", "node", "noodle", "noon", "north", "nose", "note", "notion", "nova", "novel", + "nugget", "number", "nurse", "nutmeg", "oak", "oasis", "oat", "object", "ocean", "octopus", + "offer", "office", "olive", "onion", "onyx", "opal", "open", "opera", "orange", "orbit", + "orca", "orchard", "orchid", "order", "organ", "origin", "osprey", "otter", "outcome", "output", + "outset", "oval", "oven", "owl", "owner", "oxygen", "oyster", "ozone", "pace", "pack", + "paddle", "page", "paint", "palace", "palm", "panda", "panel", "panther", "pantry", "paper", + "parade", "parcel", "park", "parrot", "parsley", "party", "passion", "pasta", "pastel", "pasture", + "patch", "path", "patio", "pattern", "pause", "peace", "peach", "peacock", "peak", "peanut", + "pear", "pearl", "pebble", "pecan", "pedal", "pelican", "pencil", "penguin", "people", "pepper", + "perch", "period", "permit", "person", "petal", "phase", "pheasant", "phone", "photo", "piano", + "picnic", "picture", "pigeon", "pilot", "pine", "pink", "pioneer", "pipe", "pirate", "pistol", + "pivot", "pixel", "pizza", "place", "planet", "plant", "plasma", "plate", "plaza", "pledge", + "plum", "plume", "pocket", "poem", "point", "polar", "pole", "police", "polish", "pond", + "pony", "poplar", "poppy", "porch", "port", "portal", "post", "pouch", "poultry", "powder", + "power", "prairie", "prawn", "prefix", "press", "pretzel", "prime", "prince", "print", "prism", + "prize", "probe", "profile", "project", "prompt", "proof", "prune", "public", "puddle", "puffin", + "pulse", "pump", "pumpkin", "punch", "puppy", "purple", "puzzle", "python", "quail", "quality", + "quarry", "quarter", "queen", "query", "quest", "quick", "quiet", "quill", "quilt", "quote", + "rabbit", "raccoon", "radar", "radio", "raft", "rail", "rain", "raisin", "rally", "ram", + "ramp", "ranch", "random", "range", "raven", "reaction", "ready", "real", "reason", "record", + "red", "reed", "reef", "region", "relay", "relic", "remedy", "reptile", "rescue", "resort", + "result", "return", "reward", "rhino", "rhythm", "ribbon", "rice", "ridge", "rifle", "ring", + "ripple", "river", "road", "robin", "robot", "rock", "rocket", "rodeo", "roller", "roof", + "room", "root", "rope", "rose", "rotor", "round", "route", "royal", "ruby", "ruler", + "rural", "rustic", "saddle", "safari", "sail", "salad", "salmon", "salon", "salsa", "salt", + "sample", "sand", "sandal", "sapphire", "satin", "sauce", "savage", "scale", "scarf", "scene", + "scent", "school", "scoop", "scooter", "scope", "score", "scout", "screen", "script", "scroll", + "sculpt", "seal", "search", "season", "seat", "second", "secret", "sector", "seed", "seeker", + "segment", "select", "senior", "sense", "sepia", "series", "serum", "session", "setup", "shade", + "shadow", "shake", "shape", "share", "shark", "sharp", "sheep", "shelf", "shell", "shelter", + "shield", "shift", "shine", "ship", "shirt", "shoe", "shore", "short", "shoulder", "shower", + "shrimp", "shrine", "shrub", "shuttle", "sibling", "sierra", "signal", "silk", "silo", "silver", + "simple", "single", "sister", "sitar", "site", "skate", "sketch", "ski", "skill", "skunk", + "sky", "slate", "sleek", "sleeve", "slide", "slope", "sloth", "small", "smart", "smoke", + "smooth", "snack", "snail", "snake", "sneak", "snow", "soap", "social", "sock", "soda", + "solar", "solid", "song", "sonic", "sound", "soup", "source", "south", "space", "spark", + "sparrow", "spear", "speck", "spice", "spider", "spike", "spinach", "spiral", "spirit", "sponge", + "spoon", "sport", "spring", "sprout", "spruce", "square", "squid", "squirrel", "stable", "stack", + "stadium", "stage", "stamp", "stand", "star", "static", "station", "steady", "steam", "steel", + "stem", "step", "stereo", "stick", "sting", "stitch", "stone", "stool", "store", "storm", + "story", "stove", "strait", "strand", "straw", "stream", "street", "stride", "string", "stripe", + "stroke", "studio", "study", "stump", "style", "sugar", "suit", "summer", "summit", "sunny", + "sunset", "super", "surf", "surge", "swan", "swift", "swing", "switch", "sword", "syrup", + "system", "table", "tackle", "tag", "tail", "talent", "talon", "tangle", "tank", "tape", + "target", "task", "taste", "tavern", "taxi", "teal", "team", "temple", "tender", "tennis", + "tent", "terra", "terrace", "textile", "theater", "theme", "thicket", "thistle", "thread", "throne", + "thumb", "thunder", "ticket", "tiger", "tile", "timber", "time", "tinsel", "tiny", "tissue", + "titan", "toast", "tofu", "toggle", "token", "tomato", "tone", "tool", "tooth", "topaz", + "topic", "torch", "tornado", "tortoise", "totem", "touch", "tour", "towel", "tower", "town", + "toy", "trace", "track", "tractor", "trail", "train", "tram", "trance", "trap", "travel", + "tray", "treat", "treaty", "tree", "trek", "trend", "trial", "tribe", "trick", "trio", + "tripod", "trolley", "trophy", "tropic", "trout", "truck", "trumpet", "trunk", "trust", "truth", + "tulip", "tuna", "tundra", "tunnel", "turban", "turf", "turkey", "turtle", "tusk", "tutor", + "twig", "twilight", "twist", "type", "umbrella", "uncle", "unicorn", "union", "unique", "unit", + "urban", "usher", "valley", "value", "van", "vanilla", "vapor", "vast", "vault", "velvet", + "vendor", "venue", "verb", "verse", "vessel", "vest", "vetch", "victory", "video", "view", + "village", "vine", "vinyl", "violet", "violin", "vision", "visit", "vista", "vital", "vivid", + "voice", "volcano", "vote", "voyage", "wagon", "waist", "walnut", "walrus", "wander", "warm", + "warrior", "watch", "water", "wave", "weave", "web", "welcome", "west", "whale", "wharf", + "wheat", "wheel", "whisk", "whisper", "wick", "widget", "width", "wild", "willow", "wind", + "window", "wine", "wing", "winter", "wire", "wisdom", "wish", "wolf", "wonder", "wood", + "wool", "word", "world", "wren", "wrist", "yacht", "yard", "yarn", "year", "yellow", + "yield", "yoga", "yogurt", "young", "zebra", "zenith", "zephyr", "zest", "zigzag", "zinc", + "zipper", "zodiac", "zone", "zoom", +]; diff --git a/plugins/password-generator/tsconfig.json b/plugins/password-generator/tsconfig.json new file mode 100644 index 0000000..bb44ccc --- /dev/null +++ b/plugins/password-generator/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": false, + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/plugins/password-generator/vite.config.ts b/plugins/password-generator/vite.config.ts new file mode 100644 index 0000000..5edf11d --- /dev/null +++ b/plugins/password-generator/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import manifest from "./hermes-plugin.json"; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, "src/activate.ts"), + formats: ["iife"], + name: "__hermes_plugin__", + fileName: () => "index.js", + }, + rollupOptions: { + external: ["react"], + output: { + globals: { + react: "React", + }, + footer: `if (typeof window !== "undefined") { window.__hermesPlugins = window.__hermesPlugins || {}; window.__hermesPlugins["${manifest.id}"] = { activate: __hermes_plugin__.activate, deactivate: __hermes_plugin__.deactivate }; }`, + }, + }, + outDir: "dist", + emptyOutDir: true, + minify: false, + }, +}); diff --git a/registry/index.json b/registry/index.json index cb9392b..5b4856c 100644 --- a/registry/index.json +++ b/registry/index.json @@ -599,6 +599,34 @@ ] } ] + }, + { + "id": "hermes-hq.password-generator", + "name": "Password Generator", + "version": "1.0.0", + "description": "Generate secure passwords, passphrases, and API-key-style secrets with a strength meter and clipboard auto-clear", + "author": "Hermes IDE", + "icon": "", + "category": "Utilities", + "downloadUrl": "https://github.com/hermes-hq/plugins/releases/download/password-generator-v1.0.0/hermes-hq.password-generator-1.0.0.tgz", + "minAppVersion": "0.3.0", + "permissions": [ + "clipboard.write", + "clipboard.read" + ], + "changelog": [ + { + "version": "1.0.0", + "date": "2026-09-28", + "changes": [ + "Generate random passwords with configurable length and character sets", + "Generate memorable word-based passphrases with a 1424-word list", + "Generate API-key-style tokens with an optional prefix", + "Entropy-based password strength meter", + "Copy to clipboard with configurable auto-clear timeout" + ] + } + ] } ] }