commit 3d066c529769b7b49019c64fd438e1fa9af46527 Author: Zoë Date: Tue Aug 25 18:02:07 2026 +0200 chore: init decky vaultwarden plugin scaffold diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..602eb67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +out/ +*.pyc +__pycache__/ +.env +*.egg-info/ +.venv/ +venv/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c3aa9b4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,34 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "pnpm run build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$tsc"] + }, + { + "label": "watch", + "type": "shell", + "command": "pnpm run watch", + "group": "build", + "isBackground": true + }, + { + "label": "typecheck", + "type": "shell", + "command": "pnpm run typecheck", + "group": "test" + }, + { + "label": "install-deps", + "type": "shell", + "command": "pnpm install", + "group": "none" + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e837d98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +MIT License + +Copyright (c) 2026 zoe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +This plugin is not affiliated with Bitwarden Inc. or Vaultwarden. +Bitwarden is a registered trademark of Bitwarden Inc. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff1ea3b --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Decky Vaultwarden + +A Decky Loader plugin for accessing Bitwarden and Vaultwarden passwords directly from your Steam Deck. + +## Features + +- **Login with Email + Password** or **API Key** (supports self-hosted Vaultwarden) +- **Browse vault** organized by folders with search +- **Copy passwords, usernames, and TOTP codes** to clipboard (auto-clears after 60 seconds) +- **PIN unlock** - set up a PIN to avoid entering your master password every time +- **2FA support** - handles two-factor authentication during login +- **Offline capable** - once synced, vault data is cached locally +- **Auto-lock** - vault locks when Steam Deck goes to sleep + +## Installation + +1. Install [Decky Loader](https://github.com/SteamDeckHomebrew/decky-loader) on your Steam Deck +2. Download the latest release ZIP from [Releases](https://github.com/zoe/Decky-vaultwarden/releases) +3. In Decky Loader settings, go to Developer > Install Plugin from ZIP File +4. Select the downloaded ZIP file + +## Setup + +### First Login + +1. Open the Decky quick access menu and find the Vaultwarden plugin +2. Enter your server URL: + - For Bitwarden cloud: `https://api.bitwarden.com` + - For self-hosted: `https://your-vaultwarden-domain.com` +3. Enter your email and master password +4. If 2FA is enabled, enter your authenticator code + +### Setting Up PIN (Recommended) + +After logging in, click "Set Up PIN" to enable quick unlock: +- Enter a 4+ character PIN +- The PIN protects your encryption key locally +- After setup, you only need the PIN to unlock (not the full master password) +- After 5 failed PIN attempts, you'll need to login with master password again + +### API Key Login + +For automation or if you prefer API keys: +1. Get your API key from Vaultwarden Settings > Security > Keys > View API Key +2. Toggle "Use API Key" in the plugin +3. Enter your Client ID, Client Secret, and Email +4. You'll still need your master password once to decrypt the vault + +## Security + +- All decryption happens locally on your device +- Master password is never stored +- PIN is used to locally encrypt your vault key (not transmitted) +- Clipboard is cleared after 60 seconds +- Vault locks on Steam Deck sleep (configurable) +- 5 failed PIN attempts triggers logout + +## Building from Source + +### Prerequisites + +- Node.js v16.14+ and pnpm v9 +- Python 3.10+ with pip +- Docker (for backend builds) + +### Build + +```bash +# Install frontend dependencies +pnpm install + +# Build frontend +pnpm run build + +# The plugin ZIP will be in the out/ directory +``` + +## Keyboard Shortcuts + +- **Ctrl+Shift+L**: Lock vault + +## License + +MIT License - see [LICENSE](LICENSE) for details. diff --git a/backend/src/__init__.py b/backend/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/bitwarden_client.py b/backend/src/bitwarden_client.py new file mode 100644 index 0000000..50d6c9a --- /dev/null +++ b/backend/src/bitwarden_client.py @@ -0,0 +1,344 @@ +""" +Bitwarden/Vaultwarden REST API client. +Handles authentication, vault sync, and API communication. +""" +import json +from base64 import b64decode, b64encode +from typing import Optional, Tuple + +import aiohttp + +from .crypto import BitwardenCrypto, PinCrypto + + +# Default URLs +BITWARDEN_API_BASE = "https://api.bitwarden.com" +BITWARDEN_IDENTITY_BASE = "https://identity.bitwarden.com" + +# OAuth2 device type (2 = CLI) +DEVICE_TYPE = 15 # Use CLI device type for compatibility + + +class BitwardenClient: + """REST API client for Bitwarden/Vaultwarden.""" + + def __init__(self, server_url: Optional[str] = None): + self.server_url = server_url or BITWARDEN_API_BASE + self.identity_url = self._get_identity_url() + self.crypto = BitwardenCrypto() + self.pin_crypto = PinCrypto() + self.access_token: Optional[str] = None + self.refresh_token: Optional[str] = None + self.enc_key: Optional[bytes] = None + self.mac_key: Optional[bytes] = None + self.user_id: Optional[str] = None + self.email: Optional[str] = None + self.kdf_type: int = 0 + self.kdf_iterations: int = 600000 + self.kdf_memory: Optional[int] = None + self.kdf_parallelism: Optional[int] = None + self._vault_data: Optional[dict] = None + + def _get_identity_url(self) -> str: + """Get the identity endpoint URL.""" + if self.server_url: + base = self.server_url.rstrip("/") + # Handle Bitwarden cloud URLs + if base == "https://api.bitwarden.com": + return BITWARDEN_IDENTITY_BASE + if base == "https://api.bitwarden.eu": + return "https://identity.bitwarden.eu" + # Handle self-hosted URLs (Vaultwarden) + # Vaultwarden uses /identity directly + if "/identity" in base: + return base + if "/api" in base: + return base.replace("/api", "/identity") + return f"{base}/identity" + return BITWARDEN_IDENTITY_BASE + + def _get_api_url(self) -> str: + """Get the API endpoint URL.""" + if self.server_url: + base = self.server_url.rstrip("/") + # Handle Bitwarden cloud URLs + if base == "https://identity.bitwarden.com": + return BITWARDEN_API_BASE + if base == "https://identity.bitwarden.eu": + return "https://api.bitwarden.eu" + # Handle self-hosted URLs (Vaultwarden) + if "/api" in base: + return base + if "/identity" in base: + return base.replace("/identity", "/api") + return f"{base}/api" + return BITWARDEN_API_BASE + + async def _request( + self, + method: str, + url: str, + headers: dict = None, + data: dict = None, + params: dict = None, + ) -> dict: + """Make an HTTP request.""" + if headers is None: + headers = {} + headers.setdefault("Content-Type", "application/json") + headers.setdefault("Accept", "application/json") + headers.setdefault("Device-Type", str(DEVICE_TYPE)) + + async with aiohttp.ClientSession() as session: + async with session.request( + method, url, headers=headers, json=data, params=params + ) as resp: + text = await resp.text() + if resp.status >= 400: + try: + error_data = json.loads(text) + message = error_data.get("error_model", {}).get( + "message", text + ) + except (json.JSONDecodeError, KeyError): + message = text + raise Exception( + f"API error {resp.status}: {message}" + ) + return json.loads(text) if text else {} + + async def prelogin(self, email: str) -> dict: + """Get KDF settings for the user.""" + url = f"{self._get_api_url()}/accounts/prelogin" + data = {"email": email} + result = await self._request("POST", url, data=data) + + self.kdf_type = result.get("kdf", 0) + self.kdf_iterations = result.get("kdfIterations", 600000) + self.kdf_memory = result.get("kdfMemory") + self.kdf_parallelism = result.get("kdfParallelism") + self.email = email + + return result + + async def login_password( + self, email: str, password: str, two_factor_token: Optional[str] = None + ) -> dict: + """Login with email and master password.""" + await self.prelogin(email) + + # Derive master key + if self.kdf_type == 0: # PBKDF2 + master_key = self.crypto.derive_master_key_pbkdf2( + password, email, self.kdf_iterations + ) + elif self.kdf_type == 1: # Argon2id + master_key = self.crypto.derive_master_key_argon2( + password, + email, + self.kdf_iterations, + self.kdf_memory, + self.kdf_parallelism, + ) + else: + raise ValueError(f"Unsupported KDF type: {self.kdf_type}") + + # Hash master password for auth + master_password_hash = b64encode( + self.crypto.hmac_sha256( + master_key, password.encode("utf-8") + ) + ).decode("utf-8") + + # Build auth request + url = f"{self.identity_url}/connect/token" + data = { + "grant_type": "password", + "username": email, + "password": master_password_hash, + "scope": "api offline_access", + "client_id": "connector", + "deviceType": DEVICE_TYPE, + "deviceName": "decky-vaultwarden", + } + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + # Handle 2FA if needed + if two_factor_token: + data["twoFactorToken"] = two_factor_token + data["twoFactorProvider"] = "0" # Authenticator + data["twoFactorRemember"] = "1" + + # Use form data instead of JSON + async with aiohttp.ClientSession() as session: + async with session.post( + url, data=data, headers=headers + ) as resp: + text = await resp.text() + if resp.status >= 400: + try: + error_data = json.loads(text) + # Check if 2FA is required + if error_data.get("error") == "invalid_grant" and "twoFactor" in text: + return {"two_factor_required": True} + message = error_data.get("error_model", {}).get( + "message", text + ) + except (json.JSONDecodeError, KeyError): + message = text + raise Exception(f"Login failed: {message}") + result = json.loads(text) + + self.access_token = result.get("access_token") + self.refresh_token = result.get("refresh_token") + self.user_id = result.get("Profile", {}).get("id") or result.get("sub") + + # Get encrypted user key + enc_user_key = result.get("Key") + if enc_user_key: + self.enc_key, self.mac_key = self.crypto.decrypt_user_key( + enc_user_key, master_key + ) + return { + "success": True, + "master_key": master_key, + "enc_key": self.enc_key, + "mac_key": self.mac_key, + } + + return {"success": True, "master_key": master_key} + + async def login_api_key( + self, client_id: str, client_secret: str, email: str + ) -> dict: + """Login with API key (OAuth2 client_credentials).""" + url = f"{self.identity_url}/connect/token" + data = { + "grant_type": "client_credentials", + "scope": "api", + "client_id": client_id, + "client_secret": client_secret, + } + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + async with aiohttp.ClientSession() as session: + async with session.post( + url, data=data, headers=headers + ) as resp: + text = await resp.text() + if resp.status >= 400: + try: + error_data = json.loads(text) + message = error_data.get("error_model", {}).get( + "message", text + ) + except (json.JSONDecodeError, KeyError): + message = text + raise Exception(f"API key login failed: {message}") + result = json.loads(text) + + self.access_token = result.get("access_token") + self.user_id = result.get("sub") + + return { + "success": True, + "needs_master_password": True, + "message": "API key authenticated. Master password required for decryption.", + } + + async def unlock_with_master_password( + self, master_password: str + ) -> dict: + """Derive keys from master password for decryption.""" + if not self.email: + raise ValueError("Must login first") + + await self.prelogin(self.email) + + if self.kdf_type == 0: + master_key = self.crypto.derive_master_key_pbkdf2( + master_password, self.email, self.kdf_iterations + ) + elif self.kdf_type == 1: + master_key = self.crypto.derive_master_key_argon2( + master_password, + self.email, + self.kdf_iterations, + self.kdf_memory, + self.kdf_parallelism, + ) + else: + raise ValueError(f"Unsupported KDF type: {self.kdf_type}") + + return {"master_key": master_key} + + async def sync_vault(self) -> dict: + """Sync and decrypt the full vault.""" + if not self.access_token: + raise ValueError("Not authenticated") + + url = f"{self._get_api_url()}/sync" + headers = { + "Authorization": f"Bearer {self.access_token}", + } + params = {"excludeDomains": "true"} + + result = await self._request("GET", url, headers=headers, params=params) + self._vault_data = result + return result + + def decrypt_vault( + self, sync_data: dict, enc_key: bytes, mac_key: bytes + ) -> dict: + """Decrypt all items in the vault.""" + ciphers = sync_data.get("ciphers", []) + folders = sync_data.get("folders", []) + collections = sync_data.get("collections", []) + + # Decrypt folders + decrypted_folders = [] + for folder in folders: + name = "" + if folder.get("name"): + try: + name = self.crypto.decrypt_cipher_string( + folder["name"], enc_key, mac_key + ).decode("utf-8") + except Exception: + name = "[encrypted]" + decrypted_folders.append({ + "id": folder.get("id"), + "name": name, + "revisionDate": folder.get("revisionDate"), + }) + + # Decrypt ciphers + decrypted_ciphers = [] + for cipher in ciphers: + if cipher.get("type") in [1, 2, 3, 4]: # Login, Note, Card, Identity + decrypted = self.crypto.decrypt_cipher( + cipher, enc_key, mac_key + ) + if decrypted: + decrypted_ciphers.append(decrypted) + + # Build folder map + folder_map = {f["id"]: f["name"] for f in decrypted_folders} + + return { + "ciphers": decrypted_ciphers, + "folders": decrypted_folders, + "folder_map": folder_map, + "collections": collections, + } + + def get_kdf_info(self) -> dict: + """Get current KDF configuration.""" + return { + "kdf_type": self.kdf_type, + "kdf_iterations": self.kdf_iterations, + "kdf_memory": self.kdf_memory, + "kdf_parallelism": self.kdf_parallelism, + } diff --git a/backend/src/clipboard.py b/backend/src/clipboard.py new file mode 100644 index 0000000..dd6f791 --- /dev/null +++ b/backend/src/clipboard.py @@ -0,0 +1,123 @@ +""" +Clipboard management with 60-second auto-clear. +Uses xclip/xdotool for SteamOS (KDE Plasma). +""" +import asyncio +import subprocess +import time +from typing import Optional + +# Clipboard state +_clipboard_timers: dict = {} +_clipboard_contents: dict = {} + + +async def copy_to_clipboard(text: str, clear_after: int = 60, label: str = "default") -> dict: + """Copy text to clipboard with auto-clear after specified seconds.""" + global _clipboard_timers, _clipboard_contents + + try: + # Cancel any existing timer for this label + if label in _clipboard_timers: + _clipboard_timers[label].cancel() + + # Store the text + _clipboard_contents[label] = text + + # Copy to clipboard using xclip + process = await asyncio.create_subprocess_exec( + "xclip", "-selection", "clipboard", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await process.communicate(input=text.encode("utf-8")) + + if process.returncode != 0: + # Fallback to xsel if xclip not available + process = await asyncio.create_subprocess_exec( + "xsel", "--clipboard", "--input", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await process.communicate(input=text.encode("utf-8")) + + if process.returncode != 0: + return {"success": False, "error": "No clipboard tool available (xclip or xsel)"} + + # Set up auto-clear timer + async def clear_clipboard(): + await asyncio.sleep(clear_after) + await clear_clipboard_label(label) + + timer = asyncio.create_task(clear_clipboard()) + _clipboard_timers[label] = timer + + return { + "success": True, + "clear_after": clear_after, + "expires_at": int(time.time()) + clear_after, + } + + except FileNotFoundError: + return {"success": False, "error": "xclip/xsel not found. Install with: pacman -S xclip"} + except Exception as e: + return {"success": False, "error": str(e)} + + +async def clear_clipboard_label(label: str = "default") -> dict: + """Clear the clipboard for a specific label.""" + global _clipboard_timers, _clipboard_contents + + # Cancel timer if exists + if label in _clipboard_timers: + _clipboard_timers[label].cancel() + del _clipboard_timers[label] + + # Clear clipboard content + if label in _clipboard_contents: + del _clipboard_contents[label] + + # Clear the actual clipboard + try: + process = await asyncio.create_subprocess_exec( + "xclip", "-selection", "clipboard", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await process.communicate(input=b"") + except FileNotFoundError: + pass + + return {"success": True} + + +async def clear_all_clipboards() -> dict: + """Clear all clipboard contents and cancel all timers.""" + global _clipboard_timers, _clipboard_contents + + for label in list(_clipboard_timers.keys()): + _clipboard_timers[label].cancel() + + _clipboard_timers.clear() + _clipboard_contents.clear() + + await clear_clipboard_label("default") + + return {"success": True} + + +def get_clipboard_status(label: str = "default") -> dict: + """Get the status of clipboard content for a label.""" + global _clipboard_contents, _clipboard_timers + + if label not in _clipboard_contents: + return {"has_content": False} + + return { + "has_content": True, + "label": label, + "has_timer": label in _clipboard_timers, + } diff --git a/backend/src/crypto.py b/backend/src/crypto.py new file mode 100644 index 0000000..158bd58 --- /dev/null +++ b/backend/src/crypto.py @@ -0,0 +1,380 @@ +""" +Bitwarden/Vaultwarden cryptography implementation. +Handles KDF (PBKDF2, Argon2), AES-CBC-256, HMAC, HKDF, and vault decryption. +""" +import hashlib +import hmac +import os +import struct +from base64 import b64decode, b64encode +from typing import Optional, Tuple + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import hashes, padding +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.backends import default_backend + +# Bitwarden-specific constants +ENCRYPTION_KEY_LENGTH = 32 # 256 bits +MAC_KEY_LENGTH = 32 +HKDF_INFO_EMAIL = b"enc" +HKDF_INFO_MASTER_PASSWORD = b"enc" + +# Cipher type markers (ST arrays in Bitwarden) +CIPHER_TYPE_AES_CBC_256_B64 = "2." +CIPHER_TYPE_AES_CBC_256_HMAC_B64 = "3." +CIPHER_TYPE_RSA_2048_OAEP_SHA256 = "4." + + +class BitwardenCrypto: + """Core cryptographic operations for Bitwarden vault decryption.""" + + def __init__(self): + self.backend = default_backend() + + def derive_master_key_pbkdf2( + self, password: str, email: str, iterations: int + ) -> bytes: + """Derive master key using PBKDF2-SHA256.""" + salt = email.lower().strip().encode("utf-8") + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=ENCRYPTION_KEY_LENGTH, + salt=salt, + iterations=iterations, + backend=self.backend, + ) + return kdf.derive(password.encode("utf-8")) + + def derive_master_key_argon2( + self, + password: str, + email: str, + iterations: int, + memory: int, + parallelism: int, + ) -> bytes: + """Derive master key using Argon2id.""" + try: + import argon2 + salt = email.lower().strip().encode("utf-8") + return argon2.low_level.hash_secret_raw( + secret=password.encode("utf-8"), + salt=salt, + time_cost=iterations, + memory_cost=memory * 1024, # Convert MB to KB + parallelism=parallelism, + hash_len=ENCRYPTION_KEY_LENGTH, + type=argon2.low_level.Type.ID, + ) + except ImportError: + raise RuntimeError( + "Argon2 support requires argon2-cffi: pip install argon2-cffi" + ) + + def stretch_master_key(self, master_key: bytes) -> Tuple[bytes, bytes]: + """Stretch master key into encryption key + MAC key using HKDF.""" + # Bitwarden uses HKDF with empty salt and specific info strings + prk = hmac.new(b"", master_key, hashlib.sha256).digest() + + # Encryption key + enc_key = hmac.new( + prk, b"\x01" + HKDF_INFO_EMAIL + b"\x00", hashlib.sha256 + ).digest() + + # MAC key + mac_key = hmac.new( + prk, b"\x02" + HKDF_INFO_EMAIL + b"\x00", hashlib.sha256 + ).digest() + + return enc_key, mac_key + + def decrypt_aes_cbc_256( + self, key: bytes, iv: bytes, ciphertext: bytes + ) -> bytes: + """Decrypt data using AES-CBC-256.""" + cipher = Cipher( + algorithms.AES(key), modes.CBC(iv), backend=self.backend + ) + decryptor = cipher.decryptor() + padded = decryptor.update(ciphertext) + decryptor.finalize() + + # Remove PKCS7 padding + unpadder = padding.PKCS7(128).unpadder() + return unpadder.update(padded) + unpadder.finalize() + + def hmac_sha256(self, key: bytes, data: bytes) -> bytes: + """Compute HMAC-SHA256.""" + return hmac.new(key, data, hashlib.sha256).digest() + + def verify_mac( + self, mac_key: bytes, data: bytes, expected_mac: bytes + ) -> bool: + """Verify HMAC-SHA256.""" + computed = self.hmac_sha256(mac_key, data) + return hmac.compare_digest(computed, expected_mac) + + def decrypt_cipher_string(self, enc_string: str, enc_key: bytes, mac_key: bytes) -> bytes: + """ + Decrypt a Bitwarden CipherString. + + Format: TYPE.BASE64(IV) | BASE64(CT) | BASE64(MAC) + or: TYPE.BASE64(IV).BASE64(CT).BASE64(MAC) + """ + # Determine cipher type + if enc_string.startswith(CIPHER_TYPE_AES_CBC_256_B64): + parts = enc_string[2:].split("|") + if len(parts) == 2: + # Format: TYPE.BASE64(IV)|BASE64(CT) + iv_b64, ct_b64 = parts + iv = b64decode(iv_b64) + ct = b64decode(ct_b64) + return self.decrypt_aes_cbc_256(enc_key, iv, ct) + elif len(parts) == 3: + # Format: TYPE.BASE64(IV)|BASE64(CT)|BASE64(MAC) + iv_b64, ct_b64, mac_b64 = parts + iv = b64decode(iv_b64) + ct = b64decode(ct_b64) + mac = b64decode(mac_b64) + if not self.verify_mac(mac_key, iv + ct, mac): + raise ValueError("MAC verification failed") + return self.decrypt_aes_cbc_256(enc_key, iv, ct) + elif enc_string.startswith(CIPHER_TYPE_AES_CBC_256_HMAC_B64): + parts = enc_string[2:].split("|") + if len(parts) == 3: + iv_b64, ct_b64, mac_b64 = parts + iv = b64decode(iv_b64) + ct = b64decode(ct_b64) + mac = b64decode(mac_b64) + if not self.verify_mac(mac_key, iv + ct, mac): + raise ValueError("MAC verification failed") + return self.decrypt_aes_cbc_256(enc_key, iv, ct) + elif len(parts) == 2: + iv_b64, ct_b64 = parts + iv = b64decode(iv_b64) + ct = b64decode(ct_b64) + return self.decrypt_aes_cbc_256(enc_key, iv, ct) + + raise ValueError(f"Unsupported cipher type in: {enc_string[:10]}...") + + def decrypt_user_key( + self, encrypted_user_key: str, master_key: bytes + ) -> Tuple[bytes, bytes]: + """ + Decrypt the encrypted user key (Key from API response). + Returns (encryption_key, mac_key). + + The user key is encrypted with the stretched master key. + """ + stretched_enc, stretched_mac = self.stretch_master_key(master_key) + user_key = self.decrypt_cipher_string( + encrypted_user_key, stretched_enc, stretched_mac + ) + + if len(user_key) == 64: + # Has separate MAC key + return user_key[:32], user_key[32:] + elif len(user_key) == 32: + # No separate MAC key, derive from the key itself + return user_key, user_key + else: + raise ValueError(f"Unexpected user key length: {len(user_key)}") + + def decrypt_cipher( + self, + cipher_data: dict, + enc_key: bytes, + mac_key: bytes, + ) -> Optional[dict]: + """Decrypt a single cipher (vault item).""" + try: + # Decrypt name + name = "" + if cipher_data.get("name"): + try: + name = self.decrypt_cipher_string( + cipher_data["name"], enc_key, mac_key + ).decode("utf-8") + except Exception: + name = "[encrypted]" + + # Decrypt fields based on type + result = { + "id": cipher_data.get("id"), + "type": cipher_data.get("type"), + "name": name, + "folderId": cipher_data.get("folderId"), + "organizationId": cipher_data.get("organizationId"), + "favorite": cipher_data.get("favorite", False), + "revisionDate": cipher_data.get("revisionDate"), + } + + cipher_type = cipher_data.get("type") + + if cipher_type == 1: # Login + login = cipher_data.get("login", {}) + result["login"] = { + "username": self._decrypt_field( + login.get("username"), enc_key, mac_key + ), + "password": self._decrypt_field( + login.get("password"), enc_key, mac_key + ), + "totp": self._decrypt_field( + login.get("totp"), enc_key, mac_key + ), + "uris": [ + { + "uri": self._decrypt_field( + u.get("uri"), enc_key, mac_key + ), + "match": u.get("match"), + } + for u in login.get("uris", []) + ], + } + elif cipher_type == 2: # Secure Note + result["notes"] = self._decrypt_field( + cipher_data.get("notes"), enc_key, mac_key + ) + elif cipher_type == 3: # Card + card = cipher_data.get("card", {}) + result["card"] = { + "cardholderName": self._decrypt_field( + card.get("cardholderName"), enc_key, mac_key + ), + "brand": self._decrypt_field( + card.get("brand"), enc_key, mac_key + ), + "number": self._decrypt_field( + card.get("number"), enc_key, mac_key + ), + "expMonth": self._decrypt_field( + card.get("expMonth"), enc_key, mac_key + ), + "expYear": self._decrypt_field( + card.get("expYear"), enc_key, mac_key + ), + } + elif cipher_type == 4: # Identity + identity = cipher_data.get("identity", {}) + result["identity"] = { + "firstName": self._decrypt_field( + identity.get("firstName"), enc_key, mac_key + ), + "lastName": self._decrypt_field( + identity.get("lastName"), enc_key, mac_key + ), + "email": self._decrypt_field( + identity.get("email"), enc_key, mac_key + ), + "phone": self._decrypt_field( + identity.get("phone"), enc_key, mac_key + ), + } + + # Decrypt custom fields + fields = [] + for field in cipher_data.get("fields", []): + fields.append({ + "name": self._decrypt_field( + field.get("name"), enc_key, mac_key + ), + "value": self._decrypt_field( + field.get("value"), enc_key, mac_key + ), + "type": field.get("type"), + "hidden": field.get("hidden", False), + }) + result["fields"] = fields + + # Decrypt notes + if cipher_data.get("notes") and cipher_type != 2: + result["notes"] = self._decrypt_field( + cipher_data.get("notes"), enc_key, mac_key + ) + + return result + except Exception as e: + # Return partial result with error info + return { + "id": cipher_data.get("id"), + "type": cipher_data.get("type"), + "name": f"[decryption error: {str(e)}]", + "error": True, + } + + def _decrypt_field( + self, + value: Optional[str], + enc_key: bytes, + mac_key: bytes, + ) -> Optional[str]: + """Decrypt a single field value.""" + if not value: + return None + try: + decrypted = self.decrypt_cipher_string(value, enc_key, mac_key) + return decrypted.decode("utf-8") + except Exception: + return "[encrypted]" + + +# PIN-related crypto +class PinCrypto: + """Handles PIN-based vault unlock (PasswordProtectedKeyEnvelope).""" + + def __init__(self): + self.crypto = BitwardenCrypto() + + def derive_pin_key( + self, pin: str, salt: str, kdf_iterations: int = 200000 + ) -> bytes: + """Derive a key from the PIN using PBKDF2.""" + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=ENCRYPTION_KEY_LENGTH, + salt=salt.encode("utf-8") if isinstance(salt, str) else salt, + iterations=kdf_iterations, + backend=default_backend(), + ) + return kdf.derive(pin.encode("utf-8")) + + def encrypt_user_key_for_pin( + self, user_key: bytes, pin_key: bytes + ) -> Tuple[bytes, bytes]: + """Encrypt the user key with the PIN-derived key. Returns (encrypted_key, iv).""" + iv = os.urandom(16) + cipher = Cipher( + algorithms.AES(pin_key), modes.CBC(iv), backend=default_backend() + ) + encryptor = cipher.encryptor() + + # Pad user key with PKCS7 + padder = padding.PKCS7(128).padder() + padded = padder.update(user_key) + padder.finalize() + + encrypted = encryptor.update(padded) + encryptor.finalize() + + # Compute HMAC + mac = self.crypto.hmac_sha256(pin_key, iv + encrypted) + + return iv + encrypted + mac, iv + + def decrypt_user_key_with_pin( + self, encrypted_envelope: bytes, pin_key: bytes + ) -> bytes: + """Decrypt the user key using the PIN-derived key.""" + # Envelope format: IV (16) + EncryptedData (32) + MAC (32) + if len(encrypted_envelope) < 80: + raise ValueError("Invalid PIN envelope length") + + iv = encrypted_envelope[:16] + mac = encrypted_envelope[-32:] + ct = encrypted_envelope[16:-32] + + # Verify HMAC + if not self.crypto.verify_mac(pin_key, iv + ct, mac): + raise ValueError("PIN MAC verification failed") + + return self.crypto.decrypt_aes_cbc_256(pin_key, iv, ct) diff --git a/backend/src/totp.py b/backend/src/totp.py new file mode 100644 index 0000000..f112475 --- /dev/null +++ b/backend/src/totp.py @@ -0,0 +1,107 @@ +""" +TOTP (Time-based One-Time Password) generation for Bitwarden vault items. +""" +import hashlib +import hmac +import struct +import time +from typing import Optional + +# Try to use pyotp if available, otherwise use manual implementation +try: + import pyotp + HAS_PYOTP = True +except ImportError: + HAS_PYOTP = False + + +def generate_totp(secret: str, period: int = 30, digits: int = 6) -> str: + """Generate a TOTP code from a secret.""" + if not secret: + return "" + + # Clean the secret (remove spaces, convert to uppercase) + secret = secret.replace(" ", "").upper() + + # Try to parse as otpauth:// URI + if secret.startswith("otpauth://"): + parsed = _parse_otpauth_uri(secret) + if parsed: + secret = parsed["secret"] + period = parsed.get("period", period) + digits = parsed.get("digits", digits) + + if HAS_PYOTP: + totp = pyotp.TOTP(secret, interval=period, digits=digits) + return totp.now() + + return _generate_totp_manual(secret, period, digits) + + +def get_totp_remaining_seconds(period: int = 30) -> int: + """Get seconds remaining until current TOTP code expires.""" + return period - (int(time.time()) % period) + + +def _generate_totp_manual(secret: str, period: int, digits: int) -> str: + """Manual TOTP implementation when pyotp is not available.""" + # Decode base32 secret + _BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + secret = secret.upper() + + # Remove padding + padding_needed = (8 - len(secret) % 8) % 8 + secret += "=" * padding_needed + + # Decode base32 + binary = b"" + for char in secret: + if char == "=": + continue + try: + val = _BASE32_CHARS.index(char) + except ValueError: + continue + binary += struct.pack(">B", val) + + # Time counter + counter = int(time.time()) // period + counter_bytes = struct.pack(">Q", counter) + + # HMAC-SHA1 + hmac_result = hmac.new(binary, counter_bytes, hashlib.sha1).digest() + + # Dynamic truncation + offset = hmac_result[-1] & 0x0F + truncated = struct.unpack( + ">I", hmac_result[offset : offset + 4] + )[0] + truncated &= 0x7FFFFFFF + + # Generate code + code = truncated % (10 ** digits) + return str(code).zfill(digits) + + +def _parse_otpauth_uri(uri: str) -> Optional[dict]: + """Parse an otpauth:// URI.""" + # otpauth://totp/Label?secret=XXX&issuer=XXX&period=30&digits=6 + if not uri.startswith("otpauth://"): + return None + + parts = uri.split("?", 1) + if len(parts) < 2: + return None + + params = {} + for param in parts[1].split("&"): + key, _, value = param.partition("=") + params[key] = value + + secret = params.get("secret", "") + return { + "secret": secret, + "issuer": params.get("issuer", ""), + "period": int(params.get("period", "30")), + "digits": int(params.get("digits", "6")), + } diff --git a/decky.pyi b/decky.pyi new file mode 100644 index 0000000..db71dfd --- /dev/null +++ b/decky.pyi @@ -0,0 +1,38 @@ +""" +Type stubs for Decky plugin development. +""" +from typing import Any, Callable, Optional + + +class logger: + """Plugin logger.""" + @staticmethod + def info(msg: str) -> None: ... + @staticmethod + def warning(msg: str) -> None: ... + @staticmethod + def error(msg: str) -> None: ... + @staticmethod + def debug(msg: str) -> None: ... + + +class plugin: + """Plugin decorator and utilities.""" + @staticmethod + def call(method: str, *args: Any) -> Any: ... + + +# Plugin lifecycle methods +async def _main() -> None: + """Called when plugin loads.""" + pass + + +async def _unload() -> None: + """Called when plugin unloads.""" + pass + + +async def _migration() -> None: + """Called on plugin version change.""" + pass diff --git a/defaults/settings.json b/defaults/settings.json new file mode 100644 index 0000000..c3dad62 --- /dev/null +++ b/defaults/settings.json @@ -0,0 +1,9 @@ +{ + "server_url": "https://api.bitwarden.com", + "email": "", + "pin_enabled": false, + "auto_lock_on_sleep": true, + "clipboard_clear_seconds": 60, + "require_password_on_restart": true, + "last_sync": null +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..321d0fa --- /dev/null +++ b/main.py @@ -0,0 +1,418 @@ +""" +Decky Vaultwarden - Bitwarden/Vaultwarden password manager plugin for Decky Loader. + +Main entry point for the Python backend. Provides API routes for the frontend. +""" +import json +import os +import time +from typing import Optional + +import decky_plugin +from .bitwarden_client import BitwardenClient +from .crypto import BitwardenCrypto, PinCrypto +from .totp import generate_totp, get_totp_remaining_seconds +from .clipboard import copy_to_clipboard, clear_all_clipboards + + +class Plugin: + """Main plugin class for Decky Vaultwarden.""" + + def __init__(self): + self.client: Optional[BitwardenClient] = None + self.pin_crypto = PinCrypto() + self.crypto = BitwardenCrypto() + self._vault_data: Optional[dict] = None + self._decrypted_vault: Optional[dict] = None + self._master_key: Optional[bytes] = None + self._enc_key: Optional[bytes] = None + self._mac_key: Optional[bytes] = None + self._session_active: bool = False + self._settings_path = os.path.join( + os.path.expanduser("~"), ".config", "decky-vaultwarden" + ) + self._pin_attempts = 0 + self._MAX_PIN_ATTEMPTS = 5 + + # ===== Lifecycle Methods ===== + + async def _main(self): + """Called when the plugin is loaded.""" + decky_plugin.logger.info("Decky Vaultwarden plugin loaded") + os.makedirs(self._settings_path, exist_ok=True) + + async def _unload(self): + """Called when the plugin is unloaded.""" + await self.lock_vault() + decky_plugin.logger.info("Decky Vaultwarden plugin unloaded") + + async def _migration(self): + """Called when plugin version changes.""" + pass + + # ===== Authentication ===== + + async def login_password( + self, server_url: str, email: str, password: str, two_factor_token: Optional[str] = None + ) -> dict: + """Login with email and master password.""" + try: + self.client = BitwardenClient(server_url) + result = await self.client.login_password(email, password, two_factor_token) + + if result.get("two_factor_required"): + return {"two_factor_required": True} + + if result.get("success"): + self._master_key = result.get("master_key") + self._enc_key = result.get("enc_key") + self._mac_key = result.get("mac_key") + self._session_active = True + self._save_settings({ + "server_url": server_url, + "email": email, + "kdf_info": self.client.get_kdf_info(), + }) + return {"success": True, "needs_unlock": result.get("enc_key") is None} + + return {"success": False, "error": "Login failed"} + + except Exception as e: + return {"success": False, "error": str(e)} + + async def login_api_key( + self, server_url: str, client_id: str, client_secret: str, email: str + ) -> dict: + """Login with API key.""" + try: + self.client = BitwardenClient(server_url) + result = await self.client.login_api_key(client_id, client_secret, email) + + if result.get("success"): + self._session_active = True + self._save_settings({ + "server_url": server_url, + "email": email, + "auth_method": "api_key", + "kdf_info": self.client.get_kdf_info(), + }) + return { + "success": True, + "needs_master_password": True, + "message": "API key authenticated. Enter master password to unlock vault.", + } + + return {"success": False, "error": result.get("error", "API key login failed")} + + except Exception as e: + return {"success": False, "error": str(e)} + + async def unlock_with_master_password(self, password: str) -> dict: + """Unlock vault with master password (for API key users or fresh login).""" + try: + if not self.client: + return {"success": False, "error": "Not authenticated. Login first."} + + result = await self.client.unlock_with_master_password(password) + self._master_key = result.get("master_key") + + # Sync and decrypt vault + sync_data = await self.client.sync_vault() + self._vault_data = sync_data + + # Get encryption keys from login or derive them + if not self._enc_key and self._master_key: + enc_user_key = sync_data.get("profile", {}).get("key") + if enc_user_key: + self._enc_key, self._mac_key = self.crypto.decrypt_user_key( + enc_user_key, self._master_key + ) + + if self._enc_key and self._mac_key: + self._decrypted_vault = self.client.decrypt_vault( + sync_data, self._enc_key, self._mac_key + ) + return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))} + + return {"success": False, "error": "Could not derive encryption keys"} + + except Exception as e: + return {"success": False, "error": str(e)} + + # ===== Vault Operations ===== + + async def get_vault_items(self) -> dict: + """Get decrypted vault items.""" + if not self._decrypted_vault: + return {"success": False, "error": "Vault not unlocked"} + return {"success": True, "data": self._decrypted_vault} + + async def get_folders(self) -> dict: + """Get decrypted folders.""" + if not self._decrypted_vault: + return {"success": False, "error": "Vault not unlocked"} + return { + "success": True, + "folders": self._decrypted_vault.get("folders", []), + } + + async def search_vault(self, query: str) -> dict: + """Search vault items by name, username, or URI.""" + if not self._decrypted_vault: + return {"success": False, "error": "Vault not unlocked"} + + query_lower = query.lower() + results = [] + + for cipher in self._decrypted_vault.get("ciphers", []): + if cipher.get("error"): + continue + + # Search in name + name = cipher.get("name", "") + if query_lower in name.lower(): + results.append(cipher) + continue + + # Search in login fields + login = cipher.get("login", {}) + username = login.get("username", "") or "" + if query_lower in username.lower(): + results.append(cipher) + continue + + # Search in URIs + for uri in login.get("uris", []): + uri_str = uri.get("uri", "") or "" + if query_lower in uri_str.lower(): + results.append(cipher) + break + + return {"success": True, "results": results} + + # ===== Credential Copy ===== + + async def copy_password(self, cipher_id: str) -> dict: + """Copy a password to clipboard with 60s auto-clear.""" + password = self._get_cipher_field(cipher_id, "password") + if password is None: + return {"success": False, "error": "Password not found"} + return await copy_to_clipboard(password, clear_after=60, label=f"password_{cipher_id}") + + async def copy_username(self, cipher_id: str) -> dict: + """Copy a username to clipboard with 60s auto-clear.""" + username = self._get_cipher_field(cipher_id, "username") + if username is None: + return {"success": False, "error": "Username not found"} + return await copy_to_clipboard(username, clear_after=60, label=f"username_{cipher_id}") + + async def copy_totp(self, cipher_id: str) -> dict: + """Copy TOTP code to clipboard with 60s auto-clear.""" + totp_secret = self._get_cipher_field(cipher_id, "totp") + if totp_secret: + code = generate_totp(totp_secret) + remaining = get_totp_remaining_seconds() + result = await copy_to_clipboard(code, clear_after=min(remaining, 30), label=f"totp_{cipher_id}") + result["remaining_seconds"] = remaining + return result + return {"success": False, "error": "TOTP not configured"} + + async def get_totp_code(self, cipher_id: str) -> dict: + """Get current TOTP code without copying.""" + totp_secret = self._get_cipher_field(cipher_id, "totp") + if totp_secret: + code = generate_totp(totp_secret) + remaining = get_totp_remaining_seconds() + return {"success": True, "code": code, "remaining_seconds": remaining} + return {"success": False, "error": "TOTP not configured"} + + # ===== PIN Management ===== + + async def setup_pin(self, pin: str) -> dict: + """Set up PIN for vault unlock.""" + try: + if not self._enc_key: + return {"success": False, "error": "Vault not unlocked. Login first."} + + settings = self._load_settings() + kdf_info = settings.get("kdf_info", {}) + email = settings.get("email", "") + + # Generate salt for PIN key derivation + pin_salt = f"{email}:{pin}".lower() + + # Derive PIN key + pin_key = self.pin_crypto.derive_pin_key( + pin, pin_salt, kdf_iterations=200000 + ) + + # Encrypt user key with PIN key + envelope, iv = self.pin_crypto.encrypt_user_key_for_pin( + self._enc_key, pin_key + ) + + # Save PIN settings + settings["pin_enabled"] = True + settings["pin_salt"] = pin_salt + settings["pin_kdf_iterations"] = 200000 + settings["pin_envelope"] = envelope.hex() + settings["pin_envelope_iv"] = iv.hex() + self._save_settings(settings) + + self._pin_attempts = 0 + return {"success": True} + + except Exception as e: + return {"success": False, "error": str(e)} + + async def unlock_with_pin(self, pin: str) -> dict: + """Unlock vault using PIN.""" + try: + settings = self._load_settings() + + if not settings.get("pin_enabled"): + return {"success": False, "error": "PIN not configured"} + + if self._pin_attempts >= self._MAX_PIN_ATTEMPTS: + return {"success": False, "error": "Too many failed attempts. Login with master password."} + + pin_salt = settings.get("pin_salt", "") + pin_iterations = settings.get("pin_kdf_iterations", 200000) + envelope_hex = settings.get("pin_envelope", "") + + if not envelope_hex or not pin_salt: + return {"success": False, "error": "Invalid PIN configuration"} + + # Derive PIN key + pin_key = self.pin_crypto.derive_pin_key( + pin, pin_salt, kdf_iterations=pin_iterations + ) + + # Decrypt user key from envelope + envelope = bytes.fromhex(envelope_hex) + user_key = self.pin_crypto.decrypt_user_key_with_pin(envelope, pin_key) + + if len(user_key) == 64: + self._enc_key = user_key[:32] + self._mac_key = user_key[32:] + elif len(user_key) == 32: + self._enc_key = user_key + self._mac_key = user_key + else: + raise ValueError("Invalid decrypted user key length") + + # Re-sync and decrypt vault + if self.client: + sync_data = await self.client.sync_vault() + self._vault_data = sync_data + self._decrypted_vault = self.client.decrypt_vault( + sync_data, self._enc_key, self._mac_key + ) + self._session_active = True + self._pin_attempts = 0 + return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))} + + return {"success": False, "error": "No active session"} + + except Exception as e: + self._pin_attempts += 1 + remaining = self._MAX_PIN_ATTEMPTS - self._pin_attempts + return { + "success": False, + "error": f"Invalid PIN. {remaining} attempts remaining.", + "attempts_remaining": remaining, + } + + async def remove_pin(self) -> dict: + """Remove PIN configuration.""" + settings = self._load_settings() + settings["pin_enabled"] = False + settings.pop("pin_salt", None) + settings.pop("pin_kdf_iterations", None) + settings.pop("pin_envelope", None) + settings.pop("pin_envelope_iv", None) + self._save_settings(settings) + return {"success": True} + + async def is_pin_enabled(self) -> dict: + """Check if PIN is configured.""" + settings = self._load_settings() + return {"enabled": settings.get("pin_enabled", False)} + + # ===== Vault Lock ===== + + async def lock_vault(self) -> dict: + """Lock the vault and clear sensitive data.""" + await clear_all_clipboards() + self._decrypted_vault = None + self._vault_data = None + self._enc_key = None + self._mac_key = None + self._session_active = False + self._pin_attempts = 0 + return {"success": True} + + async def logout(self) -> dict: + """Full logout - clear all data.""" + await self.lock_vault() + self._master_key = None + self.client = None + return {"success": True} + + # ===== Settings ===== + + async def get_settings(self) -> dict: + """Get plugin settings.""" + return {"success": True, "settings": self._load_settings()} + + async def update_settings(self, new_settings: dict) -> dict: + """Update plugin settings.""" + settings = self._load_settings() + settings.update(new_settings) + self._save_settings(settings) + return {"success": True} + + async def get_status(self) -> dict: + """Get current plugin status.""" + return { + "session_active": self._session_active, + "vault_loaded": self._decrypted_vault is not None, + "cipher_count": len(self._decrypted_vault.get("ciphers", [])) if self._decrypted_vault else 0, + "folder_count": len(self._decrypted_vault.get("folders", [])) if self._decrypted_vault else 0, + } + + # ===== Internal Helpers ===== + + def _get_cipher_field(self, cipher_id: str, field: str): + """Get a field from a decrypted cipher.""" + if not self._decrypted_vault: + return None + + for cipher in self._decrypted_vault.get("ciphers", []): + if cipher.get("id") == cipher_id: + if field == "password": + return cipher.get("login", {}).get("password") + elif field == "username": + return cipher.get("login", {}).get("username") + elif field == "totp": + return cipher.get("login", {}).get("totp") + elif field == "name": + return cipher.get("name") + return None + return None + + def _load_settings(self) -> dict: + """Load settings from disk.""" + settings_file = os.path.join(self._settings_path, "settings.json") + try: + with open(settings_file, "r") as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + def _save_settings(self, settings: dict): + """Save settings to disk.""" + os.makedirs(self._settings_path, exist_ok=True) + settings_file = os.path.join(self._settings_path, "settings.json") + with open(settings_file, "w") as f: + json.dump(settings, f, indent=2) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a1a5d9e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2210 @@ +{ + "name": "decky-vaultwarden", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decky-vaultwarden", + "version": "1.0.0", + "dependencies": { + "@decky/api": "^1.0.0", + "@decky/ui": "^4.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@decky/rollup": "^1.0.0", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-typescript": "^11.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "rollup": "^4.0.0", + "rollup-plugin-import-css": "^3.5.0", + "tslib": "^2.6.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@decky/api": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@decky/api/-/api-1.1.3.tgz", + "integrity": "sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==", + "license": "LGPL-2.1" + }, + "node_modules/@decky/rollup": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@decky/rollup/-/rollup-1.0.2.tgz", + "integrity": "sha512-ixuHH3msw156ACbgWZi4hgccdzDBIuqYGOVja8sLX4lHFgaWUKji1vomDi7Dk1CamgjmKt+Dqf75Pezv2FB6Bw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.7", + "@rollup/plugin-typescript": "^11.1.6", + "merge-anything": "^6.0.2", + "rollup": "^4.18.0", + "rollup-plugin-delete": "^2.0.0", + "rollup-plugin-external-globals": "^0.11.0", + "rollup-plugin-import-assets": "^1.1.1", + "tslib": "^2.6.3", + "typescript": "^5.5.3" + } + }, + "node_modules/@decky/rollup/node_modules/@rollup/plugin-commonjs": { + "version": "26.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.3.tgz", + "integrity": "sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^10.4.1", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@decky/rollup/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@decky/rollup/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@decky/ui": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@decky/ui/-/ui-4.12.0.tgz", + "integrity": "sha512-OwO1XEi+tNI5jRyCs6w2526o+u9URjh1A1gtfUld7HUpAcxNVthgcCySaeLtvQqTPTj7IeiDCEy7rRS88Fd+wQ==", + "license": "LGPL-2.1" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", + "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", + "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.0.tgz", + "integrity": "sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.0.tgz", + "integrity": "sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.0.tgz", + "integrity": "sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.0.tgz", + "integrity": "sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.0.tgz", + "integrity": "sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.0.tgz", + "integrity": "sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.0.tgz", + "integrity": "sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.0.tgz", + "integrity": "sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.0.tgz", + "integrity": "sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.0.tgz", + "integrity": "sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.0.tgz", + "integrity": "sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.0.tgz", + "integrity": "sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.0.tgz", + "integrity": "sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.0.tgz", + "integrity": "sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.0.tgz", + "integrity": "sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.0.tgz", + "integrity": "sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.0.tgz", + "integrity": "sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.0.tgz", + "integrity": "sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.0.tgz", + "integrity": "sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.0.tgz", + "integrity": "sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.0.tgz", + "integrity": "sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.0.tgz", + "integrity": "sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.0.tgz", + "integrity": "sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.0.tgz", + "integrity": "sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.0.tgz", + "integrity": "sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.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" + }, + "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", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-anything": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-6.0.6.tgz", + "integrity": "sha512-F3K1W45PvTjRZzbcYIhXntNr8cux00gUxR8IzNPPG+80gNlAHZGVBwFyN4x5yjw/7QkLPKDbRQBK4KrJKo69mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.0.tgz", + "integrity": "sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.0", + "@rollup/rollup-android-arm64": "4.63.0", + "@rollup/rollup-darwin-arm64": "4.63.0", + "@rollup/rollup-darwin-x64": "4.63.0", + "@rollup/rollup-freebsd-arm64": "4.63.0", + "@rollup/rollup-freebsd-x64": "4.63.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.0", + "@rollup/rollup-linux-arm-musleabihf": "4.63.0", + "@rollup/rollup-linux-arm64-gnu": "4.63.0", + "@rollup/rollup-linux-arm64-musl": "4.63.0", + "@rollup/rollup-linux-loong64-gnu": "4.63.0", + "@rollup/rollup-linux-loong64-musl": "4.63.0", + "@rollup/rollup-linux-ppc64-gnu": "4.63.0", + "@rollup/rollup-linux-ppc64-musl": "4.63.0", + "@rollup/rollup-linux-riscv64-gnu": "4.63.0", + "@rollup/rollup-linux-riscv64-musl": "4.63.0", + "@rollup/rollup-linux-s390x-gnu": "4.63.0", + "@rollup/rollup-linux-x64-gnu": "4.63.0", + "@rollup/rollup-linux-x64-musl": "4.63.0", + "@rollup/rollup-openbsd-x64": "4.63.0", + "@rollup/rollup-openharmony-arm64": "4.63.0", + "@rollup/rollup-win32-arm64-msvc": "4.63.0", + "@rollup/rollup-win32-ia32-msvc": "4.63.0", + "@rollup/rollup-win32-x64-gnu": "4.63.0", + "@rollup/rollup-win32-x64-msvc": "4.63.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-delete": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-delete/-/rollup-plugin-delete-2.2.0.tgz", + "integrity": "sha512-REKtDKWvjZlbrWpPvM9X/fadCs3E9I9ge27AK8G0e4bXwSLeABAAwtjiI1u3ihqZxk6mJeB2IVeSbH4DtOcw7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^6.1.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "rollup": "*" + } + }, + "node_modules/rollup-plugin-external-globals": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.11.0.tgz", + "integrity": "sha512-LR+sH2WkgWMPxsA5o5rT7uW7BeWXSeygLe60QQi9qoN/ufaCuHDaVOIbndIkqDPnZt/wZugJh5DCzkZFdSWlLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.2", + "magic-string": "^0.30.10" + }, + "peerDependencies": { + "rollup": "^2.25.0 || ^3.3.0 || ^4.1.4" + } + }, + "node_modules/rollup-plugin-external-globals/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/rollup-plugin-external-globals/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/rollup-plugin-import-assets": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-import-assets/-/rollup-plugin-import-assets-1.1.1.tgz", + "integrity": "sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-pluginutils": "^2.7.1", + "url-join": "^4.0.1" + }, + "peerDependencies": { + "rollup": ">=1.9.0" + } + }, + "node_modules/rollup-plugin-import-css": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.8.tgz", + "integrity": "sha512-a3YsZnwHz66mRHCKHjaPCSfWczczvS/HTkgDc+Eogn0mt/0JZXz0WjK0fzM5WwBpVtOqHB4/gHdmEY40ILsaVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "rollup": "^2.x.x || ^3.x.x || ^4.x.x" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "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/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..706b713 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "decky-vaultwarden", + "version": "1.0.0", + "description": "Bitwarden/Vaultwarden password manager plugin for Decky Loader", + "type": "module", + "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@decky/api": "^1.0.0", + "@decky/ui": "^4.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@decky/rollup": "^1.0.0", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-typescript": "^11.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "rollup": "^4.0.0", + "rollup-plugin-import-css": "^3.5.0", + "tslib": "^2.6.0", + "typescript": "^5.3.0" + } +} diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..7312ae6 --- /dev/null +++ b/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "Vaultwarden", + "author": "JustZoe101", + "flags": ["needs_community_python"], + "api_version": 1, + "content": { + "name": "Vaultwarden - Password Manager", + "description": "Access your Bitwarden or Vaultwarden passwords directly from the Steam Deck. Copy passwords, usernames, and TOTP codes to clipboard with a 60-second auto-clear.", + "version": "1.0.0", + "tags": ["security", "passwords", "bitwarden", "vaultwarden"], + "pic_value": "https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/login-logo.png" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2b6db9a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +aiohttp>=3.9.0 +pyotp>=2.9.0 +cryptography>=42.0.0 +argon2-cffi>=23.1.0 diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..4863432 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,5 @@ +import deckyPlugin from "@decky/rollup"; + +export default deckyPlugin({ + // Extra rollup options if needed +}); diff --git a/src/api/backend.ts b/src/api/backend.ts new file mode 100644 index 0000000..7f42852 --- /dev/null +++ b/src/api/backend.ts @@ -0,0 +1,192 @@ +/** + * Frontend <-> Backend API bridge. + * Calls Python backend methods via Decky's call function. + */ +import { call } from "@decky/api"; + +// Types +export interface VaultItem { + id: string; + type: number; + name: string; + folderId?: string; + organizationId?: string; + favorite?: boolean; + revisionDate?: string; + login?: { + username?: string; + password?: string; + totp?: string; + uris?: { uri?: string; match?: number }[]; + }; + notes?: string; + card?: { + cardholderName?: string; + brand?: string; + number?: string; + expMonth?: string; + expYear?: string; + }; + identity?: { + firstName?: string; + lastName?: string; + email?: string; + phone?: string; + }; + fields?: { name?: string; value?: string; type?: number; hidden?: boolean }[]; + error?: boolean; +} + +export interface Folder { + id: string; + name: string; + revisionDate?: string; +} + +export interface VaultData { + ciphers: VaultItem[]; + folders: Folder[]; + folder_map: Record; + collections: any[]; +} + +export interface LoginResult { + success?: boolean; + two_factor_required?: boolean; + needs_unlock?: boolean; + error?: string; +} + +export interface CopyResult { + success?: boolean; + clear_after?: number; + expires_at?: number; + remaining_seconds?: number; + error?: string; +} + +export interface ApiKeyLoginResult { + success?: boolean; + needs_master_password?: boolean; + message?: string; + error?: string; +} + +// API wrapper using new Decky call API +class VaultApi { + private async call(method: string, ...args: any[]): Promise { + try { + const result = await call(method, ...args); + return result as T; + } catch (error: any) { + throw new Error(error.message || "Backend error"); + } + } + + // Authentication + async loginPassword( + serverUrl: string, + email: string, + password: string, + twoFactorToken?: string + ): Promise { + return this.call( + "login_password", + serverUrl, + email, + password, + twoFactorToken + ); + } + + async loginApiKey( + serverUrl: string, + clientId: string, + clientSecret: string, + email: string + ): Promise { + return this.call( + "login_api_key", + serverUrl, + clientId, + clientSecret, + email + ); + } + + async unlockWithMasterPassword(password: string): Promise { + return this.call("unlock_with_master_password", password); + } + + // Vault + async getVaultItems(): Promise<{ success: boolean; data: VaultData }> { + return this.call("get_vault_items"); + } + + async searchVault(query: string): Promise<{ success: boolean; results: VaultItem[] }> { + return this.call("search_vault", query); + } + + // Credential copy + async copyPassword(cipherId: string): Promise { + return this.call("copy_password", cipherId); + } + + async copyUsername(cipherId: string): Promise { + return this.call("copy_username", cipherId); + } + + async copyTotp(cipherId: string): Promise { + return this.call("copy_totp", cipherId); + } + + async getTotpCode(cipherId: string): Promise<{ success: boolean; code?: string; remaining_seconds?: number; error?: string }> { + return this.call("get_totp_code", cipherId); + } + + // PIN + async setupPin(pin: string): Promise<{ success: boolean; error?: string }> { + return this.call("setup_pin", pin); + } + + async unlockWithPin(pin: string): Promise { + return this.call("unlock_with_pin", pin); + } + + async removePin(): Promise<{ success: boolean }> { + return this.call("remove_pin"); + } + + async isPinEnabled(): Promise<{ enabled: boolean }> { + return this.call("is_pin_enabled"); + } + + // Session + async lockVault(): Promise<{ success: boolean }> { + return this.call("lock_vault"); + } + + async logout(): Promise<{ success: boolean }> { + return this.call("logout"); + } + + async getStatus(): Promise<{ + session_active: boolean; + vault_loaded: boolean; + cipher_count: number; + folder_count: number; + }> { + return this.call("get_status"); + } + + // Settings + async getSettings(): Promise<{ success: boolean; settings: any }> { + return this.call("get_settings"); + } + + async updateSettings(settings: any): Promise<{ success: boolean }> { + return this.call("update_settings", settings); + } +} + +export const vaultApi = new VaultApi(); diff --git a/src/components/ItemCard.tsx b/src/components/ItemCard.tsx new file mode 100644 index 0000000..1535f48 --- /dev/null +++ b/src/components/ItemCard.tsx @@ -0,0 +1,303 @@ +import { useState, useEffect } from "react"; +import { ButtonItem } from "@decky/ui"; +import { VaultItem } from "../api/backend"; + +interface ItemCardProps { + item: VaultItem; + folderMap: Record; + copyStatus: { field: string; cipherId: string; expiresAt: number } | null; + onCopyPassword: (cipherId: string) => Promise; + onCopyUsername: (cipherId: string) => Promise; + onCopyTotp: (cipherId: string) => Promise; +} + +const CIPHER_TYPE_NAMES: Record = { + 1: "Login", + 2: "Secure Note", + 3: "Card", + 4: "Identity", +}; + +const CIPHER_TYPE_ICONS: Record = { + 1: "\uD83D\uDD11", // key + 2: "\uD83D\uDCDD", // note + 3: "\uD83D\uDCB3", // card + 4: "\uD83D\uDC64", // person +}; + +export function ItemCard({ + item, + folderMap, + copyStatus, + onCopyPassword, + onCopyUsername, + onCopyTotp, +}: ItemCardProps) { + const [expanded, setExpanded] = useState(false); + const [copying, setCopying] = useState(null); + + const isCopied = (field: string) => + copyStatus?.cipherId === item.id && copyStatus?.field === field; + + const getCopyExpiresIn = () => { + if (!copyStatus) return 0; + return Math.max(0, Math.ceil(copyStatus.expiresAt - Date.now() / 1000)); + }; + + const [expiresIn, setExpiresIn] = useState(getCopyExpiresIn()); + + useEffect(() => { + const interval = setInterval(() => { + setExpiresIn(getCopyExpiresIn()); + }, 1000); + return () => clearInterval(interval); + }, [copyStatus]); + + const handleCopy = async (field: string) => { + setCopying(field); + try { + switch (field) { + case "password": + await onCopyPassword(item.id); + break; + case "username": + await onCopyUsername(item.id); + break; + case "totp": + await onCopyTotp(item.id); + break; + } + } finally { + setCopying(null); + } + }; + + const folderName = item.folderId ? folderMap[item.folderId] : null; + + return ( +
+ {/* Header */} +
setExpanded(!expanded)} + style={{ + cursor: "pointer", + display: "flex", + alignItems: "center", + gap: "8px", + }} + > + + {CIPHER_TYPE_ICONS[item.type] || "\uD83D\uDD12"} + +
+
+ {item.name || "[unnamed]"} +
+ {item.login?.username && ( +
+ {item.login.username} +
+ )} +
+ {folderName && ( + + {folderName} + + )} + + {expanded ? "\u25B2" : "\u25BC"} + +
+ + {/* Expanded content */} + {expanded && item.login && ( +
+ {/* URI */} + {item.login.uris && item.login.uris.length > 0 && ( +
+ {item.login.uris[0].uri || ""} +
+ )} + + {/* Copy buttons */} +
+ {item.login.username && ( + handleCopy("username")} + /> + )} + + {item.login.password && ( + handleCopy("password")} + /> + )} + + {item.login.totp && ( + handleCopy("totp")} + /> + )} +
+ + {/* Custom fields */} + {item.fields && item.fields.length > 0 && ( +
+
+ Custom Fields +
+ {item.fields.map((field, idx) => ( +
+ {field.name}: + + {field.hidden ? "••••••••" : field.value || ""} + +
+ ))} +
+ )} + + {/* Notes */} + {item.notes && ( +
+ {item.notes} +
+ )} +
+ )} + + {/* Card info */} + {expanded && item.card && ( +
+ {item.card.brand &&
Brand: {item.card.brand}
} + {item.card.number && ( +
Number: •••• •••• •••• {item.card.number.slice(-4)}
+ )} + {item.card.expMonth && item.card.expYear && ( +
+ Expires: {item.card.expMonth}/{item.card.expYear} +
+ )} + {item.card.cardholderName && ( +
Holder: {item.card.cardholderName}
+ )} +
+ )} +
+ ); +} + +function CopyButton({ + label, + field, + isCopied, + expiresIn, + loading, + onClick, +}: { + label: string; + field: string; + isCopied: boolean; + expiresIn: number; + loading: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/components/LoginView.tsx b/src/components/LoginView.tsx new file mode 100644 index 0000000..c3bad3d --- /dev/null +++ b/src/components/LoginView.tsx @@ -0,0 +1,137 @@ +import { useState } from "react"; +import { + PanelSection, + TextField, + ButtonItem, + ToggleField, + Spinner, +} from "@decky/ui"; + +interface LoginViewProps { + onLogin: ( + serverUrl: string, + email: string, + password: string, + twoFactorToken?: string + ) => Promise; + onApiKeyLogin: ( + serverUrl: string, + clientId: string, + clientSecret: string, + email: string + ) => Promise; + loading: boolean; + error: string | null; + twoFactorRequired?: boolean; + needsMasterPassword?: boolean; +} + +export function LoginView({ + onLogin, + onApiKeyLogin, + loading, + error, + twoFactorRequired, + needsMasterPassword, +}: LoginViewProps) { + const [authMethod, setAuthMethod] = useState<"password" | "apikey">("password"); + const [serverUrl, setServerUrl] = useState("https://api.bitwarden.com"); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [twoFactorCode, setTwoFactorCode] = useState(""); + const [clientId, setClientId] = useState(""); + const [clientSecret, setClientSecret] = useState(""); + + const handlePasswordLogin = async () => { + if (!email || !password) return; + await onLogin(serverUrl, email, password, twoFactorCode || undefined); + }; + + const handleApiKeyLogin = async () => { + if (!clientId || !clientSecret || !email) return; + await onApiKeyLogin(serverUrl, clientId, clientSecret, email); + }; + + return ( + + {/* Server URL */} + ) => setServerUrl(e.target.value)} + /> + + {/* Auth Method Toggle */} + setAuthMethod(v ? "apikey" : "password")} + /> + + {/* Email (common to both methods) */} + ) => setEmail(e.target.value)} + /> + + {authMethod === "password" ? ( + <> + ) => setPassword(e.target.value)} + /> + + {twoFactorRequired && ( + ) => setTwoFactorCode(e.target.value)} + /> + )} + + + {loading ? : "Login with Password"} + + + ) : ( + <> + ) => setClientId(e.target.value)} + /> + + ) => setClientSecret(e.target.value)} + /> + + + {loading ? : "Login with API Key"} + + + )} + + {error && ( +
+ {error} +
+ )} +
+ ); +} diff --git a/src/components/PinSetupModal.tsx b/src/components/PinSetupModal.tsx new file mode 100644 index 0000000..6065a93 --- /dev/null +++ b/src/components/PinSetupModal.tsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { TextField, Spinner } from "@decky/ui"; + +interface PinSetupModalProps { + isOpen: boolean; + onClose: () => void; + onSetupPin: (pin: string) => Promise; + loading: boolean; + error: string | null; +} + +export function PinSetupModal({ + isOpen, + onClose, + onSetupPin, + loading, + error, +}: PinSetupModalProps) { + const [pin, setPin] = useState(""); + const [confirmPin, setConfirmPin] = useState(""); + const [localError, setLocalError] = useState(null); + + if (!isOpen) return null; + + const handleSubmit = async () => { + setLocalError(null); + + if (pin.length < 4) { + setLocalError("PIN must be at least 4 characters"); + return; + } + + if (pin !== confirmPin) { + setLocalError("PINs do not match"); + return; + } + + const success = await onSetupPin(pin); + if (success) { + setPin(""); + setConfirmPin(""); + onClose(); + } + }; + + return ( +
+
e.stopPropagation()} + > +

Set Up PIN

+ +

+ A PIN allows you to unlock your vault without entering your master + password each time. The PIN protects your encryption key locally. +

+ + ) => setPin(e.target.value)} + /> + + ) => setConfirmPin(e.target.value)} + /> + +
+ + +
+ + {(localError || error) && ( +
+ {localError || error} +
+ )} +
+
+ ); +} diff --git a/src/components/PinSetupView.tsx b/src/components/PinSetupView.tsx new file mode 100644 index 0000000..d6a17f4 --- /dev/null +++ b/src/components/PinSetupView.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { PanelSection, TextField, ButtonItem, Spinner } from "@decky/ui"; + +interface PinSetupViewProps { + onSetupPin: (pin: string) => Promise; + loading: boolean; + error: string | null; +} + +export function PinSetupView({ onSetupPin, loading, error }: PinSetupViewProps) { + const [pin, setPin] = useState(""); + const [confirmPin, setConfirmPin] = useState(""); + const [localError, setLocalError] = useState(null); + + const handleSubmit = async () => { + setLocalError(null); + + if (pin.length < 4) { + setLocalError("PIN must be at least 4 characters"); + return; + } + + if (pin !== confirmPin) { + setLocalError("PINs do not match"); + return; + } + + const success = await onSetupPin(pin); + if (success) { + setPin(""); + setConfirmPin(""); + } + }; + + return ( + +
+ A PIN allows you to unlock your vault without entering your master password + each time. The PIN protects your encryption key locally. +
+ + ) => setPin(e.target.value)} + /> + + ) => setConfirmPin(e.target.value)} + /> + + + {loading ? : "Set Up PIN"} + + + {(localError || error) && ( +
+ {localError || error} +
+ )} +
+ ); +} diff --git a/src/components/PinUnlockView.tsx b/src/components/PinUnlockView.tsx new file mode 100644 index 0000000..06ee10e --- /dev/null +++ b/src/components/PinUnlockView.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import { PanelSection, TextField, ButtonItem, Spinner } from "@decky/ui"; + +interface PinUnlockViewProps { + onUnlock: (pin: string) => Promise; + onLogout: () => Promise; + loading: boolean; + error: string | null; +} + +export function PinUnlockView({ + onUnlock, + onLogout, + loading, + error, +}: PinUnlockViewProps) { + const [pin, setPin] = useState(""); + + const handleUnlock = async () => { + if (!pin) return; + await onUnlock(pin); + setPin(""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleUnlock(); + } + }; + + return ( + +
+
🔒
+
+ Enter your PIN to unlock +
+
+ +
+ ) => setPin(e.target.value)} + /> +
+ + + {loading ? : "Unlock"} + + + + Login with Master Password + + + {error && ( +
+ {error} +
+ )} +
+ ); +} diff --git a/src/components/VaultBrowser.tsx b/src/components/VaultBrowser.tsx new file mode 100644 index 0000000..a658c05 --- /dev/null +++ b/src/components/VaultBrowser.tsx @@ -0,0 +1,170 @@ +import { useState, useEffect } from "react"; +import { + PanelSection, + TextField, + ButtonItem, + Spinner, +} from "@decky/ui"; +import { VaultData, VaultItem, Folder } from "../api/backend"; +import { ItemCard } from "./ItemCard"; + +interface VaultBrowserProps { + vaultData: VaultData; + searchQuery: string; + searchResults: VaultItem[] | null; + copyStatus: { field: string; cipherId: string; expiresAt: number } | null; + onSearch: (query: string) => void; + onCopyPassword: (cipherId: string) => Promise; + onCopyUsername: (cipherId: string) => Promise; + onCopyTotp: (cipherId: string) => Promise; + onLock: () => void; + onSetupPin: () => void; +} + +export function VaultBrowser({ + vaultData, + searchQuery, + searchResults, + copyStatus, + onSearch, + onCopyPassword, + onCopyUsername, + onCopyTotp, + onLock, + onSetupPin, +}: VaultBrowserProps) { + const [selectedFolder, setSelectedFolder] = useState(null); + const [localSearch, setLocalSearch] = useState(searchQuery); + + // Get items for current view + const getDisplayedItems = (): VaultItem[] => { + if (searchResults) { + return searchResults; + } + + let items = vaultData.ciphers.filter((c) => !c.error); + + if (selectedFolder) { + items = items.filter((c) => c.folderId === selectedFolder); + } + + // Sort by name + items.sort((a, b) => (a.name || "").localeCompare(b.name || "")); + + return items; + }; + + const displayedItems = getDisplayedItems(); + + const handleSearchChange = (value: string) => { + setLocalSearch(value); + onSearch(value); + }; + + return ( + + {/* Search */} + ) => handleSearchChange(e.target.value)} + /> + + {/* Folder tabs */} + {!searchQuery && ( +
+ !c.error).length} + selected={selectedFolder === null} + onClick={() => setSelectedFolder(null)} + /> + {vaultData.folders.map((folder) => ( + c.folderId === folder.id && !c.error + ).length} + selected={selectedFolder === folder.id} + onClick={() => setSelectedFolder(folder.id)} + /> + ))} +
+ )} + + {/* Items */} +
+ {displayedItems.length === 0 ? ( +
+ {searchQuery ? "No items found" : "No items in this folder"} +
+ ) : ( + displayedItems.map((item) => ( + + )) + )} +
+ + {/* Actions */} + + Lock Vault + + + Set Up PIN + +
+ ); +} + +function FolderTab({ + name, + count, + selected, + onClick, +}: { + name: string; + count: number; + selected: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/hooks/useVault.ts b/src/hooks/useVault.ts new file mode 100644 index 0000000..5967b3d --- /dev/null +++ b/src/hooks/useVault.ts @@ -0,0 +1,322 @@ +import { useState, useCallback, useEffect } from "react"; +import { vaultApi, VaultData, VaultItem } from "../api/backend"; + +export type VaultState = + | "logged_out" + | "two_factor_required" + | "needs_master_password" + | "pin_only" + | "loading" + | "unlocked"; + +export function useVault() { + const [state, setState] = useState("logged_out"); + const [vaultData, setVaultData] = useState(null); + const [error, setError] = useState(null); + const [pinEnabled, setPinEnabled] = useState(false); + const [selectedFolder, setSelectedFolder] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState(null); + const [copyStatus, setCopyStatus] = useState<{ + field: string; + cipherId: string; + expiresAt: number; + } | null>(null); + + // Check initial status + useEffect(() => { + checkStatus(); + }, []); + + const checkStatus = async () => { + try { + const status = await vaultApi.getStatus(); + const pin = await vaultApi.isPinEnabled(); + setPinEnabled(pin.enabled); + + if (status.vault_loaded) { + setState("unlocked"); + const result = await vaultApi.getVaultItems(); + if (result.success) { + setVaultData(result.data); + } + } else if (status.session_active) { + setState("needs_master_password"); + } else if (pin.enabled) { + setState("pin_only"); + } else { + setState("logged_out"); + } + } catch (e) { + setState("logged_out"); + } + }; + + const loginPassword = useCallback( + async (serverUrl: string, email: string, password: string, twoFactorToken?: string) => { + setState("loading"); + setError(null); + try { + const result = await vaultApi.loginPassword( + serverUrl, + email, + password, + twoFactorToken + ); + if (result.two_factor_required) { + setState("two_factor_required"); + return false; + } + if (result.needs_unlock) { + // Need to unlock with password + const unlockResult = await vaultApi.unlockWithMasterPassword(password); + if (unlockResult.success) { + await loadVault(); + return true; + } + } + if (result.success) { + await loadVault(); + return true; + } + setError(result.error || "Login failed"); + setState("logged_out"); + return false; + } catch (e: any) { + setError(e.message); + setState("logged_out"); + return false; + } + }, + [] + ); + + const loginApiKey = useCallback( + async (serverUrl: string, clientId: string, clientSecret: string, email: string) => { + setState("loading"); + setError(null); + try { + const result = await vaultApi.loginApiKey( + serverUrl, + clientId, + clientSecret, + email + ); + if (result.needs_master_password) { + setState("needs_master_password"); + return true; + } + if (result.success) { + setState("needs_master_password"); + return true; + } + setError(result.error || "API key login failed"); + setState("logged_out"); + return false; + } catch (e: any) { + setError(e.message); + setState("logged_out"); + return false; + } + }, + [] + ); + + const unlockWithPassword = useCallback(async (password: string) => { + setState("loading"); + setError(null); + try { + const result = await vaultApi.unlockWithMasterPassword(password); + if (result.success) { + await loadVault(); + return true; + } + setError(result.error || "Unlock failed"); + setState("needs_master_password"); + return false; + } catch (e: any) { + setError(e.message); + setState("needs_master_password"); + return false; + } + }, []); + + const unlockWithPin = useCallback(async (pin: string) => { + setState("loading"); + setError(null); + try { + const result = await vaultApi.unlockWithPin(pin); + if (result.success) { + await loadVault(); + return true; + } + setError(result.error || "PIN unlock failed"); + setState("pin_only"); + return false; + } catch (e: any) { + setError(e.message); + setState("pin_only"); + return false; + } + }, []); + + const loadVault = async () => { + try { + const result = await vaultApi.getVaultItems(); + if (result.success) { + setVaultData(result.data); + setState("unlocked"); + } + } catch (e: any) { + setError(e.message); + } + }; + + const search = useCallback(async (query: string) => { + setSearchQuery(query); + if (!query.trim()) { + setSearchResults(null); + return; + } + try { + const result = await vaultApi.searchVault(query); + if (result.success) { + setSearchResults(result.results); + } + } catch (e: any) { + setError(e.message); + } + }, []); + + const copyPassword = useCallback(async (cipherId: string) => { + try { + const result = await vaultApi.copyPassword(cipherId); + if (result.success) { + setCopyStatus({ + field: "password", + cipherId, + expiresAt: result.expires_at || Date.now() / 1000 + 60, + }); + setTimeout(() => setCopyStatus(null), 60000); + return true; + } + setError(result.error || "Failed to copy"); + return false; + } catch (e: any) { + setError(e.message); + return false; + } + }, []); + + const copyUsername = useCallback(async (cipherId: string) => { + try { + const result = await vaultApi.copyUsername(cipherId); + if (result.success) { + setCopyStatus({ + field: "username", + cipherId, + expiresAt: result.expires_at || Date.now() / 1000 + 60, + }); + setTimeout(() => setCopyStatus(null), 60000); + return true; + } + setError(result.error || "Failed to copy"); + return false; + } catch (e: any) { + setError(e.message); + return false; + } + }, []); + + const copyTotp = useCallback(async (cipherId: string) => { + try { + const result = await vaultApi.copyTotp(cipherId); + if (result.success) { + setCopyStatus({ + field: "totp", + cipherId, + expiresAt: result.expires_at || Date.now() / 1000 + 30, + }); + setTimeout(() => setCopyStatus(null), 30000); + return true; + } + setError(result.error || "Failed to copy TOTP"); + return false; + } catch (e: any) { + setError(e.message); + return false; + } + }, []); + + const lockVault = useCallback(async () => { + await vaultApi.lockVault(); + setVaultData(null); + setCopyStatus(null); + setSearchResults(null); + setSearchQuery(""); + const pin = await vaultApi.isPinEnabled(); + setPinEnabled(pin.enabled); + setState(pin.enabled ? "pin_only" : "logged_out"); + }, []); + + const logout = useCallback(async () => { + await vaultApi.logout(); + setVaultData(null); + setCopyStatus(null); + setSearchResults(null); + setSearchQuery(""); + setPinEnabled(false); + setState("logged_out"); + }, []); + + const setupPin = useCallback(async (pin: string) => { + try { + const result = await vaultApi.setupPin(pin); + if (result.success) { + setPinEnabled(true); + return true; + } + setError(result.error || "Failed to set up PIN"); + return false; + } catch (e: any) { + setError(e.message); + return false; + } + }, []); + + const removePin = useCallback(async () => { + try { + await vaultApi.removePin(); + setPinEnabled(false); + return true; + } catch (e: any) { + setError(e.message); + return false; + } + }, []); + + return { + state, + vaultData, + error, + pinEnabled, + selectedFolder, + searchQuery, + searchResults, + copyStatus, + setSelectedFolder, + loginPassword, + loginApiKey, + unlockWithPassword, + unlockWithPin, + search, + copyPassword, + copyUsername, + copyTotp, + lockVault, + logout, + setupPin, + removePin, + clearError: () => setError(null), + }; +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..83fac90 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,223 @@ +import { useEffect, useState } from "react"; +import { definePlugin } from "@decky/api"; +import { PanelSection, Spinner, TextField } from "@decky/ui"; + +import { useVault } from "./hooks/useVault"; +import { LoginView } from "./components/LoginView"; +import { PinUnlockView } from "./components/PinUnlockView"; +import { VaultBrowser } from "./components/VaultBrowser"; +import { PinSetupModal } from "./components/PinSetupModal"; + +function VaultwardenPlugin() { + const vault = useVault(); + const [showPinSetup, setShowPinSetup] = useState(false); + + // Handle keyboard input for PIN + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Lock vault on Ctrl+Shift+L + if (e.ctrlKey && e.shiftKey && e.key === "L") { + vault.lockVault(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [vault.lockVault]); + + // Main render + if (vault.state === "loading") { + return ( + +
+ +
Loading...
+
+
+ ); + } + + if (vault.state === "logged_out") { + return ( + + ); + } + + if (vault.state === "two_factor_required") { + return ( + + ); + } + + if (vault.state === "needs_master_password") { + return ( + + ); + } + + if (vault.state === "pin_only") { + return ( + + ); + } + + if (vault.state === "unlocked" && vault.vaultData) { + return ( + <> + setShowPinSetup(true)} + /> + + setShowPinSetup(false)} + onSetupPin={vault.setupPin} + loading={false} + error={vault.error} + /> + + ); + } + + return ( + + ); +} + +function MasterPasswordPrompt({ + onSubmit, + loading, + error, +}: { + onSubmit: (password: string) => Promise; + loading: boolean; + error: string | null; +}) { + const [password, setPassword] = useState(""); + + const handleSubmit = async () => { + if (!password) return; + await onSubmit(password); + setPassword(""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSubmit(); + } + }; + + return ( + +
+
🔐
+
+ Enter your master password to unlock +
+
+ +
+ ) => setPassword(e.target.value)} + /> +
+ + + + {error && ( +
+ {error} +
+ )} +
+ ); +} + +export default definePlugin(() => ({ + name: "Vaultwarden", + icon: ( + + + + + ), + content: , + onDismount: () => { + console.log("Vaultwarden plugin dismounted"); + }, +})); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b84f427 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "node", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "declaration": true, + "sourceMap": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +}