Add support for remembering SSH key passphrases and update UI accordingly

This commit is contained in:
gorgiaxx
2026-05-07 17:38:17 +08:00
parent 3fc9622695
commit 8a44152b36
6 changed files with 118 additions and 25 deletions

69
App.tsx
View File

@@ -37,7 +37,9 @@ import { AlertTriangle, Download, Trash2 } from 'lucide-react';
import { import {
STORAGE_KEY_DEBUG_HOTKEYS, STORAGE_KEY_DEBUG_HOTKEYS,
STORAGE_KEY_PORT_FORWARDING, STORAGE_KEY_PORT_FORWARDING,
STORAGE_KEY_DEFAULT_KEY_PASSPHRASES,
} from './infrastructure/config/storageKeys'; } from './infrastructure/config/storageKeys';
import { encryptField, decryptField } from './infrastructure/persistence/secureFieldAdapter';
import { getEffectiveKnownHosts } from './infrastructure/syncHelpers'; import { getEffectiveKnownHosts } from './infrastructure/syncHelpers';
import { TopTabs } from './components/TopTabs'; import { TopTabs } from './components/TopTabs';
import { Button } from './components/ui/button'; import { Button } from './components/ui/button';
@@ -180,6 +182,27 @@ const TerminalLayerMount: React.FC<TerminalLayerProps> = (props) => {
); );
}; };
// Helper functions for default SSH key passphrase persistence
async function saveDefaultKeyPassphrase(keyPath: string, passphrase: string): Promise<void> {
const store = localStorageAdapter.read<Record<string, string>>(STORAGE_KEY_DEFAULT_KEY_PASSPHRASES) ?? {};
store[keyPath] = await encryptField(passphrase) ?? passphrase;
localStorageAdapter.write(STORAGE_KEY_DEFAULT_KEY_PASSPHRASES, store);
}
async function loadDefaultKeyPassphrase(keyPath: string): Promise<string | null> {
const store = localStorageAdapter.read<Record<string, string>>(STORAGE_KEY_DEFAULT_KEY_PASSPHRASES);
const enc = store?.[keyPath];
if (!enc) return null;
return (await decryptField(enc)) ?? null;
}
async function removeDefaultKeyPassphrase(keyPath: string): Promise<void> {
const store = localStorageAdapter.read<Record<string, string>>(STORAGE_KEY_DEFAULT_KEY_PASSPHRASES);
if (!store) return;
delete store[keyPath];
localStorageAdapter.write(STORAGE_KEY_DEFAULT_KEY_PASSPHRASES, store);
}
function App({ settings }: { settings: SettingsState }) { function App({ settings }: { settings: SettingsState }) {
const { t } = useI18n(); const { t } = useI18n();
@@ -203,6 +226,8 @@ function App({ settings }: { settings: SettingsState }) {
const [keyboardInteractiveQueue, setKeyboardInteractiveQueue] = useState<KeyboardInteractiveRequest[]>([]); const [keyboardInteractiveQueue, setKeyboardInteractiveQueue] = useState<KeyboardInteractiveRequest[]>([]);
// Passphrase request queue for encrypted SSH keys // Passphrase request queue for encrypted SSH keys
const [passphraseQueue, setPassphraseQueue] = useState<PassphraseRequest[]>([]); const [passphraseQueue, setPassphraseQueue] = useState<PassphraseRequest[]>([]);
// Track keys that were auto-responded in this session (to detect wrong saved passphrases)
const autoRespondedKeysRef = useRef<Set<string>>(new Set());
const { const {
theme, theme,
@@ -983,8 +1008,34 @@ function App({ settings }: { settings: SettingsState }) {
const bridge = netcattyBridge.get(); const bridge = netcattyBridge.get();
if (!bridge?.onPassphraseRequest) return; if (!bridge?.onPassphraseRequest) return;
const unsubscribe = bridge.onPassphraseRequest((request) => { const unsubscribe = bridge.onPassphraseRequest(async (request) => {
console.log('[App] Passphrase request received:', request); console.log('[App] Passphrase request received:', request);
// Check if this key was already auto-responded in this session
// If so, the saved passphrase was wrong - remove it and show modal
if (autoRespondedKeysRef.current.has(request.keyPath)) {
console.log('[App] Key was auto-responded before but failed, removing saved passphrase');
autoRespondedKeysRef.current.delete(request.keyPath);
await removeDefaultKeyPassphrase(request.keyPath);
setPassphraseQueue(prev => [...prev, {
requestId: request.requestId,
keyPath: request.keyPath,
keyName: request.keyName,
hostname: request.hostname,
}]);
return;
}
// Try to load saved passphrase
const saved = await loadDefaultKeyPassphrase(request.keyPath);
if (saved) {
console.log('[App] Auto-responding with saved passphrase for:', request.keyPath);
autoRespondedKeysRef.current.add(request.keyPath);
void bridge.respondPassphrase?.(request.requestId, saved, false);
return;
}
// No saved passphrase, show modal
setPassphraseQueue(prev => [...prev, { setPassphraseQueue(prev => [...prev, {
requestId: request.requestId, requestId: request.requestId,
keyPath: request.keyPath, keyPath: request.keyPath,
@@ -999,13 +1050,25 @@ function App({ settings }: { settings: SettingsState }) {
}, []); }, []);
// Handle passphrase submit // Handle passphrase submit
const handlePassphraseSubmit = useCallback((requestId: string, passphrase: string) => { const handlePassphraseSubmit = useCallback(async (requestId: string, passphrase: string, remember: boolean) => {
const bridge = netcattyBridge.get(); const bridge = netcattyBridge.get();
if (bridge?.respondPassphrase) { if (bridge?.respondPassphrase) {
void bridge.respondPassphrase(requestId, passphrase, false); void bridge.respondPassphrase(requestId, passphrase, false);
} }
// Save passphrase if requested
if (remember) {
const request = passphraseQueue.find(r => r.requestId === requestId);
if (request?.keyPath) {
console.log('[App] Saving passphrase for:', request.keyPath);
void saveDefaultKeyPassphrase(request.keyPath, passphrase);
// Clear from auto-responded tracking since user manually entered it
autoRespondedKeysRef.current.delete(request.keyPath);
}
}
setPassphraseQueue(prev => prev.filter(r => r.requestId !== requestId)); setPassphraseQueue(prev => prev.filter(r => r.requestId !== requestId));
}, []); }, [passphraseQueue]);
// Handle passphrase cancel // Handle passphrase cancel
const handlePassphraseCancel = useCallback((requestId: string) => { const handlePassphraseCancel = useCallback((requestId: string) => {

View File

@@ -1814,6 +1814,7 @@ const en: Messages = {
'passphrase.unlock': 'Unlock', 'passphrase.unlock': 'Unlock',
'passphrase.unlocking': 'Unlocking...', 'passphrase.unlocking': 'Unlocking...',
'passphrase.skip': 'Skip', 'passphrase.skip': 'Skip',
'passphrase.remember': 'Remember this passphrase',
// Text Editor // Text Editor
'sftp.editor.wordWrap': 'Word Wrap', 'sftp.editor.wordWrap': 'Word Wrap',

View File

@@ -1823,6 +1823,7 @@ const zhCN: Messages = {
'passphrase.unlock': '解锁', 'passphrase.unlock': '解锁',
'passphrase.unlocking': '解锁中...', 'passphrase.unlocking': '解锁中...',
'passphrase.skip': '跳过', 'passphrase.skip': '跳过',
'passphrase.remember': '记住此密码',
// Text Editor // Text Editor
'sftp.editor.wordWrap': '自动换行', 'sftp.editor.wordWrap': '自动换行',

View File

@@ -25,7 +25,7 @@ export interface PassphraseRequest {
interface PassphraseModalProps { interface PassphraseModalProps {
request: PassphraseRequest | null; request: PassphraseRequest | null;
onSubmit: (requestId: string, passphrase: string) => void; onSubmit: (requestId: string, passphrase: string, remember: boolean) => void;
onCancel: (requestId: string) => void; onCancel: (requestId: string) => void;
onSkip?: (requestId: string) => void; onSkip?: (requestId: string) => void;
} }
@@ -40,6 +40,7 @@ export const PassphraseModal: React.FC<PassphraseModalProps> = ({
const [passphrase, setPassphrase] = useState(""); const [passphrase, setPassphrase] = useState("");
const [showPassphrase, setShowPassphrase] = useState(false); const [showPassphrase, setShowPassphrase] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [rememberPassphrase, setRememberPassphrase] = useState(true);
// Reset state when request changes // Reset state when request changes
useEffect(() => { useEffect(() => {
@@ -47,14 +48,15 @@ export const PassphraseModal: React.FC<PassphraseModalProps> = ({
setPassphrase(""); setPassphrase("");
setShowPassphrase(false); setShowPassphrase(false);
setIsSubmitting(false); setIsSubmitting(false);
setRememberPassphrase(true);
} }
}, [request]); }, [request]);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
if (!request || isSubmitting || !passphrase) return; if (!request || isSubmitting || !passphrase) return;
setIsSubmitting(true); setIsSubmitting(true);
onSubmit(request.requestId, passphrase); onSubmit(request.requestId, passphrase, rememberPassphrase);
}, [request, passphrase, onSubmit, isSubmitting]); }, [request, passphrase, onSubmit, isSubmitting, rememberPassphrase]);
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
if (!request) return; if (!request) return;
@@ -82,15 +84,15 @@ export const PassphraseModal: React.FC<PassphraseModalProps> = ({
return ( return (
<Dialog open={!!request} onOpenChange={(open) => !open && handleCancel()}> <Dialog open={!!request} onOpenChange={(open) => !open && handleCancel()}>
<DialogContent className="sm:max-w-[425px]" hideCloseButton> <DialogContent className="sm:max-w-[500px]" hideCloseButton>
<DialogHeader> <DialogHeader>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center"> <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<KeyRound className="h-5 w-5 text-primary" /> <KeyRound className="h-5 w-5 text-primary" />
</div> </div>
<div> <div className="min-w-0 flex-1">
<DialogTitle>{t("passphrase.title")}</DialogTitle> <DialogTitle>{t("passphrase.title")}</DialogTitle>
<DialogDescription className="mt-1"> <DialogDescription className="mt-1 break-words">
{request.hostname {request.hostname
? t("passphrase.descWithHost", { keyName: keyDisplayName, hostname: request.hostname }) ? t("passphrase.descWithHost", { keyName: keyDisplayName, hostname: request.hostname })
: t("passphrase.desc", { keyName: keyDisplayName })} : t("passphrase.desc", { keyName: keyDisplayName })}
@@ -125,9 +127,21 @@ export const PassphraseModal: React.FC<PassphraseModalProps> = ({
{showPassphrase ? <EyeOff size={16} /> : <Eye size={16} />} {showPassphrase ? <EyeOff size={16} /> : <Eye size={16} />}
</button> </button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground break-all">
{t("passphrase.keyPath")}: <code className="text-xs">{request.keyPath}</code> {t("passphrase.keyPath")}: <code className="text-xs break-all">{request.keyPath}</code>
</p> </p>
<label className="flex items-center gap-2 cursor-pointer select-none mt-2">
<input
type="checkbox"
checked={rememberPassphrase}
onChange={(e) => setRememberPassphrase(e.target.checked)}
disabled={isSubmitting}
className="accent-primary"
/>
<span className="text-xs text-muted-foreground">
{t("passphrase.remember")}
</span>
</label>
</div> </div>
</div> </div>

View File

@@ -156,6 +156,9 @@ export const STORAGE_KEY_WORKSPACE_FOCUS_SIDEBAR_WIDTH = 'netcatty_workspace_foc
// Port Forwarding (transient cross-window broadcast key) // Port Forwarding (transient cross-window broadcast key)
export const STORAGE_KEY_PF_RECONNECT_CANCEL = '__netcatty_pf_cancel_reconnect'; export const STORAGE_KEY_PF_RECONNECT_CANCEL = '__netcatty_pf_cancel_reconnect';
// Default SSH Key Passphrases (for ~/.ssh keys not managed in the vault)
export const STORAGE_KEY_DEFAULT_KEY_PASSPHRASES = 'netcatty_default_key_passphrases_v1';
// Debug Flags (no _v1 suffix — developer-only, not persisted data) // Debug Flags (no _v1 suffix — developer-only, not persisted data)
export const STORAGE_KEY_DEBUG_HOTKEYS = 'debug.hotkeys'; export const STORAGE_KEY_DEBUG_HOTKEYS = 'debug.hotkeys';
export const STORAGE_KEY_DEBUG_UPDATE_DEMO = 'debug.updateDemo'; export const STORAGE_KEY_DEBUG_UPDATE_DEMO = 'debug.updateDemo';

39
package-lock.json generated
View File

@@ -1158,6 +1158,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1803,7 +1804,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1825,7 +1825,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -1842,7 +1841,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -1857,7 +1855,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -3290,6 +3287,7 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
"integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.9", "@hono/node-server": "^1.19.9",
"ajv": "^8.17.1", "ajv": "^8.17.1",
@@ -6338,6 +6336,7 @@
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/unist": "*" "@types/unist": "*"
} }
@@ -6418,6 +6417,7 @@
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/scope-manager": "8.54.0",
@@ -6447,6 +6447,7 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0", "@typescript-eslint/types": "8.54.0",
@@ -6997,6 +6998,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -7047,6 +7049,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -7630,6 +7633,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -8372,8 +8376,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "10.1.0", "version": "10.1.0",
@@ -8657,6 +8660,7 @@
"integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.7.0", "app-builder-lib": "26.7.0",
"builder-util": "26.4.1", "builder-util": "26.4.1",
@@ -9038,7 +9042,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -9059,7 +9062,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -9289,6 +9291,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -10667,6 +10670,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -12156,6 +12160,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/debug": "^4.0.0", "@types/debug": "^4.0.0",
"debug": "^4.0.0", "debug": "^4.0.0",
@@ -12773,7 +12778,8 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
], ],
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
@@ -13028,7 +13034,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -13041,6 +13046,7 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"dompurify": "3.2.7", "dompurify": "3.2.7",
"marked": "14.0.0" "marked": "14.0.0"
@@ -13815,7 +13821,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -13833,7 +13838,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -14024,6 +14028,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -14033,6 +14038,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -15363,7 +15369,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -15428,7 +15433,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -15503,6 +15507,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -15617,6 +15622,7 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "~0.27.0", "esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
@@ -15715,6 +15721,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -15735,6 +15742,7 @@
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/unist": "^3.0.0", "@types/unist": "^3.0.0",
"bail": "^2.0.0", "bail": "^2.0.0",
@@ -16073,6 +16081,7 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -16166,6 +16175,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -16444,6 +16454,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }