diff --git a/.env.example b/.env.example index 04e74d8..fef647d 100644 --- a/.env.example +++ b/.env.example @@ -221,8 +221,17 @@ DEEPSQL_BACKEND_PORT=8080 DEEPSQL_POSTGRES_PORT=5432 DEEPSQL_VALKEY_PORT=6379 -# Browser origins allowed to call the backend -CORS_ALLOWED_ORIGINS=http://localhost:3000 +# Browser origins allowed to call the backend. +# +# This REPLACES the built-in list — it does not add to it. When you put your own +# hostname here, keep the loopback patterns too. The desktop client reaches a VM +# over an SSH tunnel and therefore serves the app from http://127.0.0.1:, +# with a port picked at runtime; if that origin is not allowed the app loads +# normally and then every login fails with a bare "403 Invalid CORS request". +# The `*` is a port wildcard (SecurityConfig uses setAllowedOriginPatterns). +# +# CORS_ALLOWED_ORIGINS=https://deepsql.example.com,http://127.0.0.1:*,http://localhost:* +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:*,http://localhost:* # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # OPTIONAL — Email / SMTP diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 0000000..42799aa --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,93 @@ +name: desktop-release + +# Builds the DeepSQL desktop client for every platform. Each OS builds its own +# targets on its own runner: cross-building Windows needs Wine and Linux targets +# need a matching glibc, and both are far less reliable than just using the +# native runner. +# +# Tag-triggered runs attach the installers to the GitHub release. Manual runs +# upload them as workflow artifacts, which is the easy way to hand a build to a +# colleague before there is a release to cut. + +on: + push: + tags: + - 'desktop-v*' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { os: macos-latest, name: macOS, target: '--mac' } + - { os: windows-latest, name: Windows, target: '--win' } + - { os: ubuntu-latest, name: Linux, target: '--linux' } + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + cache-dependency-path: desktop/package-lock.json + + - name: Install dependencies + working-directory: desktop + run: npm ci + + - name: Self-test the SSH tunnel transport + # Runs a real SSH server in-process, so it needs a display-free Electron. + # xvfb is only required on Linux; the other runners have a window server. + working-directory: desktop + run: ${{ matrix.os == 'ubuntu-latest' && 'xvfb-run --auto-servernum npm run selftest:tunnel' || 'npm run selftest:tunnel' }} + shell: bash + + - name: Build installers + working-directory: desktop + env: + # Signing is skipped when these are unset; electron-builder warns and + # produces unsigned artifacts rather than failing the build. + CSC_LINK: ${{ secrets.DESKTOP_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: npx electron-builder ${{ matrix.target }} --publish never + + - uses: actions/upload-artifact@v4 + with: + name: deepsql-desktop-${{ matrix.name }} + if-no-files-found: error + path: | + desktop/release/*.dmg + desktop/release/*.zip + desktop/release/*.exe + desktop/release/*.AppImage + desktop/release/*.deb + desktop/release/*.rpm + + release: + name: attach to release + needs: build + if: startsWith(github.ref, 'refs/tags/desktop-v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - uses: softprops/action-gh-release@v2 + with: + files: artifacts/* + fail_on_unmatched_files: true diff --git a/AGENTS.md b/AGENTS.md index 13eb0f3..02ae85a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,6 +110,32 @@ User Message → ChatController → SpringAIChatService - **UI state**: Zustand stores with selector hooks (`useActiveTab`, `useDashboardActions`) - **Independent chat threads**: Per-tab, per-connection, stored in localStorage +## Desktop Client (Electron) + +`desktop/` is a standalone Electron app (its own `package.json`, not part of the +root npm project). It is a **thin client**: it never bundles the React frontend, +it navigates a `WebContentsView` at the real DeepSQL origin, so the UI is always +the version the server runs. Two transports resolve to that origin — direct TLS, +or an in-process SSH local forward (`ssh2`, no `ssh` binary needed). + +| Path | Purpose | +|------|---------| +| `desktop/src/main/transport.js` | Transport manager: connect/disconnect/health per profile | +| `desktop/src/main/tunnel.js` | SSH local forward, host-key TOFU-then-strict, auto-reconnect | +| `desktop/src/main/tls.js` | Cert policy (system / pinned / custom CA / TOFU) for Node **and** Chromium | +| `desktop/src/main/profiles.js` | Connection profiles; secrets only as `safeStorage` ciphertext | +| `desktop/src/main/windows/workspace.js` | Frameless shell: native chrome + embedded DeepSQL view | +| `desktop/src/renderer/shared/theme.css` | Mirrors `src/index.css` tokens — keep in step | + +`docker/nginx/default.conf` already serves the SPA, `/api`, and `/agent-api` from +one origin, so cookies and SSE behave like a normal browser. **CORS is the one +backend setting the thin client still needs:** an SSH tunnel uses origin +`http://127.0.0.1:`, so `CORS_ALLOWED_ORIGINS` on the VM must keep +the loopback port wildcards (`http://127.0.0.1:*,http://localhost:*`) alongside +any public hostname. Overriding that env var *replaces* the built-in list — a +public-origin-only value breaks Desktop tunnel login with a confusing 403. +Setup and diagnosis: [`desktop/README.md`](desktop/README.md#cors-on-the-vm-the-403-nobody-can-read). + ## Performance & Safety Guardrails - Log size cap (500MB) via stream wrappers @@ -287,10 +313,15 @@ only covers cloud-specific, non-obvious caveats. `deepsql agent --connection "…"`. Interactive: `deepsql` / `deepsql agent`. The CLI is a thin client over `POST /api/agent/chat` (not a local agent runtime); backend + agent API (:8787) + provisioner must already be up. -- **Spring CORS must allow both loopback hosts.** Set - `CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000` in `.env`. Opening - the UI as `http://127.0.0.1:3000` while only `localhost` is allowlisted yields **403** - on `POST /api/agent/session` (and other cookie-auth APIs). +- **Spring CORS must allow loopback (and Desktop tunnel ports).** Prefer + `CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:*,http://localhost:*` + in `.env` (match `.env.example`). Fixed `:3000` alone is enough for Vite on that + port; the `*` port wildcards are required for DeepSQL Desktop’s SSH tunnel, which + binds a sticky random local port. Opening the UI as `http://127.0.0.1:…` while + only `localhost` (or only a public hostname) is allowlisted yields **403** on + `POST /api/agent/session` (and other cookie-auth APIs). Electron GUI itself is + out of scope for headless Cloud Agents — use Vite + these CORS patterns here; + see [`desktop/README.md`](desktop/README.md) when developing the client. - **Before running backend tests that boot the Spring context** (e.g. `ApiSmokeTest`), stop the running backend first — both use `ddl-auto=update` on the same `dba_agent` DB and can deadlock on an `ALTER TABLE`. Test env vars are documented in `CLAUDE.md` (Testing). diff --git a/CLAUDE.md b/CLAUDE.md index b2f978a..b729f63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,6 +122,126 @@ mcp/ # DeepSQL Phase 1 MCP server (Node stdio wrapper around back agent/ # DeepSQL Agent (persona, skills, skins, Dockerfile for the Compose service) ``` +## Desktop Client (`desktop/`) + +Cross-platform Electron client for a self-hosted DeepSQL VM. **Separate npm +project** — `cd desktop && npm install`, not part of the root `package.json`. + +```bash +cd desktop +npm start # run +npm run dev # run with DevTools +npm test # drift guard for the DevTools kill switch +npm run dist:mac # dmg + zip (arm64 + x64), also :win / :linux +npm run smoke -- --url https://deepsql.example.com # headless connection check +npm run selftest:tunnel # end-to-end SSH tunnel test (in-process SSH server) +npm run selftest:settings # proves an edited setting reaches the live connection +``` + +**A saved profile edit rebuilds the live connection; saving alone was never the +bug.** The launcher persists the form before every Connect and Test, so +`profiles.json` was always correct — but `transport.connect()` reused any live +connection unconditionally, so changing a tunnel's remote port and pressing +Connect did nothing, and Test reported a confident pass for settings the user had +just replaced. `profiles.transportFingerprint()` now decides whether a live +connection still *is* the connection being asked for; `transport.reconcile()` +rebuilds it on save (`ipc.saveAndReconcile`), and `Workspace.updateProfile()` +re-points the window, since a rebuilt tunnel binds a different local port and so +changes the origin. The fingerprint deliberately excludes `name` (a rename must +not drop a tunnel) and `stickyLocalPort` (chosen by us and rewritten every +connect — including it would make a connection differ from itself). A failed +rebuild does **not** restore the old connection: it was built from settings that +no longer exist, so it stays closed and the failure is reported. Entries also +store a profile re-read *after* the connect path's trust-on-first-use writes, or +the next connect would see a mismatch it caused itself. + +**DevTools are disabled in packaged builds, and `IS_DEV` is the wrong switch for +it.** Every window passes `webPreferences.devTools: DEVTOOLS_ENABLED`, defined in +`config.js` as `!app.isPackaged` and nothing else. Do not "simplify" it to +`IS_DEV`: `IS_DEV` is also true when `DEEPSQL_DESKTOP_DEV=1`, which any user can +export against the shipped app — that is precisely the hole this closes, and it +used to open DevTools automatically on both windows with no menu item involved. +`devTools: false` is the load-bearing part (Chromium then refuses to attach at +all, making `openDevTools()` a no-op); removing the menu item only hides the +door, though it also drops the `Alt+Cmd+I`/`Ctrl+Shift+I` binding, since a custom +`Menu.setApplicationMenu` means Electron contributes no `toggleDevTools` role. +Separately, `index.js` exits on `--remote-debugging-port` and friends: those open +a DevTools *protocol* endpoint that `devTools: false` does not cover. Verified +behaviourally on Electron 43 (`devTools:false` → `isDevToolsOpened()` stays false +after `openDevTools()`; a `devTools:true` control opens, so the check is not +vacuous). `desktop/src/main/devtools.test.js` fails the build if a new +`webPreferences` block omits `devTools` — the regression is otherwise silent, +since Chromium's default is *enabled*. + +**It is a thin client and deliberately does not bundle the React frontend.** It +navigates a `WebContentsView` at the real DeepSQL origin, so the UI is always the +version the VM is running — no bundle/backend skew, and no second copy of 40+ +tabs to maintain. `docker/nginx/default.conf` already serves the SPA, `/api` and +`/agent-api` from one origin, so cookies and SSE behave exactly as in a browser. +Do not "improve" this by bundling `dist/` — that reintroduces `SameSite` and +version-skew problems the current design does not have. + +**It needs exactly one piece of backend configuration, and CORS is it.** The +"zero backend changes" claim that used to sit here was wrong, and cost a long +debugging session. Over a tunnel the origin is `http://127.0.0.1:`, +not the VM's hostname, so a deployment whose `CORS_ALLOWED_ORIGINS` names only +its public hostname rejects the desktop client. The failure is maximally +misleading: Chromium omits `Origin` on same-origin GETs, so the health probe, +the SPA and every read succeed, and the *first POST* — the login — comes back +`403` with the plain-text body `Invalid CORS request`. That body has no +`message` field, so `client.js`'s axios interceptor falls through to axios's own +wording and the user sees **"Request failed with status code 403"**, which names +neither CORS nor the origin. Fix: keep loopback patterns in the allowlist — +`CORS_ALLOWED_ORIGINS=https://your-host,http://127.0.0.1:*,http://localhost:*`. +Port wildcards work only because `SecurityConfig` uses +`setAllowedOriginPatterns`; `setAllowedOrigins` would reject `*` alongside +`allowCredentials(true)`. `probe.js` now sends an `Origin` header for exactly +this reason, so the rejection is caught at connect time and named. + +**Two transports, one abstraction.** Both resolve to an *origin*, so nothing +downstream of `desktop/src/main/transport.js` knows which is in use: + +- **Direct TLS** — the VM's HTTPS origin. Four certificate modes (`system`, + `pinned`, `custom-ca`, `insecure`/TOFU), applied to **both** the Node health + probe and the Chromium session (`tls.applyToSession`). Applying it to only one + gives a connection that tests green but renders a certificate error. +- **SSH tunnel** — `ssh2` local forward, loopback-bound, no `ssh` binary needed. + The local port is *sticky* across launches on purpose: the origin includes the + port, and a fresh random port would silently reset the web app's + `localStorage`. `http://127.0.0.1:*` is a Chromium secure context, so the + backend's `Secure` cookies still work over the tunnel. **Forward to the + frontend container (3000), not a host reverse proxy on :80** — that proxy + matches on `server_name`, a tunnel arrives with `Host: 127.0.0.1:`, + and the request lands on the default vhost as a 404 that reads like a broken + backend. The container's nginx uses `server_name _` and answers any Host. + +Three non-obvious things, all found the hard way: + +1. **`Client.connect({ privateKey })` must get the raw key material, not the + object `sshUtils.parseKey` returns.** Handed a parsed key, ssh2 silently + never offers the publickey method and the server replies with a bare + authentication failure — a symptom that points at the VM's `authorized_keys` + rather than at a type mismatch on our side. `loadPrivateKey` parses only to + produce good error messages and returns the buffer. +2. **Authentication succeeding says nothing about forwarding being allowed.** + A hardened sshd (`AllowTcpForwarding no`) accepts the login and refuses every + `direct-tcpip` channel; the failure otherwise surfaces as "socket hang up" on + the first browser request, pointing nowhere near sshd. `verifyForwarding()` + opens and closes one channel right after auth and classifies the refusal by + SSH reason code — 1 (`ADMINISTRATIVELY_PROHIBITED`, verified against real + OpenSSH) names `AllowTcpForwarding`, 2 (`CONNECT_FAILED`) means nothing is + listening on the remote port. +3. **Only a session that once reached `ready` may be reconnected.** Gating + reconnects on `everReady` is what stops a connect that fails on + authentication from retrying forever behind a caller that already surfaced + the error. + +Secrets (key passphrases, SSH passwords) are stored as `safeStorage` ciphertext; +where no OS keychain exists nothing is written to disk and the launcher says so. +Each profile gets its own session partition, so two DeepSQL servers never share +cookies. `.github/workflows/desktop-release.yml` builds all three platforms on +their native runners. See `desktop/README.md` for the full picture. + ## MCP Server - `mcp/deepsql-phase1-server.js` implements a Phase 1 stdio MCP server for internal rollout. diff --git a/README.md b/README.md index a626fbe..0d66a44 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,22 @@ your schema — all from one shared brain. - **Postgres and MySQL, in your infra.** One dialect registry, read-only execution, and SSH tunnelling to reach databases behind a bastion. +### DeepSQL Desktop (optional) + +A thin Electron client for a self-hosted VM — direct TLS or an in-process SSH tunnel — +without bundling a second copy of the web UI. Separate npm project: + +```bash +cd desktop +npm install +npm start +``` + +Requires **Node 22+**. Forward the tunnel to the **frontend container (port 3000)**, not a +host reverse proxy on `:80`. Keep loopback CORS wildcards on the VM +(`http://127.0.0.1:*,http://localhost:*`) or tunnel login fails with a confusing 403 — see +[`desktop/README.md`](desktop/README.md). + --- ## Operating the stack diff --git a/backend/src/main/java/com/dbaagent/config/SecurityConfig.java b/backend/src/main/java/com/dbaagent/config/SecurityConfig.java index 0a6520f..444e94c 100644 --- a/backend/src/main/java/com/dbaagent/config/SecurityConfig.java +++ b/backend/src/main/java/com/dbaagent/config/SecurityConfig.java @@ -52,7 +52,7 @@ public class SecurityConfig { * only {@code application*.properties} — could not see it, and it ships to every reader * of the public repository. {@code CorsAllowlistSafetyTest} now scans this file too. */ - @Value("${cors.allowed.origins:http://localhost:3000,http://localhost:3001,http://localhost:3002,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002}") + @Value("${cors.allowed.origins:http://localhost:3000,http://localhost:3001,http://localhost:3002,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002,http://127.0.0.1:*,http://localhost:*}") private String corsAllowedOrigins; @Bean diff --git a/backend/src/main/resources/application-prod.properties b/backend/src/main/resources/application-prod.properties index baf6572..065d3c4 100644 --- a/backend/src/main/resources/application-prod.properties +++ b/backend/src/main/resources/application-prod.properties @@ -57,8 +57,11 @@ app.base-url=${APP_BASE_URL:http://localhost:3000} # CORS Configuration # Override CORS_ALLOWED_ORIGINS with the origin(s) your frontend is -# actually served from. The default below covers local dev only. -cors.allowed.origins=${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001} +# actually served from, and keep the loopback patterns: the desktop +# client's SSH tunnel serves the app from http://127.0.0.1:, +# so dropping them rejects every tunnel login with 403 "Invalid CORS +# request". See the longer note in application.properties. +cors.allowed.origins=${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://127.0.0.1:*,http://localhost:*} # File upload limits (slow query logs can be large) spring.servlet.multipart.max-file-size=2048MB diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 635699d..02057fb 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -93,10 +93,22 @@ db-scheduler.shutdown-max-wait=45m db-scheduler.immediate-execution-enabled=true # CORS Configuration -# Localhost only by default so `mvn spring-boot:run` + `npm run dev` works out of the box. +# Loopback only by default so `mvn spring-boot:run` + `npm run dev` works out of the box. # Any deployment serving a browser from another host must set CORS_ALLOWED_ORIGINS. -# Note: exact origins (no patterns) for reliability. -cors.allowed.origins=${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://localhost:3002,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002} +# +# The loopback entries carry a port wildcard because the desktop client's SSH tunnel +# serves the app from http://127.0.0.1:, and that port is chosen at runtime. +# Enumerating ports here is what made every tunnel connection fail with a bare +# "403 Invalid CORS request": Spring treats any request carrying Origin as cross-origin +# (the same-origin short-circuit went away in 5.3), so an unlisted loopback port is +# rejected. SecurityConfig uses setAllowedOriginPatterns, so `*` is legal in the port +# position and stays compatible with allowCredentials(true) — plain setAllowedOrigins +# would not be. +# +# IMPORTANT: overriding CORS_ALLOWED_ORIGINS replaces this list wholesale. A deployment +# that sets it to its public hostname alone drops the loopback entries and breaks the +# desktop client. Keep the loopback patterns alongside your hostname. +cors.allowed.origins=${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://localhost:3002,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002,http://127.0.0.1:*,http://localhost:*} # File upload limits (slow query logs can be large) spring.servlet.multipart.max-file-size=2048MB diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 0000000..dad03f4 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +release/ +build/icon.iconset/ +*.log diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 0000000..d468771 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,348 @@ +# DeepSQL Desktop + +A cross-platform desktop client for a self-hosted DeepSQL server. It connects to +the VM (or bare metal) running the DeepSQL stack either **directly over TLS** or +through an **SSH tunnel**, and presents the DeepSQL UI in a native window with +connection management, health, and transport status built into the chrome. + +``` +┌────────────────── DeepSQL.app ──────────────────┐ +│ Launcher Workspace window │ +│ (connections) ┌──────────────────────────┐ │ +│ │ native chrome ● 170 ms │ │ +│ ├──────────────────────────┤ │ +│ │ the DeepSQL web UI │ │ +│ │ (WebContentsView) │ │ +│ └──────────────────────────┘ │ +└──────────┬───────────────────────┬───────────────┘ + │ TLS │ SSH tunnel + ▼ ▼ + https://deepsql.example.com 127.0.0.1:PORT ⇢ ssh ⇢ VM:3000 +``` + +## Design: a thin client, not a second frontend + +The client **does not bundle a copy of the DeepSQL web app**. It navigates an +embedded `WebContentsView` at the real DeepSQL origin. Consequences worth +knowing: + +- The UI is always the exact version the VM is running. There is no + bundle/backend skew and no second copy of 40+ tabs to keep in step. +- Cookies and SSE behave exactly as in a browser, because the app is served + from one origin — `docker/nginx/default.conf` already fronts `/api` and + `/agent-api` behind the frontend container. +- **CORS is the one exception, and the one backend setting you must get right.** + Over a tunnel that origin is `http://127.0.0.1:`, so the VM's + `CORS_ALLOWED_ORIGINS` has to allow it. See + [CORS on the VM](#cors-on-the-vm-the-403-nobody-can-read). +- Everything the client adds is what a browser tab cannot show: which VM you + are on, how you are reaching it, and whether that path is healthy. + +Both transports resolve to the same thing — an origin — so nothing downstream +of the transport layer knows or cares which one is in use. + +## Install + +### From a release build + +| Platform | Artifact | +| --- | --- | +| macOS (Apple Silicon / Intel) | `DeepSQL--arm64.dmg`, `-x64.dmg` | +| Windows | `DeepSQL Setup .exe` (NSIS), portable `.exe` | +| Linux | `.AppImage`, `.deb`, `.rpm` | + +macOS builds are unsigned unless you supply signing credentials, so the first +launch needs **right-click → Open** (or `xattr -dr com.apple.quarantine +/Applications/DeepSQL.app`). + +### From source + +**Requires Node.js 22+** (matches the Desktop release CI). GUI needs a display; on +headless Linux use `xvfb-run` for the packaged app or the selftests. + +```bash +cd desktop +npm install +npm start # run the app +npm run dev # run with DevTools open (unpackaged only — see "DevTools") +npm test # drift guard for the DevTools kill switch +``` + +## Building installers + +```bash +npm run icons # regenerate build/icon.png from build/icon.svg +npm run dist:mac # dmg + zip, arm64 and x64 +npm run dist:win # nsis + portable, x64 and arm64 +npm run dist:linux # AppImage + deb + rpm +npm run dist # every target this host can build +``` + +Artifacts land in `desktop/release/`. Cross-building is limited by the host: +Windows targets need Wine on macOS/Linux, and Linux targets are most reliable in +the `electron-builder` Docker image. Building each platform on its own CI runner +is the low-friction option. + +**Code signing** is off by default. Set the standard electron-builder +environment variables to enable it: + +- macOS: `CSC_LINK`, `CSC_KEY_PASSWORD`, plus `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID` for notarisation. +- Windows: `CSC_LINK`, `CSC_KEY_PASSWORD` (or an Azure Trusted Signing config). + +**Auto-update** is opt-in. `package.json` sets `"publish": null`, so no update +feed is baked in and the updater no-ops. To enable it, either set a `publish` +target (GitHub Releases, S3, generic) before building, or point +`DEEPSQL_UPDATE_FEED` at a generic feed URL at runtime. + +## Connecting + +### Direct over TLS + +Enter the origin that serves the DeepSQL UI — the same URL you would open in a +browser. Certificate verification has four modes: + +| Mode | Use when | +| --- | --- | +| **Publicly trusted** | The VM has a real hostname and a Let's Encrypt / commercial certificate. Default. | +| **Pinned certificate** | Self-signed certificate, or the server is only reachable by IP. Leave the fingerprint blank to pin whatever is presented on the first connection. | +| **Private CA bundle** | Corporate internal PKI. Point it at the CA `.pem`. | +| **Trust on first use** | Bootstrapping only. Accepts any certificate once, then pins it. Shown in red, and the workspace chrome badges the connection `UNVERIFIED`. | + +The pin applies to both the health probe (Node) and the embedded browser +(Chromium), so the UI is held to exactly the same rule as the connection check. + +### SSH tunnel + +Equivalent to `ssh -i key.pem -L :127.0.0.1:3000 ubuntu@vm`, but run +in-process — no `ssh` binary required, which matters on Windows. + +Use it when the DeepSQL ports are closed to the internet. The Compose stack only +publishes to the VM's own host, so a tunnel reaches them without exposing +anything publicly. + +- **Remote port** is DeepSQL *as seen from inside the VM*. Use `3000`, the + frontend container. Pointing it at a host reverse proxy on `:80` usually + returns 404: that proxy matches on `server_name`, and a tunnel arrives with + `Host: 127.0.0.1:`, which matches no vhost. The container's own nginx + uses `server_name _` and answers whatever Host it is given. +- **Local port** `0` picks a free port and then reuses it on later launches, so + the origin (and therefore the web app's `localStorage`) stays stable. +- The listener binds `127.0.0.1` only — never the LAN. +- Host keys are **trust on first use, then strict**. A changed host key aborts + the connection with both fingerprints shown rather than offering a dialog to + click through. If the VM was genuinely rebuilt, clear the pinned key in the + connection settings. + +`http://127.0.0.1:` is a secure context in Chromium, so the backend's +`Secure` session cookies are still accepted over the tunnel. + +### CORS on the VM (the 403 nobody can read) + +The tunnel gives the web app the origin `http://127.0.0.1:`, not your VM's +hostname. If `CORS_ALLOWED_ORIGINS` on the VM names only the hostname, the +backend rejects that origin — and it does so in the most confusing way +available: + +- Chromium omits `Origin` on same-origin **GET**s, so the health probe, the SPA + and every read work. The connection tests green. +- Chromium *does* send `Origin` on same-origin **POST/PUT/DELETE**, and Spring + treats any request carrying `Origin` as cross-origin (the same-origin + short-circuit was removed in Spring 5.3). So the first POST — the login — + returns `403` with the plain-text body `Invalid CORS request`. +- That body has no `message` field, so the web app's axios interceptor falls + back to axios's own wording: **"Request failed with status code 403"**, naming + neither CORS nor the origin. + +Fix it on the VM by keeping loopback patterns in the allowlist alongside your +hostname, then restarting the backend: + +```bash +# /home//deepsql-self-host/.env +CORS_ALLOWED_ORIGINS=https://deepsql.example.com,http://127.0.0.1:*,http://localhost:* + +docker compose up -d backend +``` + +The `*` is a **port** wildcard, which matters because the tunnel's local port is +chosen at runtime; enumerating ports means re-editing the VM whenever it +changes. It is legal only because `SecurityConfig` uses +`setAllowedOriginPatterns` — plain `setAllowedOrigins` rejects `*` in +combination with `allowCredentials(true)`. Note that `CORS_ALLOWED_ORIGINS` +*replaces* the built-in list rather than extending it, which is how the loopback +entries usually go missing. + +Verify without opening the app: + +```bash +curl -s -o /dev/null -w '%{http_code}\n' \ + -H "Origin: http://127.0.0.1:" http://127.0.0.1:/api/actuator/health +# 200 = allowed · 403 = still missing from CORS_ALLOWED_ORIGINS +``` + +Since the probe now sends an `Origin` header, the launcher catches this at +connect time and says so, instead of letting it surface as a failed login. + +## Security model + +- **Secrets** (key passphrases, SSH passwords) are encrypted with Electron + `safeStorage`, backed by Keychain / DPAPI / libsecret. On a machine with no + usable secret service, nothing is written to disk and the secret is kept in + memory for the session only — the launcher says so rather than pretending. +- **Session isolation**: each connection gets its own persistent partition + (`persist:deepsql-`), so signing into two DeepSQL servers never + crosses cookies or cached state. +- **Renderer hardening**: `contextIsolation` on, `nodeIntegration` off, the + content view sandboxed, `webview` attachment blocked, and permissions denied + by default except sanitised clipboard writes. +- **Navigation confinement**: the embedded view cannot leave the DeepSQL origin. + Same-origin popups (shared dashboards) open in a window on the same session; + everything else goes to the OS browser. +- **No credential proxying**: the client never sees database credentials. It + speaks to DeepSQL's own API surface exactly as a browser does. +- **DevTools are off in packaged builds** — see below. + +## DevTools + +A packaged build cannot open DevTools. Every window sets +`webPreferences.devTools: DEVTOOLS_ENABLED`, and `config.js` defines that as +`!app.isPackaged` — nothing else. Chromium then refuses to attach DevTools at +all, so `openDevTools()` is a no-op and the shortcuts do nothing; the View menu +omits **Toggle Developer Tools** (and with it the `Alt+Cmd+I` / `Ctrl+Shift+I` +binding, which that item owned, since the app installs its own menu and so gets +no `toggleDevTools` role from Electron). + +`DEVTOOLS_ENABLED` is deliberately **not** `IS_DEV`. `IS_DEV` is true whenever +`DEEPSQL_DESKTOP_DEV=1`, and any user can set that on the shipped app — +`DEEPSQL_DESKTOP_DEV=1 open -a DeepSQL` used to open DevTools automatically on +both windows, no menu involved. Gating on `app.isPackaged` alone is what makes +the switch unreachable from outside the build. + +The app also refuses to start when passed `--remote-debugging-port`, +`--remote-debugging-pipe`, `--remote-allow-origins`, or `--inspect*`. Those open +a DevTools *protocol* endpoint, a separate door that `devTools: false` does not +close, and Chromium parses them before any app code runs — so the only remedy is +to exit immediately, before a window opens or a tunnel comes up. + +Development is unaffected: `npm start` and `npm run dev` are unpackaged, so +DevTools work as before. + +**Verifying the block** (source assertions cannot prove runtime behaviour): + +```bash +npm test # drift guard: every window sets devTools, gated correctly +npm run dist:mac # then, in the installed app: +# View menu has no "Toggle Developer Tools"; Alt+Cmd+I does nothing +DEEPSQL_DESKTOP_DEV=1 open -a DeepSQL # no DevTools — the closed hole +/Applications/DeepSQL.app/Contents/MacOS/DeepSQL --remote-debugging-port=9222 +# exits 1, logging "refusing to start with remote debugging enabled" +``` + +Known limits, stated plainly: `ELECTRON_RUN_AS_NODE=1` turns the binary into a +plain Node process that never loads the app, and anyone able to modify the app +bundle can undo any of this. These controls stop a curious user poking at the +shipped client; they are not a defence against someone who controls the machine. +Treat the backend's authorization as the real boundary. + +## Editing a connected profile + +Saving a profile applies it. If the connection is live and the edit changes what +the transport does, the connection is rebuilt onto the new settings and the open +window follows it to the new origin — a rebuilt tunnel binds a different local +port, so the origin changes with it. + +The comparison is `profiles.transportFingerprint()`, over the fields that decide +what the connection *is*: transport, URL, every TLS field, and the SSH host, +port, username, auth method, key path, remote host/port/scheme, pinned local +port, and pinned host key. Renaming a connection is not in it, so a rename never +costs you a working tunnel. `stickyLocalPort` is not in it either — we choose +that, not the user, and including it would make every connection differ from +itself on the next connect. + +This is all-or-nothing per save. If the rebuild fails, the old connection is +**not** kept: it was built from settings that no longer exist, so it is closed +and the failure is reported, naming the fact that the previous session used the +settings you replaced. A window that looks connected while serving settings you +have changed is the state this design removes. + +Before this, `connect()` reused any live connection unconditionally. The launcher +persists the form before every Connect, so the stored profile was always correct +and the *store* was never the problem — the reused connection simply kept running +the old settings, and Test reported a confident pass for settings that had been +replaced. Changing a tunnel's remote port and pressing Connect did nothing at +all. + +```bash +npm run selftest:settings # two fake servers; proves an edit moves the connection +``` + +## Deep links + +`deepsql://connect?url=https://deepsql.example.com&name=Production` opens the +launcher with a connection pre-filled — a one-click onboarding link for an admin +to paste into internal docs. `transport=tunnel`, `sshHost` and `sshUser` are also +accepted. + +## Diagnostics + +```bash +# Check a server the same way the app does, without opening a window +npm run smoke -- --url https://deepsql.example.com +npm run smoke -- --ssh-host 20.29.48.144 --ssh-user ubuntu --key ~/keys/vm.pem + +# Exercise the tunnel end to end against a throwaway in-process SSH server +npm run selftest:tunnel + +# Prove an edited setting reaches the live connection (two fake DeepSQL servers) +npm run selftest:settings + +# Drift guard for the DevTools kill switch +npm test +``` + +Both self-tests redirect `userData` to a temp directory, so they never touch your +real `profiles.json`. + +Logs are at `/logs/desktop.log`; the launcher footer has an **Open log +file** link. Connection profiles live in `/profiles.json` (mode 0600, +secrets stored only as `safeStorage` ciphertext). + +| Platform | userData | +| --- | --- | +| macOS | `~/Library/Application Support/DeepSQL` | +| Windows | `%APPDATA%\DeepSQL` | +| Linux | `~/.config/DeepSQL` | + +## Layout + +``` +src/ + main/ + index.js app lifecycle, single instance, deep links + config.js shared constants + profiles.js connection profiles + persistence + secrets.js safeStorage wrapper + transport.js transport manager (connect/disconnect/health) + tunnel.js SSH local forward (ssh2) + tls.js certificate policy for Node and Chromium + probe.js /api/actuator/health reachability check + ipc.js every renderer→main entry point + menu.js, updater.js, logger.js + windows/ + launcher.js connection manager window + workspace.js frameless shell: native chrome + DeepSQL view + preload/ narrow contextBridge APIs + renderer/ + shared/theme.css DeepSQL design tokens (mirrors src/index.css) + launcher/ connection manager UI + chrome/ workspace top chrome +scripts/ + generate-icons.js SVG → build/icon.png via Electron + smoke.js headless connection check + tunnel-selftest.js end-to-end SSH tunnel test + settings-selftest.js proves an edited setting reaches the live connection +``` + +The renderers are plain HTML/CSS/JS with no build step: they are chrome around +an IPC surface, and the DeepSQL React app they wrap is loaded from the server, +not reimplemented. `renderer/shared/theme.css` mirrors the tokens in the web +app's `src/index.css` — keep the two in step if the palette moves. diff --git a/desktop/build/entitlements.mac.plist b/desktop/build/entitlements.mac.plist new file mode 100644 index 0000000..8078085 --- /dev/null +++ b/desktop/build/entitlements.mac.plist @@ -0,0 +1,22 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + com.apple.security.network.client + + + com.apple.security.network.server + + + com.apple.security.files.user-selected.read-only + + + diff --git a/desktop/build/icon.png b/desktop/build/icon.png new file mode 100644 index 0000000..109c8c8 Binary files /dev/null and b/desktop/build/icon.png differ diff --git a/desktop/build/icon.svg b/desktop/build/icon.svg new file mode 100644 index 0000000..683bd0d --- /dev/null +++ b/desktop/build/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..f0b6404 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,3712 @@ +{ + "name": "deepsql-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deepsql-desktop", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "electron-updater": "^6.8.9", + "ssh2": "^1.17.0" + }, + "devDependencies": { + "electron": "^43.2.0", + "electron-builder": "^26.15.3" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/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/@electron/asar/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/@electron/asar/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/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/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/@electron/universal/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/@electron/universal/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/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/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/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/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "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/cross-spawn/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/cross-spawn/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/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/dir-compare/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/dir-compare/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/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "43.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.2.0.tgz", + "integrity": "sha512-80zvrgG7ZRXD+tD0IyLvrnN9n+veSxadMRsMaC9wKKP3iUbtC7rGM8+dVuCmOb0Rrwwv8ESW4awnUZh9Hbp1fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-updater": { + "version": "6.8.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz", + "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.7.0", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/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/filelist/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/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/glob/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/glob/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/glob/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/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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-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/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "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-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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/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/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "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/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/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/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" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..7a745f9 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,164 @@ +{ + "name": "deepsql-desktop", + "productName": "DeepSQL", + "version": "0.1.0", + "private": true, + "description": "DeepSQL desktop client — connect to a self-hosted DeepSQL VM over TLS or an SSH tunnel.", + "license": "Apache-2.0", + "author": { + "name": "DeepSQL", + "email": "support@deepsql.local" + }, + "type": "commonjs", + "main": "src/main/index.js", + "scripts": { + "start": "electron .", + "dev": "DEEPSQL_DESKTOP_DEV=1 electron .", + "test": "node --test src/main/*.test.js", + "icons": "electron scripts/generate-icons.js", + "pack": "electron-builder --dir", + "dist": "electron-builder", + "dist:mac": "electron-builder --mac", + "dist:win": "electron-builder --win", + "dist:linux": "electron-builder --linux", + "dist:all": "electron-builder --mac --win --linux", + "smoke": "electron scripts/smoke.js", + "selftest:tunnel": "electron scripts/tunnel-selftest.js", + "selftest:settings": "electron scripts/settings-selftest.js" + }, + "dependencies": { + "electron-updater": "^6.8.9", + "ssh2": "^1.17.0" + }, + "devDependencies": { + "electron": "^43.2.0", + "electron-builder": "^26.15.3" + }, + "build": { + "appId": "com.deepsql.desktop", + "productName": "DeepSQL", + "copyright": "Copyright © DeepSQL", + "directories": { + "output": "release", + "buildResources": "build" + }, + "files": [ + "src/**/*", + "build/icon.png", + "!**/*.map" + ], + "protocols": [ + { + "name": "DeepSQL", + "schemes": [ + "deepsql" + ] + } + ], + "asar": true, + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + { + "target": "dmg", + "arch": [ + "arm64", + "x64" + ] + }, + { + "target": "zip", + "arch": [ + "arm64", + "x64" + ] + } + ], + "icon": "build/icon.png", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "build/entitlements.mac.plist", + "entitlementsInherit": "build/entitlements.mac.plist", + "extendInfo": { + "NSHumanReadableCopyright": "Copyright © DeepSQL" + } + }, + "dmg": { + "title": "DeepSQL ${version}", + "contents": [ + { + "x": 140, + "y": 200, + "type": "file" + }, + { + "x": 400, + "y": 200, + "type": "link", + "path": "/Applications" + } + ] + }, + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } + ], + "icon": "build/icon.png" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "DeepSQL" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "deb", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "rpm", + "arch": [ + "x64" + ] + } + ], + "icon": "build/icon.png", + "category": "Development", + "maintainer": "DeepSQL ", + "desktop": { + "entry": { + "Name": "DeepSQL", + "Comment": "AI-powered Database Performance Assistant", + "MimeType": "x-scheme-handler/deepsql" + } + } + }, + "publish": null + } +} diff --git a/desktop/scripts/generate-icons.js b/desktop/scripts/generate-icons.js new file mode 100644 index 0000000..db26365 --- /dev/null +++ b/desktop/scripts/generate-icons.js @@ -0,0 +1,52 @@ +'use strict'; + +/** + * Rasterise build/icon.svg to build/icon.png (1024×1024). + * + * Run with `npm run icons`. It uses Electron's own Chromium rather than + * ImageMagick/rsvg so contributors on any OS can regenerate the icon with the + * dependencies already in this package. electron-builder derives the .icns and + * .ico it needs from that single PNG at package time. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const { app, BrowserWindow, nativeImage } = require('electron'); + +const SIZE = 1024; +const buildDir = path.join(__dirname, '..', 'build'); +const svgPath = path.join(buildDir, 'icon.svg'); +const pngPath = path.join(buildDir, 'icon.png'); + +app.disableHardwareAcceleration(); + +app.whenReady().then(async () => { + const svg = fs.readFileSync(svgPath, 'utf8'); + const page = ` + ${svg}`; + + const window = new BrowserWindow({ + width: SIZE, + height: SIZE, + show: false, + transparent: true, + frame: false, + webPreferences: { offscreen: true }, + }); + + await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(page)}`); + const image = await window.webContents.capturePage({ + x: 0, + y: 0, + width: SIZE, + height: SIZE, + }); + + const png = nativeImage.createFromBuffer(image.toPNG()).toPNG(); + fs.writeFileSync(pngPath, png); + process.stdout.write(`wrote ${pngPath} (${SIZE}x${SIZE}, ${png.length} bytes)\n`); + + window.destroy(); + app.quit(); +}); diff --git a/desktop/scripts/settings-selftest.js b/desktop/scripts/settings-selftest.js new file mode 100644 index 0000000..787a053 --- /dev/null +++ b/desktop/scripts/settings-selftest.js @@ -0,0 +1,190 @@ +'use strict'; + +/** + * End-to-end self-test for "an edited setting takes effect". + * + * Stands up two fake DeepSQL servers on different loopback ports and drives the + * real profiles + transport code against them. Editing a profile to point at the + * second server must move the live connection to it; that is the whole bug this + * covers, and it is invisible to any check that only inspects what was saved, + * because saving was never the broken half. + * + * npm run selftest:settings + */ + +const fs = require('node:fs'); +const http = require('node:http'); +const os = require('node:os'); +const path = require('node:path'); +const { app } = require('electron'); + +// Before anything reads it: the real store holds the user's live connections. +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deepsql-settings-test-')); +app.setPath('userData', tmpDir); + +const results = []; +function check(name, condition, detail = '') { + results.push({ name, ok: Boolean(condition), detail }); + process.stdout.write(` ${condition ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}\n`); +} + +/** A stand-in for the DeepSQL nginx, tagged so we can tell the two apart. */ +function fakeDeepSql(label) { + return http.createServer((req, res) => { + if (req.url === '/api/actuator/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'UP', server: label })); + return; + } + res.writeHead(404); + res.end(); + }); +} + +function listen(server) { + return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server.address().port))); +} + +app.whenReady().then(async () => { + const profiles = require('../src/main/profiles'); + const transport = require('../src/main/transport'); + + const serverA = fakeDeepSql('A'); + const serverB = fakeDeepSql('B'); + const portA = await listen(serverA); + const portB = await listen(serverB); + const originA = `http://127.0.0.1:${portA}`; + const originB = `http://127.0.0.1:${portB}`; + + process.stdout.write(`\nFake DeepSQL A=${originA} B=${originB}\n\n`); + + try { + // ── The reported bug: change a setting, and the behaviour must follow ── + process.stdout.write('Edited settings reach the live connection\n'); + + const saved = profiles.upsert({ name: 'Test', transport: 'direct', url: originA }); + const id = saved.id; + + const first = await transport.connect(id); + check('connects to the configured server', first.origin === originA, first.origin); + + const second = await transport.connect(id); + check( + 'an unchanged reconnect reuses the live connection', + second.reused === true && second.origin === originA, + `reused=${second.reused} origin=${second.origin}`, + ); + + // Exactly what the launcher does on Connect: persist the form, then connect. + profiles.upsert({ id, name: 'Test', transport: 'direct', url: originB }); + const third = await transport.connect(id); + check( + 'connecting after an edit moves to the new server', + third.origin === originB, + `origin=${third.origin}${third.reused ? ' (REUSED STALE CONNECTION)' : ''}`, + ); + check( + 'the stale connection was not reused', + third.reused !== true, + third.reused ? 'connect() returned the pre-edit origin' : '', + ); + + // ── reconcile(): what makes a save take effect immediately ──────────── + process.stdout.write('\nreconcile() applies a save without waiting for Connect\n'); + + profiles.upsert({ id, name: 'Test', transport: 'direct', url: originA }); + const reconciled = await transport.reconcile(id); + check( + 'reconcile rebuilds onto the saved settings', + reconciled.changed === true && reconciled.ok === true && reconciled.origin === originA, + `changed=${reconciled.changed} ok=${reconciled.ok} origin=${reconciled.origin}`, + ); + check( + 'the live origin now matches the saved profile', + transport.originFor(id) === originA, + transport.originFor(id), + ); + + // A rename must not cost the user their session. + const before = transport.get(id); + profiles.upsert({ id, name: 'Renamed', transport: 'direct', url: originA }); + const renamed = await transport.reconcile(id); + check( + 'renaming does not rebuild the connection', + renamed.changed === false && transport.get(id) === before, + `changed=${renamed.changed} sameEntry=${transport.get(id) === before}`, + ); + + // ── A failed rebuild must not leave the old connection running ──────── + process.stdout.write('\nA rebuild that fails leaves no half-applied state\n'); + + await new Promise((resolve) => serverB.close(resolve)); + profiles.upsert({ id, name: 'Renamed', transport: 'direct', url: originB }); + const failed = await transport.reconcile(id); + check( + 'reconcile reports the failure', + failed.changed === true && failed.ok === false, + `ok=${failed.ok} detail=${failed.detail}`, + ); + check( + 'no connection is left serving the replaced settings', + transport.isConnected(id) === false, + transport.isConnected(id) ? `still connected to ${transport.originFor(id)}` : '', + ); + + // ── test() must describe the settings on screen ─────────────────────── + process.stdout.write('\nTest reports on current settings, not the live ones\n'); + + profiles.upsert({ id, name: 'Renamed', transport: 'direct', url: originA }); + await transport.connect(id); + const serverC = fakeDeepSql('C'); + const portC = await listen(serverC); + const originC = `http://127.0.0.1:${portC}`; + profiles.upsert({ id, name: 'Renamed', transport: 'direct', url: originC }); + const tested = await transport.test(id); + check( + 'testing after an edit exercises the edited settings', + tested.ok === true && tested.rebuilt === true && transport.originFor(id) === originC, + `ok=${tested.ok} rebuilt=${tested.rebuilt} live=${transport.originFor(id)}`, + ); + await new Promise((resolve) => serverC.close(resolve)); + + // ── The fingerprint itself ──────────────────────────────────────────── + process.stdout.write('\nTransport fingerprint\n'); + + const base = profiles.get(id); + const sameName = { ...base, name: 'Something else' }; + check( + 'name is not part of the transport identity', + profiles.transportFingerprint(base) === profiles.transportFingerprint(sameName), + ); + const otherPort = { ...base, ssh: { ...base.ssh, remotePort: base.ssh.remotePort + 1 } }; + check( + 'the SSH remote port is part of it', + profiles.transportFingerprint(base) !== profiles.transportFingerprint(otherPort), + ); + const otherLocal = { ...base, ssh: { ...base.ssh, localPort: 44444 } }; + check( + 'the pinned local port is part of it', + profiles.transportFingerprint(base) !== profiles.transportFingerprint(otherLocal), + ); + const otherSticky = { ...base, ssh: { ...base.ssh, stickyLocalPort: 44444 } }; + check( + 'the sticky local port is NOT (we choose it, not the user)', + profiles.transportFingerprint(base) === profiles.transportFingerprint(otherSticky), + ); + + await transport.disconnectAll(); + } catch (err) { + check('self-test ran to completion', false, err.stack || err.message); + } + + await new Promise((resolve) => serverA.close(resolve)); + + const failed = results.filter((r) => !r.ok); + process.stdout.write( + `\n${results.length - failed.length}/${results.length} checks passed\n`, + ); + fs.rmSync(tmpDir, { recursive: true, force: true }); + app.exit(failed.length === 0 ? 0 : 1); +}); diff --git a/desktop/scripts/smoke.js b/desktop/scripts/smoke.js new file mode 100644 index 0000000..bb51b98 --- /dev/null +++ b/desktop/scripts/smoke.js @@ -0,0 +1,100 @@ +'use strict'; + +/** + * Headless connection check — the same transport code the app uses, without a + * window. Handy for diagnosing a VM from a terminal or CI before shipping a + * profile to users. + * + * npm run smoke -- --url https://deepsql.example.com + * npm run smoke -- --url https://10.0.0.5 --tls pinned + * npm run smoke -- --ssh-host 20.29.48.144 --ssh-user ubuntu \ + * --key ~/keys/vm.pem [--passphrase secret] [--remote-port 80] + * + * It runs against a throwaway userData directory so it never touches the + * profiles or keychain entries of the installed app. + */ + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { app } = require('electron'); + +const tmpUserData = fs.mkdtempSync(path.join(os.tmpdir(), 'deepsql-smoke-')); +app.setPath('userData', tmpUserData); + +const args = parseArgs(process.argv.slice(2)); + +app.whenReady().then(async () => { + const profiles = require('../src/main/profiles'); + const transport = require('../src/main/transport'); + + const useTunnel = Boolean(args['ssh-host']); + const profile = profiles.upsert({ + name: 'smoke', + transport: useTunnel ? 'tunnel' : 'direct', + url: args.url || '', + tls: { mode: args.tls || 'system', fingerprint: args.fingerprint || '', caPath: args.ca || '' }, + ssh: { + host: args['ssh-host'] || '', + port: args['ssh-port'] || 22, + username: args['ssh-user'] || 'ubuntu', + authMethod: args.key ? 'key' : args.password ? 'password' : 'agent', + privateKeyPath: args.key ? expand(args.key) : '', + passphrase: args.passphrase, + password: args.password, + remoteHost: args['remote-host'] || '127.0.0.1', + remotePort: args['remote-port'] || 80, + remoteScheme: args['remote-scheme'] || 'http', + localPort: 0, + }, + }); + + transport.on('status', (event) => { + if (event.detail) log(` ${event.detail}`); + }); + + log(`transport : ${profile.transport}`); + log(useTunnel ? `ssh : ${profile.ssh.username}@${profile.ssh.host}` : `url : ${profile.url}`); + + const result = await transport.test(profile.id); + + if (result.ok) { + log(''); + log(' OK'); + log(` origin : ${result.origin}`); + log(` latency : ${result.latencyMs} ms`); + if (result.certificateFingerprint) log(` cert : ${result.certificateFingerprint}`); + if (result.hostKeyFingerprint) log(` host key : ${result.hostKeyFingerprint}`); + } else { + log(''); + log(` FAILED (${result.code})`); + log(` ${result.detail}`); + } + + fs.rmSync(tmpUserData, { recursive: true, force: true }); + app.exit(result.ok ? 0 : 1); +}); + +function parseArgs(argv) { + const out = {}; + for (let i = 0; i < argv.length; i += 1) { + if (!argv[i].startsWith('--')) continue; + const key = argv[i].slice(2); + const next = argv[i + 1]; + if (next === undefined || next.startsWith('--')) { + out[key] = true; + } else { + out[key] = next; + i += 1; + } + } + return out; +} + +function expand(p) { + return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p; +} + +function log(line) { + process.stdout.write(`${line}\n`); +} diff --git a/desktop/scripts/tunnel-selftest.js b/desktop/scripts/tunnel-selftest.js new file mode 100644 index 0000000..0ea917e --- /dev/null +++ b/desktop/scripts/tunnel-selftest.js @@ -0,0 +1,303 @@ +'use strict'; + +/** + * End-to-end self-test for the SSH tunnel transport. + * + * Stands up a throwaway SSH server and a fake DeepSQL health endpoint on + * loopback, then drives the real SshTunnel + probe code against them. It + * exercises the parts that are otherwise only reachable with a live VM and a + * private key: key parsing, publickey auth, host-key pinning, direct-tcpip + * forwarding, and the sticky local port. + * + * npm run selftest:tunnel + */ + +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const http = require('node:http'); +const net = require('node:net'); +const os = require('node:os'); +const path = require('node:path'); +const { app } = require('electron'); + +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deepsql-tunnel-test-')); +app.setPath('userData', tmpDir); + +const results = []; +function check(name, condition, detail = '') { + results.push({ name, ok: Boolean(condition), detail }); + process.stdout.write(` ${condition ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}\n`); +} + +app.whenReady().then(async () => { + const { Server } = require('ssh2'); + const { SshTunnel, hostKeyFingerprint } = require('../src/main/tunnel'); + const { probe } = require('../src/main/probe'); + + // ── Fixtures ─────────────────────────────────────────────────────────── + const { privateKey, publicKeyPem } = generateRsaPem(); + const keyPath = path.join(tmpDir, 'test-key.pem'); + fs.writeFileSync(keyPath, privateKey, { mode: 0o600 }); + + // Stands in for the DeepSQL nginx inside the VM. + // + // It also emulates Spring's CORS check, because that is a real failure mode + // of the tunnel transport: the origin is http://127.0.0.1:, and + // a VM whose CORS_ALLOWED_ORIGINS lists only its hostname answers 403 with a + // plain-text body. Spring rejects on the presence of `Origin` alone — there + // is no same-origin exemption — so this mirrors that rule exactly. + let lastProbeOrigin = null; + let corsAllowedOrigins = null; // null = allow everything + const upstream = http.createServer((req, res) => { + if (req.url === '/api/actuator/health') { + const origin = req.headers.origin || null; + lastProbeOrigin = origin; + if (origin && corsAllowedOrigins && !corsAllowedOrigins.includes(origin)) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Invalid CORS request'); + return; + } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end('{"status":"UP"}'); + return; + } + res.writeHead(404).end(); + }); + await listen(upstream, 0, '127.0.0.1'); + const upstreamPort = upstream.address().port; + + let forwardedTo = null; + // Flipped on to emulate a VM with `AllowTcpForwarding no`: login succeeds, + // every direct-tcpip channel is refused. + let refuseForwarding = false; + const sshServer = new Server({ hostKeys: [privateKey] }, (client) => { + client.on('authentication', (ctx) => { + // Accepting any publickey is fine here: the point is to exercise our + // client, not to be a real authenticator. + if (ctx.method === 'publickey') ctx.accept(); + else ctx.reject(['publickey']); + }); + client.on('ready', () => { + client.on('tcpip', (accept, reject, info) => { + if (refuseForwarding) { + // 1 = ADMINISTRATIVELY_PROHIBITED. Verified against a real OpenSSH + // server with `AllowTcpForwarding no`, which sends exactly this with + // the description "open failed". A bare reject() would default to + // CONNECT_FAILED and test the wrong branch. + reject(1, 'open failed'); + return; + } + forwardedTo = `${info.destIP}:${info.destPort}`; + const channel = accept(); + const socket = net.connect(info.destPort, info.destIP, () => { + channel.pipe(socket).pipe(channel); + }); + socket.on('error', () => channel.end()); + }); + }); + client.on('error', () => {}); + }); + await listen(sshServer, 0, '127.0.0.1'); + const sshPort = sshServer.address().port; + + const expectedHostKey = hostKeyFingerprint(publicKeyPem.sshWireFormat); + + // ── 1. First connection pins the host key and forwards traffic ───────── + const profile = makeProfile({ keyPath, sshPort, upstreamPort }); + const tunnel = new SshTunnel(profile, {}); + const { localPort, hostKeyFingerprint: seenHostKey } = await tunnel.start(); + + check('tunnel binds a loopback port', localPort > 0, `port ${localPort}`); + check( + 'host key fingerprint matches the server key', + seenHostKey === expectedHostKey, + seenHostKey, + ); + + const health = await probe(`http://127.0.0.1:${localPort}`, profile); + check('health probe succeeds through the tunnel', health.ok, health.detail); + check( + 'traffic is forwarded to the configured remote endpoint', + forwardedTo === `127.0.0.1:${upstreamPort}`, + forwardedTo, + ); + + // The probe must announce the origin the browser is about to use, or a CORS + // allowlist that omits it stays invisible until the user's first login POST. + check( + 'probe sends the browser origin so CORS is checked at connect time', + lastProbeOrigin === `http://127.0.0.1:${localPort}`, + lastProbeOrigin === null ? 'no Origin header sent' : lastProbeOrigin, + ); + + // Same tunnel, same healthy backend — only the allowlist changes. + corsAllowedOrigins = ['https://deepsql.example.com']; + const corsHealth = await probe(`http://127.0.0.1:${localPort}`, profile); + check( + 'an origin missing from CORS_ALLOWED_ORIGINS fails the probe', + !corsHealth.ok && corsHealth.status === 403, + `ok=${corsHealth.ok} status=${corsHealth.status}`, + ); + corsAllowedOrigins = null; + + // The listener must never be reachable from anything but loopback. + check( + 'listener is bound to loopback only', + tunnel.server.address().address === '127.0.0.1', + tunnel.server.address().address, + ); + + await tunnel.stop(); + check('stop() releases the local port', await portIsFree(localPort)); + + // ── 2. A changed host key is refused ─────────────────────────────────── + const tamperedProfile = makeProfile({ keyPath, sshPort, upstreamPort }); + tamperedProfile.ssh.hostKeyFingerprint = 'SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + const strictTunnel = new SshTunnel(tamperedProfile, {}); + let rejected = null; + try { + await strictTunnel.start(); + } catch (err) { + rejected = err; + } + await strictTunnel.stop(); + check( + 'mismatched host key aborts the connection', + rejected !== null, + rejected ? rejected.code : 'connection was allowed', + ); + + // ── 3. A refused forwarding channel fails before the listener binds ──── + // Regression guard: this used to surface as "socket hang up" only once the + // browser made a request, with nothing pointing at the SSH layer at all. + // + // ssh2's server-side reject() takes no arguments and always answers + // CONNECT_FAILED, so the live half of this test can only cover that branch. + // The ADMINISTRATIVELY_PROHIBITED branch — what a real `AllowTcpForwarding no` + // server sends — is checked directly below against the error shape observed + // from OpenSSH: `{ reason: 1, message: '(SSH) Channel open failure: open failed' }`. + refuseForwarding = true; + const deniedProfile = makeProfile({ keyPath, sshPort, upstreamPort }); + const deniedTunnel = new SshTunnel(deniedProfile, {}); + let denied = null; + try { + await deniedTunnel.start(); + } catch (err) { + denied = err; + } + await deniedTunnel.stop(); + check( + 'a refused channel fails start() instead of the later HTTP request', + denied?.code === 'remote-port-closed', + denied ? denied.code : 'start() succeeded', + ); + check( + 'the failure never leaves a listener bound', + deniedTunnel.server === null || deniedTunnel.localPort === null, + `server=${deniedTunnel.server === null ? 'null' : 'bound'}`, + ); + refuseForwarding = false; + + const classifier = new SshTunnel(makeProfile({ keyPath, sshPort, upstreamPort }), {}); + const prohibited = classifier.translateChannelError({ + reason: 1, + message: '(SSH) Channel open failure: open failed', + }); + check( + 'AllowTcpForwarding=no is named as the cause', + prohibited.code === 'forwarding-denied' && /allowtcpforwarding/i.test(prohibited.message), + prohibited.code, + ); + check( + 'a closed remote port is distinguished from a forwarding ban', + classifier.translateChannelError({ reason: 2 }).code === 'remote-port-closed', + ); + + // ── 4. Missing key file fails with an actionable message ─────────────── + const noKeyProfile = makeProfile({ keyPath: path.join(tmpDir, 'nope.pem'), sshPort, upstreamPort }); + let keyError = null; + try { + await new SshTunnel(noKeyProfile, {}).start(); + } catch (err) { + keyError = err; + } + check('missing key file is reported clearly', keyError?.code === 'key-unreadable', keyError?.code); + + // ── 5. Teardown ──────────────────────────────────────────────────────── + sshServer.close(); + upstream.close(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + + const failed = results.filter((r) => !r.ok).length; + process.stdout.write(`\n${results.length - failed}/${results.length} checks passed\n`); + app.exit(failed === 0 ? 0 : 1); +}); + +function makeProfile({ keyPath, sshPort, upstreamPort }) { + return { + id: 'selftest', + name: 'selftest', + transport: 'tunnel', + tls: { mode: 'system' }, + ssh: { + host: '127.0.0.1', + port: sshPort, + username: 'tester', + authMethod: 'key', + privateKeyPath: keyPath, + remoteHost: '127.0.0.1', + remotePort: upstreamPort, + remoteScheme: 'http', + localPort: 0, + stickyLocalPort: 0, + hostKeyFingerprint: '', + }, + }; +} + +function generateRsaPem() { + const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, + publicKeyEncoding: { type: 'pkcs1', format: 'der' }, + }); + // ssh2 fingerprints the SSH wire format of the public key, so rebuild it from + // the RSA modulus/exponent rather than trusting the DER encoding. + const jwk = crypto.createPublicKey(privateKey).export({ format: 'jwk' }); + const e = Buffer.from(jwk.e, 'base64url'); + const n = Buffer.from(jwk.n, 'base64url'); + const sshWireFormat = Buffer.concat([ + sshString(Buffer.from('ssh-rsa')), + sshString(prefixZeroIfSigned(e)), + sshString(prefixZeroIfSigned(n)), + ]); + return { privateKey, publicKeyPem: { der: publicKey, sshWireFormat } }; +} + +function sshString(buffer) { + const length = Buffer.alloc(4); + length.writeUInt32BE(buffer.length); + return Buffer.concat([length, buffer]); +} + +function prefixZeroIfSigned(buffer) { + return buffer[0] & 0x80 ? Buffer.concat([Buffer.from([0]), buffer]) : buffer; +} + +function listen(server, port, host) { + return new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, host, () => resolve()); + }); +} + +function portIsFree(port) { + return new Promise((resolve) => { + const socket = net.connect(port, '127.0.0.1'); + socket.on('connect', () => { + socket.destroy(); + resolve(false); + }); + socket.on('error', () => resolve(true)); + }); +} diff --git a/desktop/src/main/config.js b/desktop/src/main/config.js new file mode 100644 index 0000000..22b366a --- /dev/null +++ b/desktop/src/main/config.js @@ -0,0 +1,75 @@ +'use strict'; + +/** + * Shared constants for the DeepSQL desktop client. + * + * The client is a *thin* client: it never bundles a copy of the DeepSQL web UI. + * It navigates a WebContentsView at the real DeepSQL origin (reached either + * directly over TLS or through an SSH tunnel), so the UI a user sees is always + * exactly the version their VM is running. There is no bundle/backend skew to + * manage, and cookies behave the same as they do in a browser because the app + * content is served from a single origin (the frontend nginx in + * docker/nginx/default.conf already fronts /api and /agent-api). + * + * CORS is the one thing that does *not* come for free: over a tunnel that single + * origin is http://127.0.0.1:, which the VM's CORS_ALLOWED_ORIGINS + * has to allow. See the note in probe.js — the probe sends an Origin header so a + * missing entry is caught here rather than at the user's first login attempt. + */ + +const path = require('node:path'); +const { app } = require('electron'); + +const IS_DEV = + process.env.DEEPSQL_DESKTOP_DEV === '1' || !app.isPackaged; + +/** + * Whether DevTools may be opened at all. + * + * Deliberately NOT derived from IS_DEV. IS_DEV is true whenever + * DEEPSQL_DESKTOP_DEV=1, and that variable is settable by anyone running the + * shipped app — `DEEPSQL_DESKTOP_DEV=1 open -a DeepSQL` used to hand a user + * DevTools on both windows, already open, with no menu item involved. Gating on + * app.isPackaged alone is what makes the switch unreachable from outside the + * build: a packaged app cannot be talked into a dev build by its own + * environment. + * + * This value must reach every webPreferences block in the app. `devTools: false` + * disables DevTools inside Chromium, so openDevTools() becomes a no-op and the + * keyboard shortcuts do nothing — removing menu items alone would not, since a + * WebContents that permits DevTools can still be opened by any code path that + * survives. + */ +const DEVTOOLS_ENABLED = !app.isPackaged; + +// Health probe served by the Spring backend through the frontend nginx. +const HEALTH_PATH = '/api/actuator/health'; + +// How often the workspace re-probes the connection while a window is open. +const HEALTH_INTERVAL_MS = 20_000; + +// Height of the native top chrome above the embedded DeepSQL UI. +const CHROME_HEIGHT = 44; + +const RENDERER_DIR = path.join(__dirname, '..', 'renderer'); +const PRELOAD_DIR = path.join(__dirname, '..', 'preload'); + +const LAUNCHER_HTML = path.join(RENDERER_DIR, 'launcher', 'index.html'); +const CHROME_HTML = path.join(RENDERER_DIR, 'chrome', 'index.html'); +const LAUNCHER_PRELOAD = path.join(PRELOAD_DIR, 'launcher.js'); +const CHROME_PRELOAD = path.join(PRELOAD_DIR, 'chrome.js'); + +const PROTOCOL = 'deepsql'; + +module.exports = { + IS_DEV, + DEVTOOLS_ENABLED, + HEALTH_PATH, + HEALTH_INTERVAL_MS, + CHROME_HEIGHT, + LAUNCHER_HTML, + CHROME_HTML, + LAUNCHER_PRELOAD, + CHROME_PRELOAD, + PROTOCOL, +}; diff --git a/desktop/src/main/devtools.test.js b/desktop/src/main/devtools.test.js new file mode 100644 index 0000000..2ebbdd4 --- /dev/null +++ b/desktop/src/main/devtools.test.js @@ -0,0 +1,134 @@ +'use strict'; + +/** + * Drift guard for the DevTools kill switch. + * + * These are source assertions, not behavioural ones, because the modules they + * cover require('electron') at load time and cannot be imported outside an + * Electron process. That is a real limit — it catches a webPreferences block + * added without `devTools`, not a runtime regression inside Electron itself. + * The behavioural check is the manual one in README.md ("Verifying the block"). + * + * The failure this exists to prevent is silent: a new window added without + * `devTools` inherits Chromium's default of *enabled*, so the app keeps working + * perfectly and simply hands DevTools back to users on that window. + */ + +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const MAIN_DIR = __dirname; + +/** Every non-test .js file under src/main. */ +function sourceFiles(dir = MAIN_DIR, found = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) sourceFiles(full, found); + else if (entry.name.endsWith('.js') && !entry.name.endsWith('.test.js')) found.push(full); + } + return found; +} + +/** + * Blank out comments while preserving line numbering, so prose that merely + * mentions openDevTools() is not mistaken for a call site. + */ +function stripComments(source) { + return source + .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' ')) + .replace(/(^|[^:])\/\/[^\n]*/g, (m, prefix) => prefix + ' '.repeat(m.length - prefix.length)); +} + +/** + * Extract the `{ ... }` object literal that follows each `webPreferences:` key, + * matching braces so nested objects do not truncate the block. + */ +function webPreferenceBlocks(source) { + const blocks = []; + const key = /webPreferences\s*:\s*\{/g; + let match; + while ((match = key.exec(source)) !== null) { + let depth = 1; + let i = match.index + match[0].length; + while (i < source.length && depth > 0) { + if (source[i] === '{') depth += 1; + else if (source[i] === '}') depth -= 1; + i += 1; + } + blocks.push({ + body: source.slice(match.index, i), + line: source.slice(0, match.index).split('\n').length, + }); + } + return blocks; +} + +test('every webPreferences block sets devTools', () => { + const offenders = []; + for (const file of sourceFiles()) { + const source = fs.readFileSync(file, 'utf8'); + for (const block of webPreferenceBlocks(source)) { + if (!/\bdevTools\s*:/.test(block.body)) { + offenders.push(`${path.relative(MAIN_DIR, file)}:${block.line}`); + } + } + } + assert.deepStrictEqual( + offenders, + [], + `webPreferences without devTools (Chromium defaults these to enabled): ${offenders.join(', ')}`, + ); +}); + +test('devTools is gated on DEVTOOLS_ENABLED, never on IS_DEV', () => { + for (const file of sourceFiles()) { + const source = fs.readFileSync(file, 'utf8'); + for (const block of webPreferenceBlocks(source)) { + const setting = block.body.match(/\bdevTools\s*:\s*([^,\n}]+)/); + assert.ok(setting, `no devTools value parsed in ${path.relative(MAIN_DIR, file)}`); + assert.strictEqual( + setting[1].trim(), + 'DEVTOOLS_ENABLED', + `${path.relative(MAIN_DIR, file)}:${block.line} must use DEVTOOLS_ENABLED — ` + + 'IS_DEV is true whenever DEEPSQL_DESKTOP_DEV=1, which any user can set', + ); + } + } +}); + +test('DEVTOOLS_ENABLED does not read the environment', () => { + const config = fs.readFileSync(path.join(MAIN_DIR, 'config.js'), 'utf8'); + const declaration = config.match(/const DEVTOOLS_ENABLED\s*=\s*([^;]+);/); + assert.ok(declaration, 'DEVTOOLS_ENABLED declaration not found in config.js'); + const value = declaration[1].trim(); + + assert.strictEqual( + value, + '!app.isPackaged', + 'DEVTOOLS_ENABLED must derive from app.isPackaged alone. Deriving it from ' + + 'process.env or IS_DEV lets a user re-enable DevTools on the shipped app.', + ); +}); + +test('no code path opens DevTools without checking DEVTOOLS_ENABLED', () => { + const offenders = []; + for (const file of sourceFiles()) { + const lines = stripComments(fs.readFileSync(file, 'utf8')).split('\n'); + lines.forEach((line, index) => { + if (!/\b(openDevTools|toggleDevTools)\s*\(/.test(line)) return; + // Heuristic: the guard sits on this line or opens a block shortly above + // it (the menu template spreads a conditional array several lines up). + const context = lines.slice(Math.max(0, index - 10), index + 1).join('\n'); + if (!context.includes('DEVTOOLS_ENABLED')) { + offenders.push(`${path.relative(MAIN_DIR, file)}:${index + 1}`); + } + }); + } + assert.deepStrictEqual( + offenders, + [], + `unguarded DevTools call: ${offenders.join(', ')}`, + ); +}); diff --git a/desktop/src/main/index.js b/desktop/src/main/index.js new file mode 100644 index 0000000..5ca8de8 --- /dev/null +++ b/desktop/src/main/index.js @@ -0,0 +1,162 @@ +'use strict'; + +/** + * DeepSQL desktop client — main process entry point. + * + * Lifecycle: launcher (pick a VM) → transport comes up (TLS or SSH tunnel) → + * workspace window opens on the resulting origin. Quitting tears every tunnel + * down; a leaked loopback listener would be a live hole into the VM. + */ + +const { app, BrowserWindow, shell } = require('electron'); + +const { PROTOCOL, IS_DEV, DEVTOOLS_ENABLED } = require('./config'); +const ipc = require('./ipc'); +const menu = require('./menu'); +const updater = require('./updater'); +const launcher = require('./windows/launcher'); +const workspaces = require('./windows/workspace'); +const transport = require('./transport'); +const log = require('./logger'); + +/** + * Chromium's debugging switches open a DevTools *protocol* endpoint, which is a + * separate door from the DevTools UI that `devTools: false` closes: a packaged + * app started with --remote-debugging-port will happily accept an external + * inspector on the workspace view and its authenticated session. Chromium parses + * these before any of our code runs, so the port may already be listening by the + * time we get here — refusing to start is what closes it again, immediately and + * before any window opens or any tunnel comes up. + * + * ELECTRON_RUN_AS_NODE is out of reach by construction: it turns the binary into + * a plain Node process that never loads this file. + */ +const DEBUG_SWITCH = + /^--(remote-debugging-port|remote-debugging-pipe|remote-allow-origins|inspect|inspect-brk|inspect-port)(=|$)/; + +const debugSwitches = process.argv.slice(1).filter((arg) => DEBUG_SWITCH.test(arg)); + +if (debugSwitches.length > 0 && !DEVTOOLS_ENABLED) { + log.error('app', 'refusing to start with remote debugging enabled', { + switches: debugSwitches, + }); + app.exit(1); +} +// Two instances would fight over the same profiles.json and could bind the same +// sticky tunnel port; the second one just focuses the first. +else if (!app.requestSingleInstanceLock()) { + app.quit(); +} else { + app.on('second-instance', (_event, argv) => { + const existing = launcher.get() || workspaces.all()[0]?.window; + if (existing) { + if (existing.isMinimized?.()) existing.restore(); + existing.focus(); + } else { + launcher.create(); + } + handleDeepLink(argv.find((arg) => arg.startsWith(`${PROTOCOL}://`))); + }); + + bootstrap(); +} + +function bootstrap() { + app.setName('DeepSQL'); + + if (process.defaultApp) { + // Dev: register the scheme with the electron binary + script path. + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [process.argv[1]]); + } + } else { + app.setAsDefaultProtocolClient(PROTOCOL); + } + + app.on('open-url', (event, url) => { + event.preventDefault(); + handleDeepLink(url); + }); + + app.whenReady().then(() => { + log.info('app', 'starting', { + version: app.getVersion(), + electron: process.versions.electron, + platform: `${process.platform}-${process.arch}`, + dev: IS_DEV, + }); + + ipc.register(); + menu.build(); + launcher.create(); + updater.init(); + handleDeepLinkFromArgv(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0 && workspaces.all().length === 0) { + launcher.create(); + } + }); + }); + + app.on('window-all-closed', async () => { + await transport.disconnectAll(); + if (process.platform !== 'darwin') app.quit(); + }); + + app.on('before-quit', async (event) => { + if (transport.active.size === 0) return; + event.preventDefault(); + await transport.disconnectAll(); + app.quit(); + }); + + // Defence in depth: even if a view slipped past its own navigation guard, + // nothing may open a Node-enabled window or reach a non-http scheme. + app.on('web-contents-created', (_event, contents) => { + contents.on('will-attach-webview', (event) => event.preventDefault()); + contents.setWindowOpenHandler(({ url }) => { + if (/^https?:/i.test(url)) shell.openExternal(url); + return { action: 'deny' }; + }); + }); +} + +/** + * `deepsql://connect?url=https://vm.example.com&name=Prod` opens the launcher + * with a new profile pre-filled — the one-click "connect to our VM" link an + * admin can paste into onboarding docs. + */ +function handleDeepLink(rawUrl) { + if (!rawUrl) return; + let parsed; + try { + parsed = new URL(rawUrl); + } catch { + return; + } + if (parsed.protocol !== `${PROTOCOL}:`) return; + + const window = launcher.create(); + const payload = { + action: parsed.hostname || 'connect', + url: parsed.searchParams.get('url') || '', + name: parsed.searchParams.get('name') || '', + transport: parsed.searchParams.get('transport') === 'tunnel' ? 'tunnel' : 'direct', + sshHost: parsed.searchParams.get('sshHost') || '', + sshUser: parsed.searchParams.get('sshUser') || '', + }; + const send = () => window.webContents.send('deeplink', payload); + if (window.webContents.isLoading()) { + window.webContents.once('did-finish-load', send); + } else { + send(); + } + log.info('app', 'handled deep link', payload); +} + +/** Windows/Linux cold start with a protocol argument. */ +function handleDeepLinkFromArgv() { + const arg = process.argv.find((value) => value.startsWith(`${PROTOCOL}://`)); + if (arg) handleDeepLink(arg); +} diff --git a/desktop/src/main/ipc.js b/desktop/src/main/ipc.js new file mode 100644 index 0000000..eb1581a --- /dev/null +++ b/desktop/src/main/ipc.js @@ -0,0 +1,224 @@ +'use strict'; + +/** Every renderer→main entry point in the app lives here. */ + +const { ipcMain, dialog, shell, app } = require('electron'); + +const profilesStore = require('./profiles'); +const secrets = require('./secrets'); +const transport = require('./transport'); +const launcher = require('./windows/launcher'); +const workspaces = require('./windows/workspace'); +const log = require('./logger'); +const { DEVTOOLS_ENABLED } = require('./config'); + +function register() { + // ── App ──────────────────────────────────────────────────────────────── + ipcMain.handle('app:info', () => ({ + // Not app.getVersion(): under `electron + + diff --git a/desktop/src/renderer/launcher/index.html b/desktop/src/renderer/launcher/index.html new file mode 100644 index 0000000..3b7b923 --- /dev/null +++ b/desktop/src/renderer/launcher/index.html @@ -0,0 +1,252 @@ + + + + + + DeepSQL + + + + +
+ + + + +
+
+ +
+ +

Connect to your DeepSQL server

+

+ DeepSQL runs on your own VM or hardware. Point this client at it over TLS, or + tunnel in over SSH when the ports are closed to the internet. +

+ +
+ + +
+
+ + + + diff --git a/desktop/src/renderer/launcher/launcher.css b/desktop/src/renderer/launcher/launcher.css new file mode 100644 index 0000000..ede7aaa --- /dev/null +++ b/desktop/src/renderer/launcher/launcher.css @@ -0,0 +1,532 @@ +/* Launcher — the connection manager. Mirrors AppSidebar.module.css proportions + from the web app (220px rail, 1px #e5e7eb divider, 8px nav radii). */ + +.shell { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* Draggable strip so a frameless/hidden-inset window can still be moved. */ +.drag-strip { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 38px; + -webkit-app-region: drag; + pointer-events: auto; +} + +.drag-strip * { + -webkit-app-region: no-drag; +} + +/* ── Sidebar ──────────────────────────────────────────────────────────── */ +.sidebar { + position: relative; + width: 248px; + min-width: 248px; + display: flex; + flex-direction: column; + background: var(--color-white); + border-right: 1px solid var(--color-border); +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + padding: 46px 16px 12px; + flex-shrink: 0; +} + +.brand-mark { + width: 28px; + height: 28px; + background: var(--color-ink); + border-radius: 7px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.brand-text { + font-size: 15px; + font-weight: 600; + color: var(--color-ink); + letter-spacing: -0.01em; +} + +.rail-label { + padding: 10px 18px 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-light-5); +} + +.profile-list { + flex: 1; + overflow-y: auto; + padding: 0 8px 8px; + display: flex; + flex-direction: column; + gap: 1px; +} + +.profile-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 9px 10px; + border-radius: 8px; + text-align: left; + color: var(--color-light-6); + transition: background 0.1s, color 0.1s; +} + +.profile-item:hover { + background: var(--color-light-2); + color: var(--color-text); +} + +.profile-item.is-active { + background: var(--color-light-2); + color: var(--color-text); + font-weight: 500; +} + +.profile-dot { + width: 7px; + height: 7px; + border-radius: 999px; + background: var(--color-light-4); + flex-shrink: 0; +} + +.profile-dot.is-live { + background: var(--color-success); + box-shadow: 0 0 0 3px var(--color-success-soft); +} + +.profile-copy { + min-width: 0; + display: flex; + flex-direction: column; + line-height: 1.3; +} + +.profile-name { + font-size: 13.5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.profile-sub { + font-size: 11.5px; + color: var(--color-light-5); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-footer { + border-top: 1px solid var(--color-border); + padding: 10px 12px 12px; + flex-shrink: 0; +} + +.new-connection { + width: 100%; + justify-content: flex-start; + gap: 8px; + padding: 8px 10px; + color: var(--color-light-8); + font-size: 13.5px; + font-weight: 500; +} + +.app-meta { + margin-top: 8px; + padding: 0 4px; + font-size: 11px; + line-height: 1.5; + color: var(--color-light-5); +} + +.app-meta button { + padding: 0; + font-size: 11px; + color: var(--color-light-6); + text-decoration: underline; + background: none; +} + +.app-meta button:hover { + background: none; + color: var(--color-text); +} + +/* ── Detail pane ──────────────────────────────────────────────────────── */ +.pane { + position: relative; + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.pane-drag { + height: 30px; +} + +.empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 14px; + padding: 0 64px; +} + +.empty-mark { + width: 56px; + height: 56px; + border-radius: var(--radius-lg); + background: var(--color-white); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-card); + display: flex; + align-items: center; + justify-content: center; +} + +.empty-state h1 { + margin: 0; + font-size: 20px; + font-weight: 600; + letter-spacing: -0.02em; +} + +.empty-state p { + margin: 0; + max-width: 440px; + color: var(--color-text-secondary); +} + +.empty-state .btn-primary { + margin-top: 8px; +} + +/* ── Editor ───────────────────────────────────────────────────────────── */ +.editor { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.editor-head { + padding: 34px 32px 14px; + flex-shrink: 0; +} + +.editor-title-row { + display: flex; + align-items: center; + gap: 12px; +} + +.title-input { + flex: 1; + min-width: 0; + font-size: 21px; + font-weight: 600; + letter-spacing: -0.02em; + padding: 4px 8px; + margin-left: -8px; + border: 1px solid transparent; + background: transparent; + border-radius: var(--radius-sm); +} + +.title-input:hover { + border-color: var(--color-light-3); +} + +.title-input:focus { + background: var(--color-white); + border-color: var(--color-light-4); + box-shadow: none; +} + +.editor-subtitle { + margin: 4px 0 0; + font-size: 12.5px; + color: var(--color-light-5); +} + +.pill { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: var(--radius-button); + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} + +.pill-idle { + background: var(--color-light-2); + color: var(--color-light-6); +} + +.pill-busy { + background: var(--color-warning-soft); + color: #92400e; +} + +.pill-live { + background: var(--color-success-soft); + color: #15803d; +} + +.pill-error { + background: var(--color-danger-soft); + color: var(--color-danger-text); +} + +.editor-body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 6px 32px 24px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.card { + background: var(--color-white); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.card-head h2 { + margin: 0; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--color-light-6); +} + +.card-head-inner { + margin-top: 4px; + padding-top: 16px; + border-top: 1px solid var(--color-border); +} + +.segmented { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.seg { + flex-direction: column; + align-items: flex-start; + gap: 3px; + padding: 13px 15px; + border: 1px solid var(--color-light-3); + border-radius: var(--radius-md); + background: var(--color-white); + text-align: left; + transition: border-color 0.12s, background 0.12s; +} + +.seg strong { + font-size: 13.5px; + font-weight: 600; + color: var(--color-text); +} + +.seg span { + font-size: 12px; + font-weight: 400; + color: var(--color-light-5); + line-height: 1.45; +} + +.seg:hover { + background: var(--color-white); + border-color: var(--color-light-4); +} + +.seg.is-selected { + border-color: var(--color-ink); + background: #fafafa; +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.field-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--color-light-5); +} + +.field-label em { + font-style: normal; + font-weight: 400; + text-transform: none; + letter-spacing: 0; + color: var(--color-light-4); +} + +.field-hint { + font-size: 12px; + line-height: 1.5; + color: var(--color-light-5); +} + +.field-hint code, +.notice code, +code.mono { + font-family: var(--font-family-mono); + font-size: 11.5px; + background: var(--color-light-2); + padding: 1px 5px; + border-radius: 4px; + user-select: text; +} + +.grid-3 { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 12px; +} + +.file-row { + display: flex; + gap: 8px; +} + +.file-row input { + flex: 1; + min-width: 0; +} + +.notice { + font-size: 12.5px; + line-height: 1.55; + padding: 11px 14px; + border-radius: var(--radius-md); +} + +.notice-danger { + background: var(--color-danger-soft); + border: 1px solid var(--color-danger-border); + color: var(--color-danger-text); +} + +.notice-muted { + background: var(--color-light-2); + color: var(--color-light-6); +} + +.host-key { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 11px 14px; + background: var(--color-light-2); + border-radius: var(--radius-md); +} + +.host-key code { + display: block; + margin-top: 2px; + background: transparent; + padding: 0; + color: var(--color-light-8); + word-break: break-all; +} + +/* ── Footer ───────────────────────────────────────────────────────────── */ +.editor-foot { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 16px; + padding: 14px 32px; + background: var(--color-white); + border-top: 1px solid var(--color-border); +} + +.foot-left { + flex-shrink: 0; +} + +.foot-right { + display: flex; + gap: 10px; + flex-shrink: 0; +} + +.feedback { + flex: 1; + min-width: 0; + font-size: 12.5px; + line-height: 1.45; + color: var(--color-light-6); + user-select: text; +} + +.feedback.is-error { + color: var(--color-danger-text); +} + +.feedback.is-ok { + color: var(--color-success); +} + +.spinner { + display: inline-block; + width: 11px; + height: 11px; + margin-right: 7px; + vertical-align: -1px; + border: 1.5px solid currentColor; + border-top-color: transparent; + border-radius: 999px; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/desktop/src/renderer/launcher/launcher.js b/desktop/src/renderer/launcher/launcher.js new file mode 100644 index 0000000..755b4d6 --- /dev/null +++ b/desktop/src/renderer/launcher/launcher.js @@ -0,0 +1,531 @@ +'use strict'; + +/* Launcher renderer. No framework on purpose: this is three screens of chrome + around an IPC surface, and a build step here would be a second toolchain to + keep alive for no gain. The DeepSQL app itself is React — it is loaded from + the server, not reimplemented. */ + +const api = window.deepsql; + +const el = (id) => document.getElementById(id); + +const dom = { + list: el('profile-list'), + meta: el('app-meta'), + empty: el('empty-state'), + editor: el('editor'), + subtitle: el('editor-subtitle'), + statusPill: el('status-pill'), + feedback: el('feedback'), + hostKeyRow: el('host-key-row'), + hostKeyValue: el('host-key-value'), + secretHint: el('secret-hint'), + fields: { + name: el('f-name'), + url: el('f-url'), + tlsMode: el('f-tls-mode'), + tlsFingerprint: el('f-tls-fingerprint'), + tlsCa: el('f-tls-ca'), + sshHost: el('f-ssh-host'), + sshPort: el('f-ssh-port'), + sshUser: el('f-ssh-user'), + sshAuth: el('f-ssh-auth'), + sshKey: el('f-ssh-key'), + sshPassphrase: el('f-ssh-passphrase'), + sshPassword: el('f-ssh-password'), + remoteHost: el('f-remote-host'), + remotePort: el('f-remote-port'), + localPort: el('f-local-port'), + }, +}; + +const state = { + profiles: [], + selectedId: null, + /** Unsaved new profile, held here until the first save. */ + draft: null, + transport: 'direct', + active: new Set(), + busy: false, + info: null, +}; + +// ── Rendering ──────────────────────────────────────────────────────────── + +function selected() { + if (state.draft) return state.draft; + return state.profiles.find((p) => p.id === state.selectedId) || null; +} + +function renderList() { + dom.list.replaceChildren(); + + for (const profile of state.profiles) { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'profile-item'; + if (!state.draft && profile.id === state.selectedId) item.classList.add('is-active'); + + const dot = document.createElement('span'); + dot.className = `profile-dot${state.active.has(profile.id) ? ' is-live' : ''}`; + + const copy = document.createElement('span'); + copy.className = 'profile-copy'; + const name = document.createElement('span'); + name.className = 'profile-name'; + name.textContent = profile.name; + const sub = document.createElement('span'); + sub.className = 'profile-sub'; + sub.textContent = describe(profile); + copy.append(name, sub); + + item.append(dot, copy); + item.addEventListener('click', () => select(profile.id)); + dom.list.append(item); + } + + if (state.draft) { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'profile-item is-active'; + const dot = document.createElement('span'); + dot.className = 'profile-dot'; + const copy = document.createElement('span'); + copy.className = 'profile-copy'; + const name = document.createElement('span'); + name.className = 'profile-name'; + name.textContent = state.draft.name || 'New connection'; + const sub = document.createElement('span'); + sub.className = 'profile-sub'; + sub.textContent = 'Unsaved'; + copy.append(name, sub); + item.append(dot, copy); + dom.list.append(item); + } +} + +function describe(profile) { + if (profile.transport === 'tunnel') { + const { username, host, remotePort } = profile.ssh; + return host ? `ssh ${username}@${host} → :${remotePort}` : 'SSH tunnel'; + } + try { + return new URL(profile.url).host; + } catch { + return 'No URL set'; + } +} + +function renderEditor() { + const profile = selected(); + const hasProfile = Boolean(profile); + dom.empty.hidden = hasProfile; + dom.editor.hidden = !hasProfile; + if (!profile) return; + + state.transport = profile.transport; + + dom.fields.name.value = profile.name || ''; + dom.fields.url.value = profile.url || ''; + dom.fields.tlsMode.value = profile.tls?.mode || 'system'; + dom.fields.tlsFingerprint.value = profile.tls?.fingerprint || ''; + dom.fields.tlsCa.value = profile.tls?.caPath || ''; + + const ssh = profile.ssh || {}; + dom.fields.sshHost.value = ssh.host || ''; + dom.fields.sshPort.value = ssh.port ?? 22; + dom.fields.sshUser.value = ssh.username || ''; + dom.fields.sshAuth.value = ssh.authMethod || 'key'; + dom.fields.sshKey.value = ssh.privateKeyPath || ''; + dom.fields.sshPassphrase.value = ''; + dom.fields.sshPassword.value = ''; + dom.fields.remoteHost.value = ssh.remoteHost || '127.0.0.1'; + dom.fields.remotePort.value = ssh.remotePort ?? 3000; + dom.fields.localPort.value = ssh.localPort ?? 0; + + dom.hostKeyRow.hidden = !ssh.hostKeyFingerprint; + dom.hostKeyValue.textContent = ssh.hostKeyFingerprint || ''; + + dom.secretHint.textContent = ssh.hasStoredPassphrase + ? state.info?.keychainAvailable + ? 'A passphrase is saved in your OS keychain. Type here only to replace it.' + : 'A passphrase is held for this session only — the OS keychain is unavailable.' + : ''; + + dom.subtitle.textContent = profile.lastConnectedAt + ? `Last connected ${formatWhen(profile.lastConnectedAt)}` + : state.draft + ? 'Not saved yet' + : 'Never connected'; + + el('delete-profile').hidden = Boolean(state.draft); + + syncVisibility(); + setStatus( + state.active.has(profile.id) ? 'live' : 'idle', + state.active.has(profile.id) ? 'Connected' : 'Not connected', + ); +} + +/** Show only the fields the current transport / TLS mode / auth method needs. */ +function syncVisibility() { + const transport = state.transport; + for (const seg of document.querySelectorAll('.seg')) { + seg.classList.toggle('is-selected', seg.dataset.transport === transport); + } + for (const panel of document.querySelectorAll('[data-panel]')) { + panel.hidden = panel.dataset.panel !== transport; + } + const tlsMode = dom.fields.tlsMode.value; + for (const node of document.querySelectorAll('[data-tls]')) { + node.hidden = node.dataset.tls !== tlsMode; + } + const auth = dom.fields.sshAuth.value; + for (const node of document.querySelectorAll('[data-auth]')) { + node.hidden = node.dataset.auth !== auth; + } +} + +function setStatus(kind, text) { + dom.statusPill.className = `pill pill-${kind}`; + dom.statusPill.textContent = text; +} + +function say(message, kind = '') { + dom.feedback.className = `feedback${kind ? ` is-${kind}` : ''}`; + dom.feedback.replaceChildren(); + if (kind === 'busy') { + const spinner = document.createElement('span'); + spinner.className = 'spinner'; + dom.feedback.append(spinner); + } + dom.feedback.append(document.createTextNode(message)); +} + +function formatWhen(iso) { + const then = new Date(iso); + const minutes = Math.round((Date.now() - then.getTime()) / 60000); + if (minutes < 1) return 'just now'; + if (minutes < 60) return `${minutes}m ago`; + if (minutes < 60 * 24) return `${Math.round(minutes / 60)}h ago`; + return then.toLocaleDateString(); +} + +function setBusy(busy) { + state.busy = busy; + el('connect').disabled = busy; + el('test-connection').disabled = busy; +} + +// ── Form → payload ─────────────────────────────────────────────────────── + +function readForm() { + const profile = selected(); + const payload = { + id: state.draft ? undefined : profile?.id, + name: dom.fields.name.value, + transport: state.transport, + url: dom.fields.url.value, + tls: { + mode: dom.fields.tlsMode.value, + fingerprint: dom.fields.tlsFingerprint.value, + caPath: dom.fields.tlsCa.value, + }, + ssh: { + host: dom.fields.sshHost.value, + port: dom.fields.sshPort.value, + username: dom.fields.sshUser.value, + authMethod: dom.fields.sshAuth.value, + privateKeyPath: dom.fields.sshKey.value, + remoteHost: dom.fields.remoteHost.value, + remotePort: dom.fields.remotePort.value, + localPort: dom.fields.localPort.value, + }, + }; + + // An empty secret box means "leave what is stored alone", not "clear it" — + // otherwise every save would wipe the keychain entry the user just made. + if (dom.fields.sshPassphrase.value) payload.ssh.passphrase = dom.fields.sshPassphrase.value; + if (dom.fields.sshPassword.value) payload.ssh.password = dom.fields.sshPassword.value; + + return payload; +} + +/** Persist the form and return the saved profile. */ +async function save() { + const saved = await api.profiles.save(readForm()); + state.draft = null; + state.selectedId = saved.id; + state.profiles = await api.profiles.list(); + renderList(); + return saved; +} + +function validate() { + if (state.transport === 'direct') { + if (!dom.fields.url.value.trim()) return 'Enter the DeepSQL server URL.'; + if (dom.fields.tlsMode.value === 'custom-ca' && !dom.fields.tlsCa.value.trim()) { + return 'Select the CA bundle to verify against.'; + } + return null; + } + if (!dom.fields.sshHost.value.trim()) return 'Enter the SSH host.'; + if (!dom.fields.sshUser.value.trim()) return 'Enter the SSH username.'; + if (dom.fields.sshAuth.value === 'key' && !dom.fields.sshKey.value.trim()) { + return 'Select the private key file to authenticate with.'; + } + return null; +} + +// ── Actions ────────────────────────────────────────────────────────────── + +function select(id) { + state.draft = null; + state.selectedId = id; + say(''); + renderList(); + renderEditor(); +} + +function newProfile(prefill = {}) { + state.draft = { + id: null, + name: prefill.name || 'DeepSQL VM', + transport: prefill.transport || 'direct', + url: prefill.url || '', + tls: { mode: 'system', fingerprint: '', caPath: '' }, + ssh: { + host: prefill.sshHost || '', + port: 22, + username: prefill.sshUser || 'ubuntu', + authMethod: 'key', + privateKeyPath: '', + remoteHost: '127.0.0.1', + remotePort: 3000, + localPort: 0, + hostKeyFingerprint: '', + hasStoredPassphrase: false, + hasStoredPassword: false, + }, + lastConnectedAt: null, + }; + state.selectedId = null; + say(''); + renderList(); + renderEditor(); + dom.fields.name.focus(); + dom.fields.name.select(); +} + +async function testConnection() { + const problem = validate(); + if (problem) { + say(problem, 'error'); + return; + } + setBusy(true); + setStatus('busy', 'Testing'); + say('Testing connection…', 'busy'); + try { + const profile = await save(); + const result = await api.profiles.test(profile.id, transientSecrets()); + if (result.ok) { + // `rebuilt` means these settings differed from the live connection, which + // was rebuilt onto them — so the result describes the settings on screen, + // and the open window has already moved with it. + setStatus(result.rebuilt ? 'live' : 'idle', result.rebuilt ? 'Connected' : 'Not connected'); + say( + `DeepSQL responded in ${result.latencyMs} ms${ + result.hostKeyFingerprint ? ` · host key ${result.hostKeyFingerprint}` : '' + }.${result.rebuilt ? ' The open connection was rebuilt with these settings.' : ''}`, + 'ok', + ); + state.profiles = await api.profiles.list(); + renderEditor(); + } else { + setStatus('error', 'Failed'); + // Name the lost session explicitly. The old connection was built from + // settings that have been replaced, so it is closed rather than kept — + // silently dropping it is what would feel unreliable. + say( + result.closedStaleConnection + ? `${result.detail} The previous connection used the settings you replaced, so it was closed.` + : result.detail, + 'error', + ); + } + } catch (err) { + setStatus('error', 'Failed'); + say(err.message, 'error'); + } finally { + setBusy(false); + } +} + +async function connect() { + const problem = validate(); + if (problem) { + say(problem, 'error'); + return; + } + setBusy(true); + setStatus('busy', 'Connecting'); + say('Connecting…', 'busy'); + try { + const profile = await save(); + const result = await api.connection.open(profile.id, transientSecrets()); + if (result.ok) { + state.active.add(profile.id); + setStatus('live', 'Connected'); + say('Connected. Opening DeepSQL…', 'ok'); + renderList(); + } else { + setStatus('error', 'Failed'); + say(result.detail, 'error'); + } + } catch (err) { + setStatus('error', 'Failed'); + say(err.message, 'error'); + } finally { + setBusy(false); + } +} + +/** + * Secrets typed right now are also passed straight to the transport, so a + * passphrase works on this attempt even when the OS keychain refused to store + * it (headless Linux) or the user is deliberately not saving it. + */ +function transientSecrets() { + return { + passphrase: dom.fields.sshPassphrase.value || undefined, + password: dom.fields.sshPassword.value || undefined, + }; +} + +async function deleteProfile() { + const profile = selected(); + if (!profile?.id) return; + await api.profiles.remove(profile.id); + state.active.delete(profile.id); + state.profiles = await api.profiles.list(); + state.selectedId = state.profiles[0]?.id || null; + state.draft = null; + renderList(); + renderEditor(); + say('Connection deleted.'); +} + +// ── Wiring ─────────────────────────────────────────────────────────────── + +function wire() { + el('new-connection').addEventListener('click', () => newProfile()); + el('empty-new').addEventListener('click', () => newProfile()); + el('delete-profile').addEventListener('click', deleteProfile); + el('test-connection').addEventListener('click', testConnection); + + dom.editor.addEventListener('submit', (event) => { + event.preventDefault(); + connect(); + }); + + for (const seg of document.querySelectorAll('.seg')) { + seg.addEventListener('click', () => { + state.transport = seg.dataset.transport; + syncVisibility(); + say(''); + }); + } + + dom.fields.tlsMode.addEventListener('change', syncVisibility); + dom.fields.sshAuth.addEventListener('change', syncVisibility); + + dom.fields.name.addEventListener('input', () => { + if (state.draft) state.draft.name = dom.fields.name.value; + renderList(); + }); + + el('browse-key').addEventListener('click', async () => { + const picked = await api.dialog.pickPrivateKey(); + if (picked) dom.fields.sshKey.value = picked; + }); + + el('browse-ca').addEventListener('click', async () => { + const picked = await api.dialog.pickCaBundle(); + if (picked) dom.fields.tlsCa.value = picked; + }); + + el('clear-host-key').addEventListener('click', async () => { + const profile = selected(); + if (!profile?.id) return; + const result = await api.profiles.clearHostKey(profile.id); + state.profiles = await api.profiles.list(); + renderEditor(); + // Clearing the pin now rebuilds a live connection, because leaving up the + // session that was pinned to the old key would make the clear cosmetic. + if (result?.disconnected) { + say( + `Pinned host key cleared, but reconnecting failed: ${result.detail}`, + 'error', + ); + } else if (result?.reconnected) { + say('Pinned host key cleared and reconnected, pinning the key the VM presented.', 'ok'); + } else { + say('Pinned host key cleared. The next connection will pin whatever the VM presents.'); + } + }); + + api.onStatus((payload) => { + if (state.busy && payload.detail) say(payload.detail, 'busy'); + if (payload.status === 'ready') state.active.add(payload.profileId); + if (payload.status === 'closed') state.active.delete(payload.profileId); + renderList(); + }); + + api.onWorkspaceClosed(({ profileId }) => { + state.active.delete(profileId); + renderList(); + if (selected()?.id === profileId) setStatus('idle', 'Not connected'); + }); + + api.onDeepLink((payload) => { + newProfile(payload); + say('Pre-filled from a deepsql:// link. Review it, then connect.'); + }); +} + +async function renderMeta() { + const info = state.info; + dom.meta.replaceChildren(); + + const version = document.createElement('div'); + version.textContent = `DeepSQL Desktop ${info.version} · Electron ${info.electron}`; + + const keychain = document.createElement('div'); + keychain.textContent = info.keychainAvailable + ? 'Secrets stored in the OS keychain' + : 'No OS keychain — secrets kept for this session only'; + + const logs = document.createElement('button'); + logs.type = 'button'; + logs.textContent = 'Open log file'; + logs.addEventListener('click', () => api.app.openLog()); + + dom.meta.append(version, keychain, logs); +} + +async function boot() { + state.info = await api.app.info(); + state.profiles = await api.profiles.list(); + state.active = new Set(await api.connection.activeIds()); + state.selectedId = state.profiles[0]?.id || null; + + wire(); + await renderMeta(); + renderList(); + renderEditor(); + + if (state.profiles.length === 0) newProfile(); +} + +boot(); diff --git a/desktop/src/renderer/shared/theme.css b/desktop/src/renderer/shared/theme.css new file mode 100644 index 0000000..099a9d5 --- /dev/null +++ b/desktop/src/renderer/shared/theme.css @@ -0,0 +1,206 @@ +/* + * DeepSQL design tokens, lifted from src/index.css in the web app so the native + * shell and the embedded UI read as one product. Keep these in sync: if the web + * app's palette moves, this file moves with it. + * + * The webfont is loaded from Google Fonts exactly as the web app does. When the + * machine is offline the stack falls through to the system UI font, which is + * why the fallbacks are spelled out rather than left to `sans-serif`. + */ +@import url("https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400;500;600;700;800&display=swap"); + +:root { + --font-family-sans: "Maven Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + --font-family-mono: "JetBrains Mono", "SF Mono", "Fira Code", "IBM Plex Mono", Menlo, + Consolas, monospace; + + --color-white: #ffffff; + --color-canvas: #f5f5f7; + --color-light-2: #f3f4f6; + --color-light-3: #e5e7eb; + --color-light-4: #d1d5db; + --color-light-5: #9ca3af; + --color-light-6: #6b7280; + --color-light-8: #374151; + --color-text: #111827; + --color-text-secondary: #6b7280; + --color-ink: #0d0d0d; + --color-primary: #000000; + --color-primary-dark: #333333; + --color-border: #e5e7eb; + + --color-success: #16a34a; + --color-success-soft: #dcfce7; + --color-warning: #f59e0b; + --color-warning-soft: #fef3c7; + --color-danger: #ef4444; + --color-danger-dark: #dc2626; + --color-danger-soft: #fef2f2; + --color-danger-border: #fecaca; + --color-danger-text: #b91c1c; + + --radius-sm: 0.5rem; + --radius-md: 0.75rem; + --radius-lg: 1rem; + --radius-button: 9999px; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-card: 0 4px 24px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* The UA rule for [hidden] is `display: none`, which any later `display: flex` + on the same element silently beats. Both renderers toggle visibility with the + hidden attribute, so it has to win outright. */ +[hidden] { + display: none !important; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + background: var(--color-canvas); + color: var(--color-text); + font-family: var(--font-family-sans); + font-size: 14px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; + user-select: none; + cursor: default; +} + +button { + font-family: inherit; + font-size: 14px; + line-height: 1.4; + border: none; + background: transparent; + color: var(--color-light-8); + border-radius: 8px; + padding: 6px 12px; + font-weight: 500; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + letter-spacing: -0.01em; + transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease; +} + +button:hover:not(:disabled) { + background: var(--color-light-2); + color: var(--color-text); +} + +button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +button:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid var(--color-light-4); + outline-offset: 1px; +} + +input, +select { + font-family: inherit; + font-size: 14px; + width: 100%; + padding: 9px 12px; + color: var(--color-text); + background: var(--color-white); + border: 1px solid var(--color-light-4); + border-radius: var(--radius-md); + transition: border-color 0.15s ease, box-shadow 0.15s ease; + user-select: text; +} + +input::placeholder { + color: var(--color-light-5); +} + +input:focus, +select:focus { + outline: none; + border-color: var(--color-light-6); + box-shadow: 0 0 0 3px rgba(209, 213, 219, 0.45); +} + +.btn-primary { + background: var(--color-primary); + color: var(--color-white); + border-radius: var(--radius-button); + padding: 10px 22px; + font-weight: 600; +} + +.btn-primary:hover:not(:disabled) { + background: var(--color-primary-dark); + color: var(--color-white); +} + +.btn-secondary { + background: var(--color-white); + border: 1px solid var(--color-light-4); + border-radius: var(--radius-button); + padding: 9px 18px; + color: var(--color-light-8); + font-weight: 500; +} + +.btn-secondary:hover:not(:disabled) { + background: var(--color-light-2); +} + +.btn-danger { + color: var(--color-danger-dark); +} + +.btn-danger:hover:not(:disabled) { + background: var(--color-danger-soft); + color: var(--color-danger-text); +} + +.mono { + font-family: var(--font-family-mono); + font-size: 12px; + letter-spacing: -0.01em; +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-thumb { + background: var(--color-light-4); + border: 3px solid transparent; + background-clip: content-box; + border-radius: 999px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-light-5); + background-clip: content-box; +} + +::-webkit-scrollbar-track { + background: transparent; +} diff --git a/docker-compose.yml b/docker-compose.yml index c2bb83f..3572230 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,8 +94,12 @@ services: ENCRYPTION_KEYS: ${ENCRYPTION_KEYS:-} ENCRYPTION_KEY_ID: ${ENCRYPTION_KEY_ID:-} - # CORS — allow the frontend service (and optional custom domain) - cors.allowed.origins: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000} + # CORS — allow the frontend service (and optional custom domain). + # Keep the loopback patterns when you set CORS_ALLOWED_ORIGINS: this + # variable REPLACES the list, and the desktop client's SSH tunnel serves + # the app from http://127.0.0.1:. Dropping them makes every + # tunnel login fail with a bare 403 "Invalid CORS request". + cors.allowed.origins: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000,http://127.0.0.1:*,http://localhost:*} # DeepSQL Agent — compose-network URLs (Agent tab, dashboards, Slack/CLI) AGENT_WEBUI_URL: ${AGENT_WEBUI_URL:-http://deepsql-agent:8787} diff --git a/docs/root/SELF_HOST_GUIDE.md b/docs/root/SELF_HOST_GUIDE.md index bd93b9a..ca95d64 100644 --- a/docs/root/SELF_HOST_GUIDE.md +++ b/docs/root/SELF_HOST_GUIDE.md @@ -426,17 +426,22 @@ serve from. | `DEEPSQL_BACKEND_PORT` | 8080 | [`docker-compose.yml:94`](../../docker-compose.yml) | | `DEEPSQL_POSTGRES_PORT` | 5432 (published as `127.0.0.1:…` only) | [`docker-compose.yml`](../../docker-compose.yml) | | `DEEPSQL_VALKEY_PORT` | 6379 (published as `127.0.0.1:…` only) | [`docker-compose.yml`](../../docker-compose.yml) | -| `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | `application.properties:87` → [`SecurityConfig.java`](../../backend/src/main/java/com/dbaagent/config/SecurityConfig.java) | +| `CORS_ALLOWED_ORIGINS` | loopback hosts + port wildcards (see `.env.example`) | `application.properties` → [`SecurityConfig.java`](../../backend/src/main/java/com/dbaagent/config/SecurityConfig.java) | ```env DEEPSQL_FRONTEND_PORT=13000 -CORS_ALLOWED_ORIGINS=http://localhost:13000 +# Port wildcards keep DeepSQL Desktop SSH-tunnel origins working (sticky local port). +CORS_ALLOWED_ORIGINS=http://localhost:13000,http://127.0.0.1:*,http://localhost:* ``` -Behind a reverse proxy, `CORS_ALLOWED_ORIGINS` must list the **public** origin -(`https://deepsql.your-company.example`), not the container port. Consider binding -postgres and valkey to `127.0.0.1` on a public host, or dropping their `ports:` entries -entirely — nothing outside the Compose network needs them. +`CORS_ALLOWED_ORIGINS` **replaces** the built-in allowlist rather than extending it. +Behind a reverse proxy, list the **public** origin +(`https://deepsql.your-company.example`) **and** keep +`http://127.0.0.1:*,http://localhost:*` if anyone will use [DeepSQL Desktop](../../desktop/README.md) +(SSH tunnel → `http://127.0.0.1:`). A public-origin-only value makes tunnel +login fail with `403 Invalid CORS request` while GETs still look healthy. Consider +binding postgres and valkey to `127.0.0.1` on a public host, or dropping their +`ports:` entries entirely — nothing outside the Compose network needs them. ### Optional integrations @@ -482,7 +487,8 @@ Once TLS is in place, three settings must follow or authentication behaves oddly ```env SECURITY_COOKIE_SECURE=true -CORS_ALLOWED_ORIGINS=https://deepsql.your-company.example +# Public origin for browsers + loopback wildcards for DeepSQL Desktop tunnels. +CORS_ALLOWED_ORIGINS=https://deepsql.your-company.example,http://127.0.0.1:*,http://localhost:* APP_BASE_URL=https://deepsql.your-company.example APP_PUBLIC_URL=https://deepsql.your-company.example ``` @@ -803,7 +809,7 @@ Agent-specific (in addition to the core stack): - [ ] `ENCRYPTION_KEY` backed up somewhere other than where the database dumps live - [ ] TLS terminated in front of the frontend, with `SECURITY_COOKIE_SECURE=true` - [ ] `APP_BASE_URL` and `APP_PUBLIC_URL` set to the public URL, and the startup log line checked -- [ ] `CORS_ALLOWED_ORIGINS` set to the public origin only +- [ ] `CORS_ALLOWED_ORIGINS` includes the public origin **and** `http://127.0.0.1:*,http://localhost:*` (Desktop tunnel / local loopback) - [ ] `EMBEDDING_FAIL_OPEN=false` (the `prod` default) so retrieval failures surface - [ ] Postgres and valkey not published to a public interface - [ ] `.env` never committed — it is gitignored, keep it that way @@ -816,6 +822,7 @@ Agent-specific (in addition to the core stack): - [`README.md`](../../README.md) — overview, quick start, LLM configuration, development - [`.env.example`](../../.env.example) — inline documentation for every shipped variable +- [`desktop/README.md`](../../desktop/README.md) — DeepSQL Desktop (Electron) install, SSH tunnel, and CORS requirements - [`mcp/README.md`](../../mcp/README.md) — the `deepsql` CLI and MCP server, including client configuration for Claude Desktop and Codex - [`docs/root/MCP_PHASE1.md`](./MCP_PHASE1.md) — MCP tools and environment variables - [`docs/LOGGING-GUIDE.md`](../LOGGING-GUIDE.md) — log format