diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 000000000..82b1f6bb2 --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,178 @@ +name: Desktop validation + +on: + pull_request: + paths: ['desktop/**', 'src/powercontext/**', 'pyproject.toml', 'uv.lock', 'openapi/powercontext.yaml', 'tests/fixtures/transport_loopback_vectors.json', 'website/assets/**', '.github/workflows/desktop.yml'] + push: + branches: [master, codex/desktop-preview] + paths: ['desktop/**', 'src/powercontext/**', 'pyproject.toml', 'uv.lock', 'openapi/powercontext.yaml', 'tests/fixtures/transport_loopback_vectors.json', 'website/assets/**', '.github/workflows/desktop.yml'] + workflow_dispatch: + inputs: + installer_run: + description: 'Optional run ID from this repository to diagnose its exact existing installer' + required: false + type: string + default: '' + +permissions: + contents: read + +concurrency: + group: desktop-${{ github.event.pull_request.number || github.ref }}-${{ inputs.installer_run || 'build' }} + cancel-in-progress: true + +jobs: + windows: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.installer_run == '' }} + runs-on: windows-2025 + timeout-minutes: 40 + defaults: + run: + working-directory: desktop + shell: pwsh + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + fetch-depth: 0 + - name: Verify Server version baseline tag + run: | + git fetch --no-tags https://github.com/oceanbase/powercontext.git refs/tags/powercontext-v1.0.0:refs/tags/powercontext-v1.0.0 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ((git rev-parse refs/tags/powercontext-v1.0.0) -ne 'ec97efba5f26fc972e9d8bf5027d8233acab3ca1') { + throw 'Unexpected Server version baseline tag' + } + - uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2 + with: + working_directory: desktop + - name: Install pinned Windows Rust toolchain + run: rustup toolchain install 1.95.0-x86_64-pc-windows-msvc --profile minimal --component rustfmt --component clippy + - name: Install locked dependencies + run: pnpm install --frozen-lockfile + - run: pnpm lint + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build + - run: cargo fmt --manifest-path src-tauri/Cargo.toml --check + - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings + - run: cargo test --locked --manifest-path src-tauri/Cargo.toml + - run: pnpm ipc:check + - name: Verify diagnostic process containment + run: cargo run --locked --manifest-path src-tauri/Cargo.toml --example diagnostic_process_probe + - name: Verify Windows vault across processes + run: cargo run --locked --manifest-path src-tauri/Cargo.toml --example credential_probe + - name: Set up Python fixture tooling + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + - name: Install locked Server test dependencies + working-directory: . + run: uv sync --locked --python 3.12 + - name: Build Server wheel for isolated acceptance + working-directory: . + run: uv build --wheel --out-dir desktop/.artifacts/server-wheel + - name: Check installed UI acceptance harness + working-directory: . + run: | + uv run --no-sync ruff check --no-fix desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + uv run --no-sync ruff format --check desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + uv run --no-sync ty check desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + uv run --no-sync ty check --python-platform linux desktop/tests/installed_ui.py desktop/tests/installed_fixture.py desktop/tests/installed_workflow.py + - name: Build native API acceptance client + run: cargo build --locked --manifest-path src-tauri/Cargo.toml --example server_probe --example diagnostic_cli_probe + - name: Test native adapter with real SQLite Server + working-directory: . + run: uv run --no-sync python desktop/tests/real_server.py + - name: Test native diagnostics with isolated real CLI + working-directory: . + run: uv run --no-sync python desktop/tests/real_cli.py + - name: Build unsigned internal Windows installer + run: pnpm desktop:build + - name: Prepare matching WebView2 driver + run: ./scripts/prepare-webdriver.ps1 + - name: Smoke test installed package + timeout-minutes: 8 + run: ./scripts/windows-smoke.ps1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: desktop-windows-internal-unsigned + include-hidden-files: true + path: | + desktop/src-tauri/target/release/bundle/nsis/*-setup.exe + desktop/.artifacts/windows-smoke.json + desktop/.artifacts/real-server.json + desktop/.artifacts/real-cli.json + desktop/.artifacts/installed-ui.json + desktop/.artifacts/installed-ui.png + desktop/.artifacts/installed-ui-driver.log + desktop/.artifacts/installed-ui-app.log + desktop/.artifacts/installed-ui-processes.json + desktop/.artifacts/installed-ui-window.png + if-no-files-found: error + + installed-package: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.installer_run != '' }} + runs-on: windows-2025 + timeout-minutes: 15 + permissions: + contents: read + actions: read + defaults: + run: + shell: pwsh + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + fetch-depth: 0 + - name: Verify Server version baseline tag + run: | + git fetch --no-tags https://github.com/oceanbase/powercontext.git refs/tags/powercontext-v1.0.0:refs/tags/powercontext-v1.0.0 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ((git rev-parse refs/tags/powercontext-v1.0.0) -ne 'ec97efba5f26fc972e9d8bf5027d8233acab3ca1') { throw 'Unexpected Server version baseline tag' } + - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + - name: Prepare isolated Server fixture + run: | + uv sync --locked --python 3.12 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + uv build --wheel --out-dir desktop/.artifacts/server-wheel + - name: Retrieve and verify exact installer + env: + GH_TOKEN: ${{ github.token }} + INSTALLER_RUN: ${{ inputs.installer_run }} + run: | + if ($env:INSTALLER_RUN -notmatch '^\d+$') { throw 'Expected numeric workflow run ID.' } + $run = gh api "repos/$env:GITHUB_REPOSITORY/actions/runs/$env:INSTALLER_RUN" | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($run.path -ne '.github/workflows/desktop.yml' -or $run.head_branch -ne 'codex/desktop-preview') { throw 'Expected Desktop preview workflow artifact.' } + gh run download $env:INSTALLER_RUN --repo $env:GITHUB_REPOSITORY --name desktop-windows-internal-unsigned --dir desktop/.artifacts/source-installer + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $packages = @(Get-ChildItem desktop/.artifacts/source-installer -Recurse -Filter '*-setup.exe') + $reports = @(Get-ChildItem desktop/.artifacts/source-installer -Recurse -Filter windows-smoke.json) + if ($packages.Count -ne 1 -or $reports.Count -ne 1) { throw 'Expected exactly one package and package report.' } + $report = Get-Content -LiteralPath $reports[0].FullName -Raw | ConvertFrom-Json + if ($report.commit -ne $run.head_sha -or (Get-FileHash -LiteralPath $packages[0].FullName).Hash -ne $report.installerSha256 -or $packages[0].Length -ne $report.installerBytes) { throw 'Installer identity mismatch.' } + $bundle = 'desktop/src-tauri/target/release/bundle/nsis' + New-Item -ItemType Directory -Path $bundle -Force | Out-Null + Copy-Item -LiteralPath $packages[0].FullName -Destination $bundle + "DESKTOP_INSTALLER_COMMIT=$($run.head_sha)" >> $env:GITHUB_ENV + - name: Prepare matching WebView2 driver + run: ./desktop/scripts/prepare-webdriver.ps1 + - name: Exercise existing installed package + timeout-minutes: 8 + run: ./desktop/scripts/windows-smoke.ps1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: desktop-installed-diagnostics + include-hidden-files: true + path: | + desktop/.artifacts/windows-smoke.json + desktop/.artifacts/installed-ui* + if-no-files-found: warn diff --git a/desktop/.gitattributes b/desktop/.gitattributes new file mode 100644 index 000000000..6a4b22287 --- /dev/null +++ b/desktop/.gitattributes @@ -0,0 +1,17 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +* text=auto eol=lf +*.png binary +*.ico binary diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 000000000..587e8ea22 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +src-tauri/target/ +src-tauri/gen/ +src-tauri/permissions/autogenerated/ +.artifacts/ + +# Generated validation reports belong in .artifacts/, not source control. +/evidence/ diff --git a/desktop/.mise.toml b/desktop/.mise.toml new file mode 100644 index 000000000..34565706f --- /dev/null +++ b/desktop/.mise.toml @@ -0,0 +1,17 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[tools] +node = "24.14.1" +pnpm = "11.13.1" diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 000000000..f728a6667 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,117 @@ +# PowerContext Desktop preview + +Internal Windows preview for [#1654](https://github.com/oceanbase/powercontext/issues/1654), following RFC #1455. It provides a packaged Tauri 2 shell with Home, Connections, My memories, Chinese/English settings, semantic light/dark/system themes and honest disconnected states. Connection profiles, explicit activation, identity/readiness checks, exact Scope selection and local diagnostics are implemented. Shared note saving, bounded FTS search and exact-version reading are implemented; native workflow qualification remains open. + +See [中文说明](README.zh.md), [security boundary](SECURITY.md), and [validation guide](VALIDATION.md). This is not a supported or signed release, and does not close #1654 or #1428. + +## Run the installed preview + +Download and extract `desktop-windows-internal-unsigned` from a successful **Desktop validation** Actions run on this branch. The installer is under `src-tauri/target/release/bundle/nsis/`. Its accompanying `.artifacts/windows-smoke.json` records the commit, SHA-256 and signature status; compare the downloaded installer with `Get-FileHash -Algorithm SHA256 `. The current package is unsigned and intended for internal validation. + +Install it, then open **PowerContext Desktop Preview** from the Windows Start menu. Vite, Python and a local Server are not prerequisites for launching the installed UI. No connection is active at startup; explicitly connect to an existing Server and select a Scope before saving or searching. Without a Server, the shell and settings remain available. Use the commands below for development mode. + +Uninstall Desktop through Windows **Installed apps**. Uninstallation does not manage the independent Server, business database or Agent configuration; removing Server data is not part of removing Desktop. + +## Build on Windows + +Use Windows 11 x64, Visual Studio C++ Build Tools with a Windows SDK, Node **24.14.1**, pnpm **11.13.1**, Rust **1.95.0 MSVC**, and WebView2. `desktop/.mise.toml`, `rust-toolchain.toml`, `pnpm-lock.yaml` and `src-tauri/Cargo.lock` pin the tools and dependencies independently of Python and the website. + +Some machines default to Rust's GNU host. In PowerShell, explicitly select the pinned MSVC host for this process: + +```powershell +rustup toolchain install 1.95.0-x86_64-pc-windows-msvc --profile minimal --component clippy --component rustfmt +$env:RUSTUP_TOOLCHAIN = '1.95.0-x86_64-pc-windows-msvc' +pnpm --dir desktop install --frozen-lockfile +pnpm --dir desktop desktop:dev +``` + +From the repository root: + +```powershell +pnpm --dir desktop lint +pnpm --dir desktop typecheck +pnpm --dir desktop test +pnpm --dir desktop build +pnpm --dir desktop ipc:check +cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check +cargo clippy --locked --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings +cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml +pnpm --dir desktop desktop:build +``` + +`build` produces UI assets only. `desktop:build` produces the release executable and current-user NSIS installer under `desktop/src-tauri/target/release/bundle/nsis/`. No Python, private HTTP server, or Vite process is embedded or started by the installed application. Closing the window exits Desktop. + +The unsigned installer is for internal verification. If WebView2 is absent, its configured download bootstrapper needs network access and may require the user to complete Microsoft installation prerequisites. The absent-runtime and standard-user cases require a disposable Windows environment; never remove a developer's WebView2 to simulate them. + +## Connect an existing Server + +1. Open Connections and add a named profile. HTTP is restricted to literal loopback hosts; other addresses require HTTPS. A reverse-proxy path prefix is preserved. +2. Explicitly choose unauthenticated loopback access or Bearer authentication. Bearer storage is either Windows Credential Manager or this session only. Trust/address changes require credential reconfiguration. An optional CA augments system trust without disabling certificate checks. +3. Select a qualified compatibility profile after comparing its tested build with your deployment. The selection does not prove the remote binary identity. See [validation guide](VALIDATION.md) for the exact fixture and supported combinations. +4. Save, then explicitly use the connection. Merely selecting a saved profile does not activate it. Review liveness, readiness, identity and capabilities separately; none implies resource authorization. +5. Find an authorized Scope by title (50 per page), inspect a default suggestion, or enter an exact Scope ID. Selection never creates a Scope or changes Agent bindings. Editing a query cancels its old read; connection and identity changes invalidate old results. + +Profiles persist under the app data directory; credentials never appear in profile JSON. Active authorization, session-only credentials, Scope selection and query/results are not restored as an authenticated offline session. Remove a profile to remove its Desktop configuration and owned credential reference; it does not stop the Server or remove business data. + +## Save, find and read a note + +After explicitly activating a qualified connection and choosing a Scope, use Home's note form or **My memories → Add note**. The target connection and exact Scope are shown before submission. Enter inserts a newline; only the save button submits. The input is plain text, limited conservatively to 8192 raw UTF-8 bytes without truncation. The Server owns normalization and the returned text is authoritative. + +Search uses FTS in the selected Scope and returns at most 10 matches. This is not a full directory or history, and ten matches do not establish a total count. **Read exact version** sends the complete returned citation; it never substitutes the latest version. Copy buttons explicitly copy either the full plain text or citation JSON. + +A successful save without an entry is reported as an operation success without inventing a citation. A timeout or interrupted dispatched write is **unknown**, not a safe invitation to retry: inspect the original Server/Scope before deciding whether to submit again. Identical text alone cannot identify that operation. Desktop does not automatically replay writes or keep an offline queue. Switching context hides old results while retaining minimal original-operation metadata for this session. See [validation guide](VALIDATION.md) for tested behavior and qualification gaps. + +## Contracts and resources + +`pnpm --dir desktop generate` derives TypeScript schemas and the reviewed ten-operation manifest from `openapi/powercontext.yaml`; `generate:check` detects drift, including a normalized contract SHA-256. Typed Rust adapters consume that generated manifest and generated wire schemas. No public route is independently handwritten in the application. The manifest is not an IPC permission grant. + +`cargo run --locked --manifest-path desktop/src-tauri/Cargo.toml --example export_ipc` derives the TypeScript IPC request/receipt/error types from Rust. `ipc:check` verifies them. IPC exposes typed connection, Scope, memory and diagnostic operations to the main window only; no generic fetch, shell, file, database or secret-read command is granted. + +The canonical brand source is `website/assets/powercontext-color.png`, which is read directly without running the website. `pnpm --dir desktop icons` extracts its square mark and uses the pinned Tauri CLI to derive the Windows icon; `icons:check` verifies all three generated assets against the canonical source. UI SVGs originate from the repository's Desktop design assets and are promoted into `ui/src/assets/` for reproducible builds. They are project resources under the repository Apache-2.0 license. + +## Native credential feasibility + +```powershell +cargo run --locked --manifest-path desktop/src-tauri/Cargo.toml --example credential_probe +``` + +This explicit probe creates a uniquely named synthetic credential in the preview namespace, loads it in another process and deletes it. It never reads another application's credential or prints a secret. Normal tests simulate unavailable storage and require an explicit session-only choice. Do not treat the probe as end-to-end S2 credential-form verification. + +The Windows CI workflow runs on PRs and the preview branch, verifies native vault persistence, builds an unsigned internal installer, and tests Chinese-path installation/uninstallation on its disposable runner. It uploads the installer and a JSON smoke report. CI and mocked IPC tests do not prove clean-machine installation, notification activation, independent service login or Agent capture/recall. + +## Local CLI diagnostics + +Settings provides explicit local service and Agent integration checks. They remain separate from the active remote connection; a missing local CLI does not disable remote operations. Integration checks may start temporary Agent helpers and do not prove capture/recall. + +Register an explicitly trusted local installation in `%APPDATA%/com.powercontext.desktop.preview/diagnostic-cli.json`: + +```json +{ + "executable": "C:/trusted/powercontext/Scripts/powercontext.exe", + "sha256": "REPLACE_WITH_VERIFIED_64_CHARACTER_SHA256", + "version": "1.0.1.dev61+g63f918b7e.d20260919", + "source": "explicit_local_installation" +} +``` + +The native adapter checks the absolute executable path, pinned digest and fixed `--version` result before running either `service status --json` or `doctor integrations --json`. This is a local installation pin, not publisher-signature verification; the Python environment and dependencies must also be trusted. The adapter admits 1.0.1 and 1.1.1 and their development builds; the registration must pin the exact installed version. The example identifies the tested baseline. Other version series require adapter qualification. No PATH-first CLI selection or renderer-provided commands are accepted. + +Version verification has a 15-second deadline, service status 20 seconds, and integration diagnostics 60 seconds. Each invocation limits combined stdout/stderr to 256 KiB. Helpers run hidden in an owned Windows Job; completion, timeout and cancellation clean up their descendants. Only allowlisted status fields reach the UI. Valid unhealthy JSON remains useful even with exit code 1. Isolated real-CLI checks pass; installed-application qualification remains open in [validation guide](VALIDATION.md). + +[Validation guide](VALIDATION.md) describes reproducible checks and outstanding platform and product gates. + +## Remote installed-package acceptance + +Windows GitHub Actions builds an unsigned installer, installs into a temporary Chinese path, and uses a matching Microsoft-signed WebDriver to operate the actual installed WebView2 page. An independent SQLite Server with synthetic data supports explicit connection activation, exact Scope selection, multiline Chinese note save, FTS search, exact reading, and paste-back verification of copied text and citation. The fixture Server and its temporary workspace are cleaned up afterward. + +Reports, a screenshot and driver logs accompany the installer in the `desktop-windows-internal-unsigned` artifact. Failed or pending steps are not acceptance passes. The UI script permits only GitHub Windows runners and does not operate your local desktop. Hosted runners do not establish clean standard-user Windows 11, actual IME, screen-reader or Agent-host qualification. + +To diagnose installed UI tests, manually dispatch `Desktop validation` with `installer_run` set to an existing Desktop CI run ID. It verifies the original package commit, digest and size before reusing that exact installer, and reports harness and installer commits separately. This diagnostic run does not replace final full-build acceptance. + +Installed UI acceptance also checks content isolation between two independent connections, disconnect/reconnect, Server data preservation after removing an inactive profile, and the unknown outcome without replay after a real committed write loses its response. Credit each scenario only when its matching run report passes. + +The CI-only lifecycle scenario forcibly ends its own installed Desktop process after saving a synthetic note, then checks that the independent Server still serves the original exact entry and accepts a new readable write. Consult the matching lifecycle report for its result; it does not simulate normal window closure or uninstall preservation. + +Installed boundary checks exercise an 8192-byte Unicode note, reject over-budget input, display zero and capped-ten search results, and verify cancel/confirm behavior when disconnecting with an unsaved draft. Each result requires its matching remote report. + +After upgrading from the earlier preview, select `sqlite-1.1.1-v1` and recheck your connection. The previous `sqlite-63f918b7-v1` selection is not silently upgraded to a different contract. See [current qualification](VALIDATION.md). diff --git a/desktop/README.zh.md b/desktop/README.zh.md new file mode 100644 index 000000000..68b5d3dcb --- /dev/null +++ b/desktop/README.zh.md @@ -0,0 +1,99 @@ +# PowerContext Desktop 预览版 + +这是 #1654 的内部工程预览。已建立 Tauri 2、React、TypeScript 和 Vite 独立工程,提供首页、连接、我的记忆、双语设置及浅色/深色/系统主题。无连接时保存和搜索不可用,不展示示例业务数据。 + +已实现连接配置、显式激活、身份与就绪检查、精确范围选择和本地诊断。已实现共享笔记表单、FTS 搜索和精确版本阅读;原生交互闭环仍待完整验收。当前不是已签名的正式发行版。 + +## 直接运行安装版 + +在本分支的 GitHub Actions「Desktop validation」成功运行中,下载 `desktop-windows-internal-unsigned` 产物并解压,找到 `src-tauri/target/release/bundle/nsis/` 中的安装程序。对应 `.artifacts/windows-smoke.json` 记录提交、安装包 SHA-256 和签名状态,可用 `Get-FileHash -Algorithm SHA256 <安装包路径>` 核对下载文件。当前产物未签名,供内部验证。 + +运行安装程序后,从开始菜单打开「PowerContext Desktop Preview」。不需要先启动 Vite、Python 或本地 Server;首次打开没有活动连接,按下一节连接已有 Server 后才能保存和搜索。没有可用 Server 时仍可查看界面与设置。开发模式请使用下方命令。 + +在 Windows「已安装的应用」中卸载 Desktop。卸载不负责删除 Server、业务数据库或 Agent 配置;不要把清理 Server 数据作为卸载桌面的步骤。 + +## 开发与构建 + +Windows 11 x64 上需要 Node 24.14.1、pnpm 11.13.1、Rust 1.95.0 MSVC、Visual Studio C++ Build Tools、Windows SDK 和 WebView2。安装后的用户程序不依赖 Node、Rust、Python 或本地 Server。 + +从仓库根目录执行: + +```powershell +rustup toolchain install 1.95.0-x86_64-pc-windows-msvc --profile minimal --component clippy --component rustfmt +$env:RUSTUP_TOOLCHAIN = '1.95.0-x86_64-pc-windows-msvc' +pnpm --dir desktop install --frozen-lockfile +pnpm --dir desktop desktop:dev +``` + +`pnpm --dir desktop build` 只构建前端。`pnpm --dir desktop desktop:build` 构建原生 release 程序和当前用户 NSIS 安装包,输出到 `desktop/src-tauri/target/release/bundle/nsis/`。完整检查入口见 [英文 README](README.md)。 + +缺少 WebView2 时,安装器配置为下载 Microsoft bootstrapper,需要网络。标准用户、无 WebView2、无 Python 的干净机器场景必须在隔离环境验证,不能通过卸载开发机依赖来模拟。 + +## 连接已有 Server + +1. 在连接页添加名称和 Server 地址。HTTP 只允许字面 loopback 地址,其他地址必须 HTTPS;可保留反向代理路径前缀。 +2. 明确选择未认证本机服务或 Bearer。凭据可保存到 Windows 凭据管理器或仅本次会话;地址或信任变化后需要重新配置凭据。自定义 CA 不会关闭证书校验。 +3. 核对部署版本后选择已验证兼容配置,具体构建摘要和范围见 [验证说明](VALIDATION.md)。选择配置不能证明远端二进制身份。 +4. 保存后点击“使用此连接”。查看其他配置不会自动切换当前连接。分别查看可达性、就绪、身份和能力,不把其中一项当作资源授权。 +5. 按标题分页查找范围、查看默认建议,或输入精确 Scope ID。每页最多 50 项;不创建范围或修改 Agent 绑定。修改查询取消旧请求,切换连接或身份后旧结果失效。 + +连接配置存放在应用数据目录,JSON 不包含秘密。重启后需要重新验证连接和选择范围;仅会话凭据不保留。移除连接仅移除此桌面的配置和自有凭据,不停止 Server 或删除业务数据。 + +工程边界: + +- OpenAPI 自动派生的 TypeScript schema 和十项 operation 映射,包含契约摘要及漂移检查。 +- Rust 自动派生的 IPC 类型与安全错误;凭据只写请求必须显式选择持久化或仅会话,成功回执只含存储模式,不含秘密或凭据标识。 +- Windows Credential Manager 原生适配器;保存失败直接报错,只有显式选择才采用仅会话内存。秘密类型不支持序列化,不实现 Debug。 +- 使用系统证书信任的原生 HTTPS;可为单个客户端添加显式 CA,继续校验主机名。 +- HTTP 仅允许 loopback,拒绝地址中的用户信息、查询、片段、反斜杠及路径逃逸;不继承代理、不跟随重定向。 +- 连接超时 5 秒、完整请求超时 15 秒、响应上限 1 MiB、每个客户端最多 4 个并发读取;超限明确失败。 +- 主窗口可调用明确列出的连接、Scope、记忆和诊断命令;没有通用 fetch、shell、文件、数据库或读取秘密的 IPC。 + +应用不启动 Server、不管理服务、不持久化正文或查询;关闭窗口即退出。当前语言和主题也仅保留于会话内。 + +[安全边界](SECURITY.md)记录具体约束;[验证说明](VALIDATION.md)列出测试范围和仍未满足的 P0 门槛。构建成功、Mock 测试和当前用户机器上的运行都不能代替标准用户、签名、通知冷启动激活、独立服务和 Agent 宿主验收。 + +## 本机 CLI 诊断 + +设置中的诊断只检查当前电脑,与远程连接状态分开。程序不从 PATH 自动选择 CLI,也不安装或启动 Server。集成检查可能运行临时 Agent 辅助进程;结果不代表实际 capture/recall 已验证。 + +显式信任某个本地 PowerContext 安装后,在 `%APPDATA%/com.powercontext.desktop.preview/diagnostic-cli.json` 写入下面的配置。`executable` 必须是 `powercontext.exe` 的绝对路径,`sha256` 是该文件经核对的 SHA-256。当前诊断适配器允许 1.0.1 和 1.1.1 及其开发构建;必须填写实际安装的精确版本,下列示例是已测试基线。其他版本系列需另行验证。不要把文件摘要匹配当作发布者签名认证;Python 安装及其依赖也必须来自你信任的环境。 + +```json +{ + "executable": "C:/trusted/powercontext/Scripts/powercontext.exe", + "sha256": "填写经核对的64位SHA256摘要", + "version": "1.0.1.dev61+g63f918b7e.d20260919", + "source": "explicit_local_installation" +} +``` + +Desktop 在每次诊断前检查固定路径、摘要,并用固定的 `--version` 核验版本;然后只允许 `service status --json` 或 `doctor integrations --json`。版本核验最长 15 秒,服务检查最长 20 秒,集成检查最长 60 秒;每次进程调用的 stdout/stderr 合计最多 256 KiB。窗口隐藏,进程及其后代归属于本次专属 Windows Job,结束或超时后清理。界面只显示允许的状态字段,不显示任意 detail、路径或原始输出。合法 JSON 配合退出码 1 仍可显示不健康结果。 + +未配置或未通过核验时,本机诊断不可用,独立远程业务仍可使用。进程树清理和隔离真实 CLI 测试已通过;安装后交互与完整 S2 资格仍待验收。 + +## 保存、查找和阅读 + +明确启用合格连接并选择精确范围后,在首页填写笔记,或在「我的记忆」中点击「记一条」。提交前会显示目标连接和范围。Enter 只换行,点击保存才提交。正文是纯文本,保守限制为原始文本最多 8192 个 UTF-8 字节,不会截断;规范化由 Server 完成,以返回的正文为准。 + +全文搜索只在当前范围执行,每次最多返回 10 条,不代表完整目录、历史或总数。阅读使用完整引用读取精确版本,不自动改读最新版本。复制按钮分别复制纯文本正文和引用 JSON。 + +保存成功但没有返回 entry 时,只报告操作成功,不编造引用。请求发出后的超时或中断可能意味着「结果未知」:先核对原连接和范围,再决定是否重新提交;相同正文不能证明属于这次操作。应用不会自动重试写入,也没有离线队列。切换连接或范围后隐藏旧结果,本次会话只保留最小操作状态和原目标。测试范围与剩余验收见 [验证说明](VALIDATION.md)。 + +[验证说明](VALIDATION.md)列出可复现的检查方式和待完成的平台、产品验收项。 + +## 远程安装包验收 + +Windows GitHub Actions 会构建未签名安装包,安装到临时中文路径,并通过匹配且验证 Microsoft 签名的 WebDriver 操作实际安装的 WebView2 页面。测试使用独立 SQLite Server,覆盖启用连接、精确范围选择、中文多行笔记保存、全文查找、精确阅读,以及正文和引用复制后的粘贴核对。Server 使用合成数据,测试结束后清理。 + +运行结果、界面截图和驱动日志与安装包一起保存在 `desktop-windows-internal-unsigned` 工件中;失败或尚未执行的步骤不计为验收通过。该脚本仅允许在 GitHub Windows runner 执行,不需要操作你的电脑。托管 runner 不等同于干净的 Windows 11 普通用户环境,也不能代替真实输入法、读屏软件和 Agent 宿主验收。 + +调试安装后测试时,可以手动运行 `Desktop validation`,将 `installer_run` 填为已有 Desktop CI 的运行 ID。流水线会核对原安装包的提交、摘要和大小,再复用该包测试;报告分别记录测试脚本和安装包的提交。这种诊断不代替最终完整构建验收。 + +界面验收还检查两个独立连接之间的内容隔离、断开重连、移除非活动配置后保留 Server 数据,以及真实写入响应丢失后的“结果未知”和不自动重试。各场景是否已通过,以对应运行报告为准。 + +仅在 CI 执行的生命周期场景会先通过安装版保存测试记忆,再强制结束本次测试启动的 Desktop,独立检查 Server 是否仍能读取原记忆并完成新的写入和读取。结果以对应生命周期报告为准;这不代替正常关窗或卸载保留数据库的验收。 + +安装版边界测试覆盖 8192 字节 Unicode 正文、超限输入禁止提交、搜索无结果和最多 10 条的提示,以及未保存草稿时取消或确认断开连接的行为。是否通过以对应远程报告为准。 + +从早期预览版升级后,请选择 `sqlite-1.1.1-v1` 并重新检查连接。旧的 `sqlite-63f918b7-v1` 选择不会静默升级为另一份契约。当前验证范围见[验证说明](VALIDATION.md)。 diff --git a/desktop/SECURITY.md b/desktop/SECURITY.md new file mode 100644 index 000000000..ccb810024 --- /dev/null +++ b/desktop/SECURITY.md @@ -0,0 +1,37 @@ +# Native boundary + +## Renderer + +Only the packaged main window receives the explicit `main` capability. The app manifest enumerates every typed connection, Scope and diagnostic command, so Tauri does not implicitly grant custom commands to every window. The handler also checks the native window label. No capability grants a remote origin, general HTTP, shell, filesystem, SQL, notification, opener or credential-read access. + +Production CSP allows packaged scripts/styles/images and Tauri IPC only, with no remote frames, forms or network fetch. The navigation callback permits the packaged origin, and permits the exact Vite origin only in debug builds. Production builds use Tauri's `custom-protocol` feature. No CSP bypass, unsafe eval, remote image or HTML execution is enabled. React renders text, never `dangerouslySetInnerHTML`. + +## Secrets and transport + +The vault namespace is `com.powercontext.desktop.preview`. A native credential ID is an opaque bounded identifier; it is not an endpoint or a user-controlled vault path. Windows credentials use the OS store only. Unavailable storage returns a stable error; session-only storage is an explicit separate choice. Secret values have zeroizing storage and cannot be serialized or debug formatted. The renderer has no secret readback operation. The synthetic opt-in probe only touches its unique test entry. + +The Rust-derived write protocol accepts only `secret` and an explicit `storage` choice (`persistent` or `session_only`). Its input cannot be serialized; the success receipt contains only the storage choice. Missing modes, unknown modes and extra fields are rejected. The native caller supplies the credential ID; the renderer cannot choose a vault address. A failed persistent write produces no success receipt or automatic fallback. + +Native code creates credential IDs and binds them to an exact endpoint/trust configuration. Editing the address or trust invalidates the active context and prevents retaining its old credential. Profile storage is atomic and secret-free, with a bounded journal for deferred cleanup of owned vault entries. + +Native reqwest uses Windows system trust through native-tls/Schannel, plus an optional connection-local PEM CA (maximum 64 KiB). Hostname/certificate validation remains enabled. Automatic proxy inheritance and redirects are disabled. HTTP requires loopback. Userinfo, query, fragment, control characters, backslashes and dot segments are rejected before URL normalization. UTF-8 and percent-encoded base paths are accepted only after rejecting encoded separators, dot segments, control characters and ambiguous double encoding. Public operation paths come from the generated OpenAPI manifest and preserve the base prefix. + +Typed adapters separate liveness, readiness, identity and capabilities. Native context generations cancel old reads on connection, identity and Scope changes; a separate query generation cancels superseded Scope searches. Business operations require explicit qualified compatibility and actual Server authorization. The real-Server fixture exercises native memory adapters; product Memory commands remain unavailable until immutable write context and unknown-result handling are implemented. No automatic write retry or persistent queue exists. + +## Budgets and error projection + +| Boundary | Limit | +| --- | --- | +| Endpoint input | 2048 bytes | +| Bearer input | 1–2048 printable ASCII bytes | +| Additional CA PEM | 64 KiB | +| Connect timeout | 5 seconds | +| Complete request, including response body | 15 seconds | +| Response body | 1 MiB, checked both by declared size and streamed bytes | +| Concurrent reads per client | 4; excess fails as busy | + +Error responses are enum codes. Transport exception text, endpoint URLs, bodies and credentials are never returned or logged. No product logging, telemetry, crash uploader or persistent business cache is configured. The app does not read databases, start a Server or inspect process environments. Explicit local diagnostics use a pinned absolute executable, digest and fixed version check, then one of two native-defined commands. Each invocation has a combined 256 KiB output budget and a deadline. Windows helpers enter an owned kill-on-close Job before resuming; raw output is projected to allowlisted status fields and discarded. A local CLI pin does not authenticate its publisher or freeze its Python dependencies; the configured installation must be trusted. + +Tests cover actual TLS fixtures and adversarial HTTP responses, shared loopback policy, mocked Tauri permission resolution with the real capability configuration, navigation rejection, unavailable vault behavior and the explicit session path. The opt-in Windows vault probe separately verifies persistence across processes. See the [validation guide](VALIDATION.md) for packaged checks, CI artifacts and remaining platform qualification. + +Framework references: [Tauri capabilities](https://v2.tauri.app/security/capabilities/), [Windows installer options](https://v2.tauri.app/distribute/windows-installer/). diff --git a/desktop/VALIDATION.md b/desktop/VALIDATION.md new file mode 100644 index 000000000..b2c719836 --- /dev/null +++ b/desktop/VALIDATION.md @@ -0,0 +1,39 @@ +# Desktop validation + +Desktop is an unsigned internal Windows preview. Passing CI does not establish release qualification. + +## Reproducible checks + +Run from the repository root: + +```sh +pnpm --dir desktop install --frozen-lockfile +pnpm --dir desktop lint +pnpm --dir desktop typecheck +pnpm --dir desktop test +pnpm --dir desktop build +pnpm --dir desktop ipc:check +cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check +cargo clippy --locked --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings +cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml +``` + +[Desktop CI](../.github/workflows/desktop.yml) also builds a Server wheel, runs real Server and CLI tests, checks Windows credentials and process containment, builds the installer, and exercises installation, the installed UI and uninstallation. Consult the PR checks for the exact commit being reviewed. Reports, logs and screenshots are generated under ignored `.artifacts/` and uploaded as Actions artifacts; do not copy them into the repository. + +## Compatibility scope + +[compatibility.json](src-tauri/src/connections/compatibility.json) identifies the qualified Server source, normalized OpenAPI contract digest and qualification-wheel digest for `sqlite-1.1.1-v1`. The qualification wheel was built from checkout `3501e4a6`; its backend sources, OpenAPI contract, project configuration and lock match the Server commit recorded in that manifest. CI builds have their own artifact identities. + +The real Server harness covers SQLite with anonymous loopback, static Bearer, HTTPS with explicit CA/base path, and injected-provider/enforced access. It exercises save/search/exact reads, identity changes, binding revocation, ambiguous writes without replay and Scope pagination. Installed UI checks cover explicit connection/Scope selection, clipboard copies, connection isolation, draft cancellation, byte limits and independent Server survival after Desktop exit. + +Selecting a compatibility profile does not attest the remote binary identity. Existing `sqlite-63f918b7-v1` selections require explicitly choosing the new profile and rechecking the connection. CLI diagnostics separately verify the registered executable path, digest and exact version. + +## Remaining release qualification + +- Clean standard-user Windows 11, including absent/present WebView2 and bootstrap recovery. +- Publisher signing and release ownership. +- Actual IME, screen reader, high contrast and complete keyboard accessibility. +- Notification cold activation, independent service login/reboot behavior and a named Agent host's capture/recall workflow. +- Maintainer ownership and approved performance budgets. + +Hosted Windows CI and developer-machine checks do not replace these platform and product gates. This preview does not close #1654 or #1428. diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 000000000..ca025974c --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,49 @@ +{ + "name": "powercontext-desktop", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@11.13.1", + "engines": { + "node": "24.14.1" + }, + "scripts": { + "dev": "vite --host 127.0.0.1", + "generate": "node scripts/generate.mjs", + "generate:check": "node scripts/generate.mjs --check", + "lint": "oxlint ui scripts && pnpm generate:check && pnpm icons:check && pnpm format:check", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "build": "pnpm generate:check && tsc --noEmit && vite build", + "tauri": "tauri", + "desktop:dev": "tauri dev", + "desktop:build": "tauri build --bundles nsis --ci -- --locked", + "ipc:check": "cargo run --locked --manifest-path src-tauri/Cargo.toml --example export_ipc -- --check", + "format:check": "prettier --check ui/src/app ui/src/shared ui/tests scripts vite.config.ts", + "icons": "node scripts/icons.mjs", + "icons:check": "node scripts/icons.mjs --check" + }, + "dependencies": { + "@tauri-apps/api": "2.11.1", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@tauri-apps/cli": "2.11.4", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", + "@types/node": "26.3.0", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.5", + "@vitejs/plugin-react": "6.1.1", + "jsdom": "26.1.0", + "openapi-typescript": "7.13.0", + "oxlint": "1.80.0", + "prettier": "3.6.2", + "sharp": "0.34.5", + "typescript": "5.9.3", + "vite": "8.3.0", + "vitest": "5.0.1", + "yaml": "2.9.0" + } +} diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml new file mode 100644 index 000000000..303b5ced5 --- /dev/null +++ b/desktop/pnpm-lock.yaml @@ -0,0 +1,2079 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tauri-apps/api': + specifier: 2.11.1 + version: 2.11.1 + react: + specifier: 19.2.8 + version: 19.2.8 + react-dom: + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) + devDependencies: + '@tauri-apps/cli': + specifier: 2.11.4 + version: 2.11.4 + '@testing-library/react': + specifier: 16.3.0 + version: 16.3.0(@testing-library/dom@10.4.2)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.2) + '@types/node': + specifier: 26.3.0 + version: 26.3.0 + '@types/react': + specifier: 19.2.18 + version: 19.2.18 + '@types/react-dom': + specifier: 19.2.5 + version: 19.2.5(@types/react@19.2.18) + '@vitejs/plugin-react': + specifier: 6.1.1 + version: 6.1.1(vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0)) + jsdom: + specifier: 26.1.0 + version: 26.1.0(supports-color@10.2.2) + openapi-typescript: + specifier: 7.13.0 + version: 7.13.0(typescript@5.9.3) + oxlint: + specifier: 1.80.0 + version: 1.80.0 + prettier: + specifier: 3.6.2 + version: 3.6.2 + sharp: + specifier: 0.34.5 + version: 0.34.5 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 8.3.0 + version: 8.3.0(@types/node@26.3.0)(yaml@2.9.0) + vitest: + specifier: 5.0.1 + version: 5.0.1(@types/node@26.3.0)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0)) + yaml: + specifier: 2.9.0 + version: 2.9.0 + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.150.0': + resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.20': + resolution: {integrity: sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@rolldown/binding-android-arm-eabi@1.2.9': + resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.9': + resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.9': + resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.9': + resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.9': + resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.9': + resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.9': + resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.9': + resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.9': + resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.9': + resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.9': + resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.9': + resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.9': + resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tauri-apps/api@2.11.1': + resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.4': + resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.4': + resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} + engines: {node: '>= 10'} + hasBin: true + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.0': + resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.3.0': + resolution: {integrity: sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==} + + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@vitejs/plugin-react@6.1.1': + resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + '@vitest/mocker@5.0.1': + resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.1': + resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@1.4.1: + resolution: {integrity: sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nwsapi@2.2.27: + resolution: {integrity: sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rolldown@1.2.9: + resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + vite@8.3.0: + resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.1: + resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.1 + '@vitest/browser-preview': 5.0.1 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.1 + '@vitest/coverage-v8': 5.0.1 + '@vitest/ui': 5.0.1 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/runtime@7.29.7': {} + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.150.0': {} + + '@oxlint/binding-android-arm-eabi@1.80.0': + optional: true + + '@oxlint/binding-android-arm64@1.80.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.80.0': + optional: true + + '@oxlint/binding-darwin-x64@1.80.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.80.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.80.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.80.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.80.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.80.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.80.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.80.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.80.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.80.0': + optional: true + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.20(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.3.2 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@rolldown/binding-android-arm-eabi@1.2.9': + optional: true + + '@rolldown/binding-android-arm64@1.2.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.9': + optional: true + + '@rolldown/binding-darwin-x64@1.2.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.9': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.9': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tauri-apps/api@2.11.1': {} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli@2.11.4': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.4 + '@tauri-apps/cli-darwin-x64': 2.11.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 + '@tauri-apps/cli-linux-arm64-musl': 2.11.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-musl': 2.11.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 + '@tauri-apps/cli-win32-x64-msvc': 2.11.4 + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.0(@testing-library/dom@10.4.2)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.2)': + dependencies: + '@testing-library/dom': 10.4.2 + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@26.3.0': + dependencies: + undici-types: 8.3.0 + + '@types/react-dom@19.2.5(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.0(@types/node@26.3.0)(yaml@2.9.0) + + '@vitest/mocker@5.0.1(vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.1 + estree-walker: 3.0.3 + magic-string: 1.4.1 + optionalDependencies: + vite: 8.3.0(@types/node@26.3.0)(yaml@2.9.0) + + '@vitest/spy@5.0.1': {} + + agent-base@7.1.4: {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.1.7: + dependencies: + balanced-match: 1.0.2 + + chai@6.2.2: {} + + change-case@5.4.4: {} + + colorette@1.4.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + decimal.js@10.6.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + entities@6.0.1: {} + + es-module-lexer@2.3.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + index-to-position@1.2.0: {} + + is-potential-custom-element-name@1.0.1: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0(supports-color@10.2.2): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2(supports-color@10.2.2) + https-proxy-agent: 7.0.6(supports-color@10.2.2) + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.27 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + json-schema-traverse@1.0.0: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lru-cache@10.4.3: {} + + lz-string@1.5.0: {} + + magic-string@1.4.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.7 + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + nwsapi@2.2.27: {} + + obug@2.2.1: {} + + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.20(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + + oxlint@1.80.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pluralize@8.0.0: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.6.2: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react@19.2.8: {} + + require-from-string@2.0.2: {} + + rolldown@1.2.9: + dependencies: + '@oxc-project/types': 0.150.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.9 + '@rolldown/binding-android-arm64': 1.2.9 + '@rolldown/binding-darwin-arm64': 1.2.9 + '@rolldown/binding-darwin-x64': 1.2.9 + '@rolldown/binding-freebsd-x64': 1.2.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.9 + '@rolldown/binding-linux-arm64-gnu': 1.2.9 + '@rolldown/binding-linux-arm64-musl': 1.2.9 + '@rolldown/binding-linux-ppc64-gnu': 1.2.9 + '@rolldown/binding-linux-s390x-gnu': 1.2.9 + '@rolldown/binding-linux-x64-gnu': 1.2.9 + '@rolldown/binding-linux-x64-musl': 1.2.9 + '@rolldown/binding-openharmony-arm64': 1.2.9 + '@rolldown/binding-win32-arm64-msvc': 1.2.9 + '@rolldown/binding-win32-x64-msvc': 1.2.9 + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@7.8.5: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + supports-color@10.2.2: {} + + symbol-tree@3.2.4: {} + + tinybench@6.1.4: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tslib@2.8.1: + optional: true + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + undici-types@8.3.0: {} + + uri-js-replace@1.0.1: {} + + vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.9 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.3.0 + fsevents: 2.3.3 + yaml: 2.9.0 + + vitest@5.0.1(@types/node@26.3.0)(jsdom@26.1.0(supports-color@10.2.2))(vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@26.3.0)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.1 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.3.0(@types/node@26.3.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.3.0 + jsdom: 26.1.0(supports-color@10.2.2) + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yaml-ast-parser@0.0.43: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} diff --git a/desktop/pnpm-workspace.yaml b/desktop/pnpm-workspace.yaml new file mode 100644 index 000000000..6aebbe802 --- /dev/null +++ b/desktop/pnpm-workspace.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +allowBuilds: + sharp: true diff --git a/desktop/rust-toolchain.toml b/desktop/rust-toolchain.toml new file mode 100644 index 000000000..75433afd3 --- /dev/null +++ b/desktop/rust-toolchain.toml @@ -0,0 +1,18 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[toolchain] +channel = "1.95.0" +targets = ["x86_64-pc-windows-msvc"] +components = ["rustfmt", "clippy"] diff --git a/desktop/scripts/capture-installed-process.ps1 b/desktop/scripts/capture-installed-process.ps1 new file mode 100644 index 000000000..2e6f00d1c --- /dev/null +++ b/desktop/scripts/capture-installed-process.ps1 @@ -0,0 +1,65 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +param([Parameter(Mandatory)][int]$ApplicationPid, [Parameter(Mandatory)][string]$ArtifactDirectory) +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_ACTIONS -ne 'true') { throw 'Disposable CI runner required.' } +$allProcesses = @(Get-CimInstance Win32_Process) +$owned = [System.Collections.Generic.HashSet[int]]::new() +[void]$owned.Add($ApplicationPid) +do { + $added = $false + foreach ($candidate in $allProcesses) { + if ($owned.Contains([int]$candidate.ParentProcessId) -and $owned.Add([int]$candidate.ProcessId)) { $added = $true } + } +} while ($added) +$details = @($allProcesses | Where-Object { $owned.Contains([int]$_.ProcessId) } | ForEach-Object { + $process = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue + [ordered]@{ + pid = $_.ProcessId; parentPid = $_.ParentProcessId; name = $_.Name + commandLine = $_.CommandLine; sessionId = $_.SessionId + responding = $process.Responding; windowTitle = $process.MainWindowTitle + windowHandle = $process.MainWindowHandle.ToInt64() + } +}) +$principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()) +[ordered]@{ runnerSessionId = (Get-Process -Id $PID).SessionId; runnerElevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator); processes = $details } | + ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $ArtifactDirectory 'installed-ui-processes.json') -Encoding utf8 +$app = Get-Process -Id $ApplicationPid -ErrorAction SilentlyContinue +if (-not $app -or $app.MainWindowHandle -eq [IntPtr]::Zero) { exit 0 } +Add-Type -AssemblyName System.Drawing +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +public static class InstalledWindowCapture { + [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left, Top, Right, Bottom; } + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr window, out Rect rect); + [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr window, IntPtr target, uint flags); +} +"@ +$bounds = [InstalledWindowCapture+Rect]::new() +if (-not [InstalledWindowCapture]::GetWindowRect($app.MainWindowHandle, [ref]$bounds)) { exit 0 } +$width = $bounds.Right - $bounds.Left +$height = $bounds.Bottom - $bounds.Top +if ($width -le 0 -or $height -le 0 -or $width -gt 4096 -or $height -gt 4096) { exit 0 } +$bitmap = [System.Drawing.Bitmap]::new($width, $height) +try { + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + try { + $context = $graphics.GetHdc() + try { $captured = [InstalledWindowCapture]::PrintWindow($app.MainWindowHandle, $context, 2) } + finally { $graphics.ReleaseHdc($context) } + } finally { $graphics.Dispose() } + if ($captured) { $bitmap.Save((Join-Path $ArtifactDirectory 'installed-ui-window.png')) } +} finally { $bitmap.Dispose() } diff --git a/desktop/scripts/generate.mjs b/desktop/scripts/generate.mjs new file mode 100644 index 000000000..89e7b29c0 --- /dev/null +++ b/desktop/scripts/generate.mjs @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import YAML from "yaml"; +import openapiTS, { astToString } from "openapi-typescript"; + +const root = new URL("../", import.meta.url); +const contractUrl = new URL("../openapi/powercontext.yaml", root); +const raw = await readFile(contractUrl, "utf8"); +const contract = YAML.parse(raw); +const wanted = [ + "get_liveness", + "get_readiness", + "get_capabilities", + "get_access_principal", + "list_scopes", + "get_scope", + "get_default_scope", + "remember_memory", + "search_memory", + "get_memory_entry", +]; +const operations = {}; +for (const [path, item] of Object.entries(contract.paths)) { + for (const [method, op] of Object.entries(item)) { + if (wanted.includes(op?.operationId)) { + if (operations[op.operationId]) throw new Error("Duplicate operation"); + operations[op.operationId] = { method: method.toUpperCase(), path }; + } + } +} +if (Object.keys(operations).length !== wanted.length) + throw new Error("Missing public operation"); +const digest = createHash("sha256") + .update(raw.replaceAll("\r\n", "\n")) + .digest("hex"); +const license = + (await readFile(new URL(import.meta.url), "utf8")).split(" */")[0] + + " */\n\n"; +const header = + license + "// Generated from openapi/powercontext.yaml. Do not edit.\n"; +// Rust wire models share the same reachable OpenAPI schema graph as the reviewed operations. +const schemaNames = new Set(); +function collectSchemas(value) { + if (!value || typeof value !== "object") return; + if (value.$ref?.startsWith("#/components/schemas/")) { + const name = value.$ref.split("/").at(-1); + if (!schemaNames.has(name)) { + schemaNames.add(name); + collectSchemas(contract.components.schemas[name]); + } + } + for (const child of Object.values(value)) collectSchemas(child); +} +for (const item of Object.values(contract.paths)) + for (const op of Object.values(item)) + if (wanted.includes(op?.operationId)) collectSchemas(op); +function rustType(schema) { + if (schema.$ref) return schema.$ref.split("/").at(-1); + switch (schema.type) { + case "string": + return "String"; + case "integer": + return "i64"; + case "number": + return "f64"; + case "boolean": + return "bool"; + case "array": + return `Vec<${rustType(schema.items)}>`; + case "object": + if ( + schema.additionalProperties && + typeof schema.additionalProperties === "object" + ) + return `std::collections::BTreeMap`; + throw new Error("Unsupported inline object in Desktop wire model"); + default: + throw new Error(`Unsupported wire schema ${JSON.stringify(schema)}`); + } +} +const rustModels = [...schemaNames] + .sort() + .map((name) => { + const schema = contract.components.schemas[name]; + const derive = + "#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]"; + if (schema.enum) { + const values = schema.enum.filter((v) => v !== null); + const members = values.map( + (value) => + ` #[serde(rename = ${JSON.stringify(value)})]\n ${value + .split(/[^a-zA-Z0-9]+/) + .map((part) => part[0].toUpperCase() + part.slice(1)) + .join("")},`, + ); + return `${derive}\npub enum ${name} {\n${members.join("\n")}\n}\n`; + } + if (schema.type !== "object") + return `pub type ${name} = ${rustType(schema)};\n`; + const fields = Object.entries(schema.properties).map(([field, value]) => { + let type = rustType(value); + if (value.nullable || !schema.required?.includes(field)) + type = `Option<${type}>`; + return `${!schema.required?.includes(field) ? ' #[serde(skip_serializing_if = "Option::is_none")]\n #[ts(optional = nullable)]\n' : ""} pub r#${field}: ${type},`; + }); + return `${derive}\n#[serde(deny_unknown_fields)]\npub struct ${name} {\n${fields.join("\n")}\n}\n`; + }) + .join("\n"); +const rustDeclarations = `\n#[rustfmt::skip]\npub fn declarations(config: &ts_rs::Config) -> Vec {\n vec![\n${[ + ...schemaNames, +] + .sort() + .map((name) => ` <${name} as ts_rs::TS>::decl(config),`) + .join("\n")}\n ]\n}\n`; +const outputs = { + "src-tauri/src/transport/wire.rs": + header + + "// rustfmt uses this generated layout verbatim.\n" + + rustModels + + rustDeclarations, + "ui/src/generated/api.d.ts": header + astToString(await openapiTS(contract)), + "ui/src/generated/operations.ts": + header + + "export const contractSha256 = " + + JSON.stringify(digest) + + ";\nexport const operations = " + + JSON.stringify(operations, null, 2) + + " as const;\n", + "src-tauri/src/transport/operations.json": + JSON.stringify({ contractSha256: digest, operations }, null, 2) + "\n", +}; +for (const [name, text] of Object.entries(outputs)) { + const target = new URL(name, root); + if (process.argv.includes("--check")) { + if ((await readFile(target, "utf8")).replaceAll("\r\n", "\n") !== text) + throw new Error("Contract drift: " + name); + } else { + await mkdir(fileURLToPath(new URL(".", target)), { recursive: true }); + await writeFile(target, text); + } +} +console.log( + process.argv.includes("--check") + ? "Desktop contract matches OpenAPI." + : "Desktop contract generated.", +); diff --git a/desktop/scripts/icons.mjs b/desktop/scripts/icons.mjs new file mode 100644 index 000000000..90c421620 --- /dev/null +++ b/desktop/scripts/icons.mjs @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import sharp from "sharp"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +const root = new URL("../", import.meta.url); +const source = new URL("../website/assets/powercontext-color.png", root); +const output = new URL("src-tauri/icons/", root); +const temporary = new URL(".artifacts/icon-generation/", root); +await mkdir(temporary, { recursive: true }); +await mkdir(output, { recursive: true }); +// Extract the project's square mark, never a screenshot or third-party logo. +const brand = await sharp(fileURLToPath(source)) + .extract({ left: 0, top: 0, width: 240, height: 240 }) + .png() + .toBuffer(); +await writeFile(new URL("brand.png", temporary), brand); +execFileSync( + process.execPath, + [ + fileURLToPath(new URL("node_modules/@tauri-apps/cli/tauri.js", root)), + "icon", + fileURLToPath(new URL("brand.png", temporary)), + "--output", + fileURLToPath(new URL("derived/", temporary)), + ], + { stdio: "pipe" }, +); +for (const name of ["brand.png", "icon.png", "icon.ico"]) { + const expected = + name === "brand.png" + ? brand + : await readFile(new URL("derived/" + name, temporary)); + const destination = new URL(name, output); + if (process.argv.includes("--check")) { + if (!(await readFile(destination)).equals(expected)) + throw new Error("Brand resource drift: " + name); + } else { + await writeFile(destination, expected); + } +} +console.log("Desktop brand resources match their canonical source."); diff --git a/desktop/scripts/prepare-webdriver.ps1 b/desktop/scripts/prepare-webdriver.ps1 new file mode 100644 index 000000000..82015b832 --- /dev/null +++ b/desktop/scripts/prepare-webdriver.ps1 @@ -0,0 +1,27 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +$ErrorActionPreference = 'Stop' +$runtimeRoot = "${env:ProgramFiles(x86)}/Microsoft/EdgeWebView/Application" +$runtime = Get-ChildItem -LiteralPath $runtimeRoot -Directory | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1 +if (-not $runtime) { throw 'WebView2 runtime not found.' } +$driverRoot = Join-Path $env:RUNNER_TEMP 'desktop-webdriver' +New-Item -ItemType Directory -Path $driverRoot -Force | Out-Null +$archive = Join-Path $driverRoot 'driver.zip' +Invoke-WebRequest "https://msedgedriver.microsoft.com/$($runtime.Name)/edgedriver_win64.zip" -OutFile $archive +Expand-Archive -LiteralPath $archive -DestinationPath $driverRoot +$driver = Join-Path $driverRoot 'msedgedriver.exe' +$signature = Get-AuthenticodeSignature -LiteralPath $driver +if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notlike '*Microsoft Corporation*') { throw 'WebDriver publisher verification failed.' } +"DESKTOP_EDGE_DRIVER=$driver" >> $env:GITHUB_ENV diff --git a/desktop/scripts/windows-smoke.ps1 b/desktop/scripts/windows-smoke.ps1 new file mode 100644 index 000000000..61c3551e3 --- /dev/null +++ b/desktop/scripts/windows-smoke.ps1 @@ -0,0 +1,83 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# CI only: this does not qualify interactive or standard-user behavior. +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_ACTIONS -ne 'true') { throw 'Run on a disposable GitHub Actions runner.' } +$desktopRoot = Split-Path -Parent $PSScriptRoot +$packages = @(Get-ChildItem -LiteralPath "$desktopRoot/src-tauri/target/release/bundle/nsis" -Filter '*-setup.exe') +if ($packages.Count -ne 1) { throw 'Expected exactly one installer.' } +$package = $packages[0] +$testRoot = Join-Path $env:RUNNER_TEMP "desktop-smoke-$([guid]::NewGuid().ToString('N'))" +$installDir = Join-Path $testRoot '中文安装目录' +$sentinel = Join-Path $testRoot 'external-data.txt' +New-Item -ItemType Directory -Path $testRoot | Out-Null +Set-Content -LiteralPath $sentinel -Value 'Synthetic external data; not a Server database.' -Encoding utf8 +$sentinelHash = (Get-FileHash -LiteralPath $sentinel).Hash +$signature = Get-AuthenticodeSignature -LiteralPath $package.FullName +$os = Get-CimInstance Win32_OperatingSystem +$report = [ordered]@{ + scope = 'Hosted runner; not standard-user, absent-WebView2 or visual UI qualification' + runnerImage = $env:ImageVersion + commit = $env:GITHUB_SHA + sourceInstallerCommit = $(if ($env:DESKTOP_INSTALLER_COMMIT) { $env:DESKTOP_INSTALLER_COMMIT } else { $env:GITHUB_SHA }) + measuredAtUtc = [DateTime]::UtcNow.ToString('o') + os = "$($os.Caption) $($os.Version) $($os.OSArchitecture)" + buildProfile = 'release' + installerBytes = $package.Length + installerSha256 = (Get-FileHash -LiteralPath $package.FullName).Hash + signature = $signature.Status.ToString() + signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null } + installedExecutableSha256 = $null + installedExecutableBytes = $null + installExit = $null + uninstallExit = $null + externalSentinelPreserved = $false +} +try { + # NSIS /D must be last and unquoted, including paths with spaces. + $installer = Start-Process -FilePath $package.FullName -ArgumentList "/S /D=$installDir" -PassThru -Wait -WindowStyle Hidden + $report.installExit = $installer.ExitCode + if ($installer.ExitCode -ne 0) { throw 'Installer failed.' } + if (-not (Test-Path -LiteralPath (Join-Path $installDir 'powercontext-desktop.exe'))) { + throw 'Installed executable missing.' + } + $installed = Get-Item -LiteralPath (Join-Path $installDir 'powercontext-desktop.exe') + $report.installedExecutableSha256 = (Get-FileHash -LiteralPath $installed.FullName).Hash + $report.installedExecutableBytes = $installed.Length + uv run --no-sync python "$desktopRoot/tests/installed_ui.py" $installed.FullName + if ($LASTEXITCODE -ne 0) { throw 'Installed native UI check failed.' } +} finally { + try { + # Only the exact task-owned installed program may be cleaned up after a failed UI run. + $ownedExecutable = Join-Path $installDir 'powercontext-desktop.exe' + Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq $ownedExecutable } | ForEach-Object { Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue } + $uninstaller = Join-Path $installDir 'uninstall.exe' + if (Test-Path -LiteralPath $uninstaller) { + $uninstall = Start-Process -FilePath $uninstaller -ArgumentList "/S _?=$installDir" -PassThru -Wait -WindowStyle Hidden + $report.uninstallExit = $uninstall.ExitCode + if ($uninstall.ExitCode -ne 0 -or (Test-Path -LiteralPath (Join-Path $installDir 'powercontext-desktop.exe'))) { + throw 'Uninstall did not remove the application.' + } + } + $report.externalSentinelPreserved = (Test-Path -LiteralPath $sentinel) -and ((Get-FileHash -LiteralPath $sentinel).Hash -eq $sentinelHash) + if (-not $report.externalSentinelPreserved) { throw 'External sentinel changed.' } + } finally { + $artifactDir = Join-Path $desktopRoot '.artifacts' + New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null + $report | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $artifactDir 'windows-smoke.json') -Encoding utf8 + $report | ConvertTo-Json | Write-Output + } +} +if ($null -eq $report.uninstallExit) { throw 'Installed uninstaller was missing.' } diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 000000000..8b7fa02bb --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,5231 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.2", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "log", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powercontext-desktop" +version = "0.1.0" +dependencies = [ + "keyring", + "native-tls", + "rcgen", + "reqwest 0.12.28", + "rustls", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tempfile", + "tokio", + "tokio-rustls", + "ts-rs", + "url", + "uuid", + "windows-sys 0.61.2", + "zeroize", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.5", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.6+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ts-rs" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" +dependencies = [ + "thiserror 2.0.20", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "termcolor", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec 0.9.1", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 000000000..06d40e369 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,54 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "powercontext-desktop" +version = "0.1.0" +edition = "2024" +rust-version = "1.95" +license = "Apache-2.0" +default-run = "powercontext-desktop" + +[lib] +name = "powercontext_desktop" + +[features] +custom-protocol = ["tauri/custom-protocol"] + +[build-dependencies] +tauri-build = { version = "=2.6.0", features = [] } + +[dependencies] +tauri = { version = "=2.11.0", features = [] } +serde = { version = "=1.0.228", features = ["derive"] } +serde_json = "=1.0.149" +sha2 = "=0.10.9" +ts-rs = "=12.0.1" +reqwest = { version = "=0.12.28", default-features = false, features = ["native-tls", "json", "stream"] } +url = "=2.5.8" +tokio = { version = "=1.52.1", features = ["macros", "rt-multi-thread", "net", "io-util", "sync", "time"] } +zeroize = "=1.8.2" +native-tls = "=0.2.18" +uuid = { version = "=1.26.1", features = ["v4"] } +tempfile = "=3.27.0" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "=0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Threading", "Win32_System_JobObjects", "Win32_System_Pipes", "Win32_Storage_FileSystem"] } +keyring = { version = "=3.6.3", default-features = false, features = ["windows-native"] } + +[dev-dependencies] +tauri = { version = "=2.11.0", features = ["test"] } +rcgen = "=0.14.8" +tokio-rustls = "=0.26.4" +rustls = { version = "=0.23.40", default-features = false, features = ["ring", "std", "tls12"] } diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 000000000..39c942ee8 --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +fn main() { + // Integration-test executables also need ComCtl32 v6 for Tauri's menu imports. + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + println!("cargo:rustc-link-arg-tests=/MANIFEST:EMBED"); + println!( + "cargo:rustc-link-arg-tests=/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'" + ); + } + tauri_build::try_build(tauri_build::Attributes::new().app_manifest( + tauri_build::AppManifest::new().commands(&[ + "foundation_info", + "local_diagnostics", + "remember_memory", + "search_memory", + "memory_entry", + "cancel_memory_reads", + "desktop_state", + "save_profile", + "remove_profile", + "check_connection", + "disconnect", + "invalidate_profile", + "list_scopes", + "cancel_scope_reads", + "default_scope", + "select_scope", + ]), + )) + .expect("desktop build configuration is invalid"); +} diff --git a/desktop/src-tauri/capabilities/main.json b/desktop/src-tauri/capabilities/main.json new file mode 100644 index 000000000..89e1f1db5 --- /dev/null +++ b/desktop/src-tauri/capabilities/main.json @@ -0,0 +1,26 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main", + "description": "Only the packaged main window can use typed connection, Scope and local diagnostic commands.", + "windows": [ + "main" + ], + "permissions": [ + "allow-foundation-info", + "allow-local-diagnostics", + "allow-remember-memory", + "allow-search-memory", + "allow-memory-entry", + "allow-cancel-memory-reads", + "allow-desktop-state", + "allow-save-profile", + "allow-remove-profile", + "allow-check-connection", + "allow-disconnect", + "allow-invalidate-profile", + "allow-list-scopes", + "allow-cancel-scope-reads", + "allow-default-scope", + "allow-select-scope" + ] +} diff --git a/desktop/src-tauri/examples/credential_probe.rs b/desktop/src-tauri/examples/credential_probe.rs new file mode 100644 index 000000000..9ce34a24d --- /dev/null +++ b/desktop/src-tauri/examples/credential_probe.rs @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Opt-in native test. Stores only a synthetic marker under a unique test ID, then removes it. +use powercontext_desktop::credentials::{CredentialId, Secret, Vault, WindowsVault}; +fn main() { + let args: Vec = std::env::args().collect(); + if args.get(1).is_some_and(|s| s == "read") { + let id = CredentialId::new(args[2].clone()).unwrap(); + // Loading a valid secret after process restart proves it remained in the OS vault. + WindowsVault + .read(&id) + .expect("native credential read failed"); + return; + } + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let name = format!("s1-probe-{}-{nonce}", std::process::id()); + let id = CredentialId::new(name.clone()).unwrap(); + WindowsVault + .put( + &id, + &Secret::new("synthetic-s1-credential-only".into()).unwrap(), + ) + .expect("native credential write failed"); + let result = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["read", &name]) + .status(); + let cleanup = WindowsVault.delete(&id); + cleanup.expect("test credential cleanup failed"); + assert!(result.unwrap().success(), "child read failed"); + assert!( + WindowsVault.read(&id).is_err(), + "test credential was not removed" + ); + println!("PASS: Windows vault write, new-process read, delete; no secret returned."); +} diff --git a/desktop/src-tauri/examples/diagnostic_cli_probe.rs b/desktop/src-tauri/examples/diagnostic_cli_probe.rs new file mode 100644 index 000000000..d84899578 --- /dev/null +++ b/desktop/src-tauri/examples/diagnostic_cli_probe.rs @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#[cfg(windows)] +#[tokio::main] +async fn main() { + use powercontext_desktop::diagnostics::{DiagnosticKind, LocalDiagnostics}; + let path = std::env::args_os() + .nth(1) + .expect("isolated fixture registration"); + let diagnostics = LocalDiagnostics::new(path.into()); + let service = diagnostics + .run(DiagnosticKind::Service) + .await + .expect("real service status projection"); + let integrations = diagnostics + .run(DiagnosticKind::Integrations) + .await + .expect("real integrations projection"); + assert!( + service + .items + .iter() + .any(|v| v.field == "registration" && v.status == "not_installed") + ); + assert_eq!(integrations.hosts.len(), 8); + println!( + "{}", + serde_json::json!({"service":service,"integrations":integrations}) + ); +} +#[cfg(not(windows))] +fn main() { + panic!("Windows qualification required"); +} diff --git a/desktop/src-tauri/examples/diagnostic_process_probe.rs b/desktop/src-tauri/examples/diagnostic_process_probe.rs new file mode 100644 index 000000000..2ea4c53e3 --- /dev/null +++ b/desktop/src-tauri/examples/diagnostic_process_probe.rs @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Explicit native subprocess-budget probe. Never registered as a product IPC command. +#[cfg(windows)] +use powercontext_desktop::{diagnostics, error}; +#[cfg(windows)] +#[path = "../src/diagnostic_process.rs"] +mod diagnostic_process; +#[cfg(windows)] +fn main() { + use std::os::windows::process::CommandExt; + use std::{ + io::Write, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, + }; + match std::env::args().nth(1).as_deref() { + Some("exitone") => { + println!("{{\"status\":\"failed\"}}"); + std::process::exit(1); + } + Some("tree") => { + // Intentionally outlive this root to verify the native Job cleans descendants. + #[allow(clippy::zombie_processes)] + let child = std::process::Command::new(std::env::current_exe().unwrap()) + .arg("wait") + .creation_flags(0x08000000) + .spawn() + .unwrap(); + println!("{}", child.id()); + return; + } + Some("wait") => { + std::thread::sleep(Duration::from_secs(30)); + return; + } + Some("overflow") => { + let output = vec![b'x'; diagnostics::OUTPUT_LIMIT + 1]; + let _ = std::io::stdout().write_all(&output); + std::thread::sleep(Duration::from_secs(30)); + return; + } + _ => {} + } + let executable = std::env::current_exe().unwrap(); + struct Sentinel(std::process::Child); + impl Drop for Sentinel { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } + let mut sentinel = Sentinel( + std::process::Command::new(&executable) + .arg("wait") + .creation_flags(0x08000000) + .spawn() + .unwrap(), + ); + let invoke = + |arg, timeout, cancel| diagnostic_process::run(&executable, &[arg], timeout, cancel); + let result = invoke( + "exitone", + Duration::from_secs(5), + Arc::new(AtomicBool::new(false)), + ) + .unwrap(); + assert_eq!(result.exit_code, 1); + assert!(String::from_utf8(result.stdout).unwrap().contains("failed")); + assert!(matches!( + invoke( + "wait", + Duration::from_millis(200), + Arc::new(AtomicBool::new(false)) + ), + Err(error::SafeError::Timeout) + )); + assert!(matches!( + invoke( + "overflow", + Duration::from_secs(5), + Arc::new(AtomicBool::new(false)) + ), + Err(error::SafeError::ResponseTooLarge) + )); + let cancelled = Arc::new(AtomicBool::new(false)); + let flag = cancelled.clone(); + let trigger = std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(200)); + flag.store(true, Ordering::Relaxed); + }); + assert!(matches!( + invoke("wait", Duration::from_secs(5), cancelled), + Err(error::SafeError::StaleContext) + )); + trigger.join().unwrap(); + let started = std::time::Instant::now(); + let tree = invoke( + "tree", + Duration::from_secs(5), + Arc::new(AtomicBool::new(false)), + ) + .unwrap(); + let child_pid: u32 = std::str::from_utf8(&tree.stdout) + .unwrap() + .trim() + .parse() + .unwrap(); + assert!(started.elapsed() < Duration::from_secs(5)); + unsafe { + use windows_sys::Win32::{Foundation::*, System::Threading::*}; + let child = OpenProcess(PROCESS_SYNCHRONIZE, 0, child_pid); + if child.is_null() { + assert_eq!(GetLastError(), ERROR_INVALID_PARAMETER); + } else { + let status = WaitForSingleObject(child, 1000); + CloseHandle(child); + assert_eq!(status, WAIT_OBJECT_0); + } + } + assert!( + sentinel.0.try_wait().unwrap().is_none(), + "unrelated process must survive" + ); + println!( + "PASS: valid nonzero output, bounded output, timeout, cancellation, descendant cleanup, unrelated process preserved" + ); +} +#[cfg(not(windows))] +fn main() { + panic!("Windows qualification required"); +} diff --git a/desktop/src-tauri/examples/export_ipc.rs b/desktop/src-tauri/examples/export_ipc.rs new file mode 100644 index 000000000..9bbc3dca3 --- /dev/null +++ b/desktop/src-tauri/examples/export_ipc.rs @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + connections::{ + profiles::{Authentication, CredentialState, ProfileInput, ProfileView}, + session::{ + ActiveView, CheckReport, CompatibilityProfile, DesktopState, Fact, MemoryContext, + WriteOutcome, WriteRecord, WriteStatus, + }, + }, + credentials::{CredentialWriteReceipt, CredentialWriteRequest, StorageChoice}, + diagnostics::{DiagnosticItem, DiagnosticKind, DiagnosticReport, HostDiagnostic}, + error::SafeError, + ipc::FoundationInfo, + transport::{ApiFailure, wire}, +}; +use ts_rs::TS; +fn main() { + let config = ts_rs::Config::default().with_large_int("number"); + let license = include_str!("export_ipc.rs") + .split(" */") + .next() + .unwrap() + .to_owned() + + " */\n\n"; + let mut output = license + + &format!( + "// Generated from Rust IPC types. Do not edit.\nexport {}\nexport {}\nexport {}\nexport {}\nexport {}\n", + SafeError::decl(&config), + FoundationInfo::decl(&config), + StorageChoice::decl(&config), + CredentialWriteRequest::decl(&config), + CredentialWriteReceipt::decl(&config) + ); + for declaration in [ + DiagnosticKind::decl(&config), + DiagnosticItem::decl(&config), + HostDiagnostic::decl(&config), + DiagnosticReport::decl(&config), + Authentication::decl(&config), + ProfileView::decl(&config), + ProfileInput::decl(&config), + CredentialState::decl(&config), + Fact::::decl(&config), + CheckReport::decl(&config), + CompatibilityProfile::decl(&config), + ActiveView::decl(&config), + MemoryContext::decl(&config), + WriteStatus::decl(&config), + WriteRecord::decl(&config), + WriteOutcome::decl(&config), + DesktopState::decl(&config), + ApiFailure::decl(&config), + ] + .into_iter() + .chain(wire::declarations(&config)) + { + output.push_str(&format!("export {declaration}\n")); + } + let output = output + .lines() + .map(str::trim_end) + .collect::>() + .join("\n") + + "\n"; + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../ui/src/generated/ipc.ts"); + if std::env::args().any(|arg| arg == "--check") { + assert_eq!( + std::fs::read_to_string(path).unwrap().replace("\r\n", "\n"), + output, + "IPC drift: run cargo run --example export_ipc" + ); + } else { + std::fs::write(path, output).unwrap(); + } +} diff --git a/desktop/src-tauri/examples/server_probe.rs b/desktop/src-tauri/examples/server_probe.rs new file mode 100644 index 000000000..30341bdd7 --- /dev/null +++ b/desktop/src-tauri/examples/server_probe.rs @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Only invoked by the isolated real-Server harness; never registered as an IPC command. +use powercontext_desktop::{ + connections::{ + profiles::ProfileRepository, + session::{ConnectionManager, WriteStatus}, + }, + credentials::{Secret, WindowsVault}, + error::SafeError, + transport::{Endpoint, ServerApi}, +}; +use serde::Deserialize; +#[derive(Deserialize)] +struct Fixture { + response_loss_path: Option, + identity_change_path: Option, + endpoint: String, + scope_id: String, + token: Option, + ca_pem: Option, +} +#[tokio::main] +async fn main() { + let args: Vec = std::env::args().collect(); + if args.get(1).is_some_and(|a| a == "certificates") { + use rcgen::{ + BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, + KeyUsagePurpose, + }; + let dir = std::path::Path::new(&args[2]); + let mut ca = CertificateParams::new(vec![]).unwrap(); + ca.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca.distinguished_name + .push(rcgen::DnType::CommonName, "Desktop test CA"); + ca.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; + let ca_key = KeyPair::generate().unwrap(); + let ca_cert = ca.self_signed(&ca_key).unwrap(); + let issuer = Issuer::new(ca, ca_key); + let mut leaf = + CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()]).unwrap(); + leaf.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + let key = KeyPair::generate().unwrap(); + let cert = leaf.signed_by(&key, &issuer).unwrap(); + std::fs::write(dir.join("ca.pem"), ca_cert.pem()).unwrap(); + std::fs::write(dir.join("server.pem"), cert.pem()).unwrap(); + std::fs::write(dir.join("server-key.pem"), key.serialize_pem()).unwrap(); + return; + } + let mut fixture: Fixture = serde_json::from_slice(&std::fs::read(&args[1]).unwrap()).unwrap(); + let has_token = fixture.token.is_some(); + let endpoint = Endpoint::parse(&fixture.endpoint).unwrap(); + let api = ServerApi::new( + endpoint.clone(), + fixture.ca_pem.as_deref().map(str::as_bytes), + fixture.token.take(), + ) + .unwrap(); + api.live().await.unwrap(); + api.readiness().await.unwrap(); + if has_token { + api.principal().await.unwrap(); + } else { + assert_eq!( + api.principal().await.err().unwrap().code, + SafeError::RuntimeNotReady + ); + assert_eq!( + api.readiness() + .await + .unwrap() + .checks + .get("access_mode") + .map(String::as_str), + Some("disabled") + ); + } + let capabilities = api.capabilities().await.unwrap(); + assert!(capabilities.artifact_families.iter().any(|v| v == "memory")); + api.default_scope().await.unwrap(); + let scopes = api.scopes("", None).await.unwrap(); + assert!(!scopes.items.is_empty()); + assert_eq!( + api.scope(&fixture.scope_id).await.unwrap().scope_id, + fixture.scope_id + ); + let keyword = format!("desktop{}", uuid::Uuid::new_v4().simple()); + let text = format!("{keyword} 中文记录 café\nSecond line."); + let saved = api.remember(&fixture.scope_id, &text).await.unwrap(); + let entry = saved + .entry + .expect("unique synthetic note must produce an exact entry"); + let results = api.search(&fixture.scope_id, &keyword).await.unwrap(); + assert!( + results + .hits + .iter() + .any(|hit| hit.citation == entry.citation) + ); + let exact = api.entry(&fixture.scope_id, &entry.citation).await.unwrap(); + assert_eq!(exact.text, text); + // Exercise the same native context owner used by product IPC, not only bare HTTP adapters. + let temporary = tempfile::tempdir().unwrap(); + let manager = ConnectionManager::new( + ProfileRepository::open( + temporary.path().join("profiles.json"), + std::sync::Arc::new(WindowsVault), + ) + .unwrap(), + ); + let raw: serde_json::Value = serde_json::from_slice(&std::fs::read(&args[1]).unwrap()).unwrap(); + let compatibility = manager.state().unwrap().compatibility_profiles[0] + .id + .clone(); + let credential = raw["token"] + .as_str() + .map(|value| serde_json::json!({"secret":value,"storage":"session_only"})); + let profile = manager.save_profile(serde_json::from_value(serde_json::json!({ + "id":null,"revision":null,"name":"Synthetic integration", "endpoint":fixture.endpoint, + "authentication":if has_token { "bearer" } else { "unauthenticated_loopback" }, + "caPem":fixture.ca_pem,"compatibility":compatibility,"keepCredential":false,"credential":credential + })).unwrap()).unwrap().profiles[0].id.clone(); + let generation = manager.check(&profile, true).await.unwrap().generation; + let generation = manager + .select_scope(generation, &fixture.scope_id) + .await + .unwrap() + .generation; + let keyword = format!("context{}", uuid::Uuid::new_v4().simple()); + let submitted = format!(" {keyword} 中文 café\nSecond line. "); + let expected = format!("{keyword} 中文 café\nSecond line."); + let saved = manager.remember(generation, &submitted).await.unwrap(); + assert_eq!(saved.record.status, WriteStatus::Succeeded); + let entry = saved.result.unwrap().entry.unwrap(); + assert_eq!(entry.text, expected); + let matches = manager.search_memory(generation, &keyword).await.unwrap(); + assert!( + matches + .hits + .iter() + .any(|hit| hit.citation == entry.citation) + ); + let exact = manager + .memory_entry(generation, &entry.citation) + .await + .unwrap(); + assert_eq!(exact.text, expected); + assert_eq!(matches.hits.len(), 1); + assert!( + manager + .search_memory(generation, "absentuniquefixtureword") + .await + .unwrap() + .hits + .is_empty() + ); + let batch = format!("batch{}", uuid::Uuid::new_v4().simple()); + let mut save_ms = vec![]; + for index in 0..11 { + let start = std::time::Instant::now(); + let result = manager + .remember(generation, &format!("{batch} synthetic note {index}")) + .await + .unwrap(); + save_ms.push(start.elapsed().as_secs_f64() * 1000.0); + assert_eq!(result.record.status, WriteStatus::Succeeded); + } + let mut search_ms = vec![]; + let mut exact_ms = vec![]; + for _ in 0..20 { + let start = std::time::Instant::now(); + let matches = manager.search_memory(generation, &batch).await.unwrap(); + search_ms.push(start.elapsed().as_secs_f64() * 1000.0); + assert_eq!(matches.hits.len(), 10); + let start = std::time::Instant::now(); + let exact = manager + .memory_entry(generation, &entry.citation) + .await + .unwrap(); + exact_ms.push(start.elapsed().as_secs_f64() * 1000.0); + assert_eq!(exact.text, expected); + } + if let Some(path) = &fixture.response_loss_path { + std::fs::write(path, b"0").unwrap(); + let keyword = format!("lostresponse{}", uuid::Uuid::new_v4().simple()); + let text = format!("{keyword} committed synthetic note"); + let outcome = manager.remember(generation, &text).await.unwrap(); + assert_eq!(outcome.record.status, WriteStatus::Unknown); + assert!(outcome.result.is_none()); + let results = manager.search_memory(generation, &keyword).await.unwrap(); + assert_eq!(results.hits.len(), 1); + let committed = manager + .memory_entry(generation, &results.hits[0].citation) + .await + .unwrap(); + assert_eq!(committed.text, text); + assert_eq!(std::fs::read_to_string(path).unwrap(), "1"); + } + if let Some(reader_token) = raw["reader_token"].as_str() { + verify_revocation(&fixture, &raw, reader_token, &entry.citation, &expected).await; + verify_scope_pages(&fixture, &raw, &manager, generation).await; + } + if let Some(path) = fixture.identity_change_path { + // Test-only out-of-band provider control, not a product endpoint or IPC command. + std::fs::write(path, b"change").unwrap(); + assert_eq!( + manager + .memory_entry(generation, &entry.citation) + .await + .err() + .unwrap() + .code, + SafeError::StaleContext + ); + let state = manager.state().unwrap(); + assert!(state.active.is_none()); + assert!(state.generation > generation); + assert_eq!( + api.entry(&fixture.scope_id, &entry.citation) + .await + .err() + .unwrap() + .code, + SafeError::Forbidden + ); + } + manager.disconnect().unwrap(); + api.live().await.unwrap(); + if has_token { + let bad = ServerApi::new( + endpoint, + fixture.ca_pem.as_deref().map(str::as_bytes), + Some(Secret::new("wrong-synthetic-token".into()).unwrap()), + ) + .unwrap(); + assert_eq!( + bad.principal().await.err().unwrap().code, + SafeError::Unauthorized + ); + } + println!( + "{}", + serde_json::json!({ + "result":"passed", + "performance": { + "scope":"Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "noteCount":13, + "save": distribution(save_ms), "search": distribution(search_ms), "exactRead": distribution(exact_ms) + } + }) + ); +} + +fn distribution(mut values: Vec) -> serde_json::Value { + values.sort_by(f64::total_cmp); + let percentile = |p: f64| values[(values.len() as f64 * p).ceil() as usize - 1]; + serde_json::json!({"samplesMs":values, "count":values.len(), "p50Ms":percentile(0.5), "p95Ms":percentile(0.95)}) +} + +async fn verify_revocation( + fixture: &Fixture, + raw: &serde_json::Value, + reader_token: &str, + citation: &powercontext_desktop::transport::wire::MemoryCitation, + expected: &str, +) { + // Only the isolated test administrator mutates fixture policy; never exposed to Desktop IPC. + let client = reqwest::Client::builder().no_proxy().build().unwrap(); + let admin = raw["token"].as_str().unwrap(); + let binding: serde_json::Value = client + .post(format!("{}/v1/access/bindings/create", fixture.endpoint)) + .bearer_auth(admin) + .json(&serde_json::json!({ + "subject":{"type":"user","id":"desktop-fixture-reader"}, + "resource":{"type":"scope","scope_id":fixture.scope_id}, + "role":"scope.viewer","idempotency_key":"desktop-reader-grant" + })) + .send() + .await + .unwrap() + .error_for_status() + .unwrap() + .json() + .await + .unwrap(); + let reader = ServerApi::new( + Endpoint::parse(&fixture.endpoint).unwrap(), + None, + Some(Secret::new(reader_token.into()).unwrap()), + ) + .unwrap(); + let principal = reader.principal().await.unwrap(); + assert_eq!( + reader + .entry(&fixture.scope_id, citation) + .await + .unwrap() + .text, + expected + ); + client + .post(format!("{}/v1/access/bindings/revoke", fixture.endpoint)) + .bearer_auth(admin) + .json(&serde_json::json!({ + "binding_id":binding["binding_id"],"expected_version":binding["version"], + "idempotency_key":"desktop-reader-revoke" + })) + .send() + .await + .unwrap() + .error_for_status() + .unwrap(); + assert_eq!(reader.principal().await.unwrap(), principal); + assert_eq!( + reader + .entry(&fixture.scope_id, citation) + .await + .err() + .unwrap() + .code, + SafeError::Forbidden + ); +} + +async fn verify_scope_pages( + fixture: &Fixture, + raw: &serde_json::Value, + manager: &ConnectionManager, + generation: u32, +) { + let client = reqwest::Client::builder().no_proxy().build().unwrap(); + let title = format!("paging{}", uuid::Uuid::new_v4().simple()); + let mut created = std::collections::BTreeSet::new(); + for index in 0..51 { + let scope: serde_json::Value = client + .post(format!("{}/v1/scopes", fixture.endpoint)) + .bearer_auth(raw["token"].as_str().unwrap()) + .json(&serde_json::json!({ + "title":title,"summary":"Synthetic same-title pagination fixture", + "idempotency_key":format!("{title}-{index}") + })) + .send() + .await + .unwrap() + .error_for_status() + .unwrap() + .json() + .await + .unwrap(); + assert!(created.insert(scope["scope_id"].as_str().unwrap().to_owned())); + } + let first = manager.scopes(generation, &title, None).await.unwrap(); + assert_eq!(first.items.len(), 50); + let second = manager + .scopes(generation, &title, first.next_cursor.as_deref()) + .await + .unwrap(); + assert!(first.next_cursor.is_some()); + assert_eq!(second.items.len(), 1); + assert!(second.next_cursor.is_none()); + let found: std::collections::BTreeSet<_> = first + .items + .iter() + .chain(second.items.iter()) + .map(|scope| scope.scope_id.clone()) + .collect(); + assert_eq!(found, created); + // Exact lookups distinguish the same display name without modifying the active memory Scope. + let api = ServerApi::new( + Endpoint::parse(&fixture.endpoint).unwrap(), + None, + Some(Secret::new(raw["token"].as_str().unwrap().into()).unwrap()), + ) + .unwrap(); + for id in [created.first().unwrap(), created.last().unwrap()] { + assert_eq!(&api.scope(id).await.unwrap().scope_id, id); + } + assert_eq!( + manager + .state() + .unwrap() + .active + .unwrap() + .scope + .unwrap() + .scope_id, + fixture.scope_id + ); +} diff --git a/desktop/src-tauri/icons/brand.png b/desktop/src-tauri/icons/brand.png new file mode 100644 index 000000000..e2ac6be44 Binary files /dev/null and b/desktop/src-tauri/icons/brand.png differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 000000000..0b81bf075 Binary files /dev/null and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 000000000..8481e3910 Binary files /dev/null and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/src/commands.rs b/desktop/src-tauri/src/commands.rs new file mode 100644 index 000000000..d0b87222a --- /dev/null +++ b/desktop/src-tauri/src/commands.rs @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::{ + connections::{ + profiles::ProfileInput, + session::{ConnectionManager, DesktopState}, + }, + error::SafeError, + ipc::authorize_window, + transport::{ + ApiFailure, + wire::{ScopeDescriptor, ScopePage}, + }, +}; +pub struct HostState { + pub manager: Result, +} +fn manager<'a, R: tauri::Runtime>( + window: &tauri::WebviewWindow, + state: &'a HostState, +) -> Result<&'a ConnectionManager, ApiFailure> { + authorize_window(window.label())?; + state.manager.as_ref().map_err(|e| ApiFailure::before(*e)) +} + +#[tauri::command] +pub fn desktop_state( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, +) -> Result { + manager(&window, &state)?.state().map_err(Into::into) +} + +#[tauri::command] +pub fn save_profile( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + input: ProfileInput, +) -> Result { + manager(&window, &state)? + .save_profile(input) + .map_err(Into::into) +} + +#[tauri::command] +pub fn remove_profile( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + id: String, + revision: u32, +) -> Result { + manager(&window, &state)? + .remove_profile(&id, revision) + .map_err(Into::into) +} + +#[tauri::command] +pub async fn check_connection( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + id: String, + activate: bool, +) -> Result { + manager(&window, &state)?.check(&id, activate).await +} + +#[tauri::command] +pub fn disconnect( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, +) -> Result { + manager(&window, &state)?.disconnect().map_err(Into::into) +} + +#[tauri::command] +pub fn invalidate_profile( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + id: String, +) -> Result { + manager(&window, &state)? + .invalidate_profile(&id) + .map_err(Into::into) +} + +#[tauri::command] +pub async fn list_scopes( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, + query: String, + cursor: Option, +) -> Result { + manager(&window, &state)? + .scopes(generation, &query, cursor.as_deref()) + .await +} + +#[tauri::command] +pub async fn default_scope( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, +) -> Result { + manager(&window, &state)?.default_scope(generation).await +} + +#[tauri::command] +pub async fn select_scope( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, + id: String, +) -> Result { + manager(&window, &state)? + .select_scope(generation, &id) + .await +} + +#[tauri::command] +pub fn cancel_scope_reads( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, +) -> Result<(), ApiFailure> { + manager(&window, &state)? + .cancel_scope_reads(generation) + .map_err(Into::into) +} + +#[tauri::command] +pub async fn local_diagnostics( + window: tauri::WebviewWindow, + state: tauri::State<'_, crate::diagnostics::DiagnosticHost>, + kind: crate::diagnostics::DiagnosticKind, +) -> Result { + authorize_window(window.label())?; + #[cfg(windows)] + { + state + .local + .as_ref() + .ok_or(SafeError::NotFound)? + .run(kind) + .await + } + #[cfg(not(windows))] + { + let _ = (state, kind); + Err(SafeError::NotFound) + } +} + +#[tauri::command] +pub async fn remember_memory( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, + text: String, +) -> Result { + manager(&window, &state)?.remember(generation, &text).await +} +#[tauri::command] +pub async fn search_memory( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, + query: String, +) -> Result { + manager(&window, &state)? + .search_memory(generation, &query) + .await +} +#[tauri::command] +pub async fn memory_entry( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, + citation: crate::transport::wire::MemoryCitation, +) -> Result { + manager(&window, &state)? + .memory_entry(generation, &citation) + .await +} +#[tauri::command] +pub fn cancel_memory_reads( + window: tauri::WebviewWindow, + state: tauri::State<'_, HostState>, + generation: u32, +) -> Result<(), ApiFailure> { + manager(&window, &state)? + .cancel_memory_reads(generation) + .map_err(Into::into) +} diff --git a/desktop/src-tauri/src/connections/compatibility.json b/desktop/src-tauri/src/connections/compatibility.json new file mode 100644 index 000000000..32b72621f --- /dev/null +++ b/desktop/src-tauri/src/connections/compatibility.json @@ -0,0 +1,21 @@ +[ + { + "id": "sqlite-1.1.1-v1", + "serverCommit": "1e90376a18e80437a74f2f55141d4301dc7f89c4", + "contractSha256": "915866dd26fd2bdce6f987a52fed7febaa859eaf506412765e7c847ea530a6ee", + "artifactSha256": "163973110ce96e4332d0304796efa2f98d45e3efca3964e66b9c8ebcdc8f8658", + "operations": [ + "get_liveness", + "get_readiness", + "get_access_principal", + "get_capabilities", + "list_scopes", + "get_scope", + "get_default_scope", + "remember_memory", + "search_memory", + "get_memory_entry" + ], + "evidence": "desktop/VALIDATION.md" + } +] diff --git a/desktop/src-tauri/src/connections/mod.rs b/desktop/src-tauri/src/connections/mod.rs new file mode 100644 index 000000000..35b2fc01f --- /dev/null +++ b/desktop/src-tauri/src/connections/mod.rs @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod profiles; +pub mod session; diff --git a/desktop/src-tauri/src/connections/profiles.rs b/desktop/src-tauri/src/connections/profiles.rs new file mode 100644 index 000000000..7e73c7afe --- /dev/null +++ b/desktop/src-tauri/src/connections/profiles.rs @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Atomic, bounded profile storage. Only references to our own vault entries are persisted. +use crate::{ + credentials::{Credential, CredentialId, CredentialWriteRequest, Vault}, + error::SafeError, + transport::Endpoint, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, io::Write, path::PathBuf, sync::Arc}; +use ts_rs::TS; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum Authentication { + UnauthenticatedLoopback, + Bearer, +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct StoredProfile { + id: String, + revision: u32, + name: String, + endpoint: String, + authentication: Authentication, + ca_pem: Option, + compatibility: Option, + credential_ref: Option, +} +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Document { + schema: u32, + profiles: Vec, + pending_deletions: Vec, +} +impl Default for Document { + fn default() -> Self { + Self { + schema: 1, + profiles: vec![], + pending_deletions: vec![], + } + } +} + +#[derive(Clone, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct ProfileView { + pub id: String, + pub revision: u32, + pub name: String, + pub endpoint: String, + pub authentication: Authentication, + pub ca_pem: Option, + pub compatibility: Option, + pub credential_state: CredentialState, +} +#[derive(Clone, Copy, Serialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum CredentialState { + NotRequired, + Stored, + SessionOnly, + Missing, +} + +// A request never implements Serialize/Debug because it can temporarily carry a secret. +#[derive(Deserialize, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ProfileInput { + pub id: Option, + pub revision: Option, + pub name: String, + pub endpoint: String, + pub authentication: Authentication, + pub ca_pem: Option, + pub compatibility: Option, + pub keep_credential: bool, + pub credential: Option, +} + +pub struct ProfileRepository { + path: PathBuf, + document: Document, + session: BTreeMap>, + vault: Arc, +} +impl ProfileRepository { + pub fn open(path: PathBuf, vault: Arc) -> Result { + let document = if path.exists() { + let metadata = path.metadata().map_err(|_| SafeError::Storage)?; + if metadata.len() > 1024 * 1024 { + return Err(SafeError::ProfileCorrupt); + } + let bytes = std::fs::read(&path).map_err(|_| SafeError::Storage)?; + serde_json::from_slice::(&bytes).map_err(|_| SafeError::ProfileCorrupt)? + } else { + Document::default() + }; + if document.schema != 1 + || document.profiles.len() > 20 + || document.pending_deletions.len() > 100 + { + return Err(SafeError::ProfileCorrupt); + } + let mut ids = std::collections::BTreeSet::new(); + let mut names = std::collections::BTreeSet::new(); + let mut refs = std::collections::BTreeSet::new(); + for profile in &document.profiles { + if !valid_id(&profile.id) + || profile.revision == 0 + || !ids.insert(&profile.id) + || !names.insert(profile.name.to_lowercase()) + || validate_fields( + &profile.name, + &profile.endpoint, + profile.authentication, + profile.ca_pem.as_deref(), + ) + .is_err() + || profile + .credential_ref + .as_ref() + .is_some_and(|id| !valid_id(id) || !refs.insert(id)) + { + return Err(SafeError::ProfileCorrupt); + } + } + if document + .pending_deletions + .iter() + .any(|id| !valid_id(id) || refs.contains(id)) + { + return Err(SafeError::ProfileCorrupt); + } + let mut result = Self { + path, + document, + session: BTreeMap::new(), + vault, + }; + result.cleanup(); + Ok(result) + } + pub fn views(&self) -> Vec { + self.document + .profiles + .iter() + .map(|p| ProfileView { + id: p.id.clone(), + revision: p.revision, + name: p.name.clone(), + endpoint: p.endpoint.clone(), + authentication: p.authentication, + ca_pem: p.ca_pem.clone(), + compatibility: p.compatibility.clone(), + credential_state: if p.authentication == Authentication::UnauthenticatedLoopback { + CredentialState::NotRequired + } else if p.credential_ref.is_some() { + CredentialState::Stored + } else if self.session.contains_key(&p.id) { + CredentialState::SessionOnly + } else { + CredentialState::Missing + }, + }) + .collect() + } + pub fn pending_cleanup(&self) -> usize { + self.document.pending_deletions.len() + } + pub fn api(&self, id: &str) -> Result { + let p = self + .document + .profiles + .iter() + .find(|p| p.id == id) + .ok_or(SafeError::NotFound)?; + let secret = if p.authentication == Authentication::Bearer { + Some(if let Some(reference) = &p.credential_ref { + self.vault.read(&CredentialId::new(reference.clone())?)? + } else { + self.session + .get(id) + .ok_or(SafeError::CredentialMissing)? + .load(self.vault.as_ref())? + }) + } else { + None + }; + crate::transport::ServerApi::new( + Endpoint::parse(&p.endpoint)?, + p.ca_pem.as_deref().map(str::as_bytes), + secret, + ) + } + pub fn save(&mut self, input: ProfileInput) -> Result { + let endpoint = validate_fields( + &input.name, + &input.endpoint, + input.authentication, + input.ca_pem.as_deref(), + )?; + if input.compatibility.as_ref().is_some_and(|v| v.len() > 128) { + return Err(SafeError::InvalidInput); + } + let old = input + .id + .as_ref() + .map(|id| { + self.document + .profiles + .iter() + .find(|p| &p.id == id) + .cloned() + .ok_or(SafeError::NotFound) + }) + .transpose()?; + if old.as_ref().map(|p| p.revision) != input.revision { + return Err(SafeError::Conflict); + } + if old.is_none() && self.document.profiles.len() >= 20 { + return Err(SafeError::InvalidInput); + } + if self.document.profiles.iter().any(|p| { + Some(&p.id) != input.id.as_ref() + && p.name.to_lowercase() == input.name.trim().to_lowercase() + }) { + return Err(SafeError::DuplicateName); + } + let same_target = old.as_ref().is_some_and(|p| { + p.endpoint == endpoint + && p.ca_pem == input.ca_pem + && p.authentication == input.authentication + }); + if input.keep_credential && (!same_target || input.credential.is_some()) { + return Err(SafeError::InvalidCredential); + } + if input.authentication == Authentication::UnauthenticatedLoopback + && input.credential.is_some() + { + return Err(SafeError::InvalidCredential); + } + let id = input.id.unwrap_or_else(new_id); + let revision = old + .as_ref() + .map_or(Some(1), |p| p.revision.checked_add(1)) + .ok_or(SafeError::Storage)?; + let mut reference = if input.keep_credential { + old.as_ref().and_then(|p| p.credential_ref.clone()) + } else { + None + }; + let mut new_credential = None; + if let Some(request) = input.credential { + let credential_id = new_id(); + // Persist cleanup intent before touching the OS vault, including crash/failure paths. + if request.storage_choice() == crate::credentials::StorageChoice::Persistent { + let mut journal = self.document.clone(); + journal.pending_deletions.push(credential_id.clone()); + self.persist(&journal)?; + self.document = journal; + } + let stored = request.store( + self.vault.as_ref(), + CredentialId::new(credential_id.clone())?, + ); + match stored { + Ok((credential, _)) => { + if matches!(credential, Credential::Persistent(_)) { + reference = Some(credential_id); + } + new_credential = Some(Arc::new(credential)); + } + Err(error) => { + self.cleanup(); + return Err(error); + } + } + } + let profile = StoredProfile { + id: id.clone(), + revision, + name: input.name.trim().into(), + endpoint, + authentication: input.authentication, + ca_pem: input.ca_pem, + compatibility: input.compatibility, + credential_ref: reference, + }; + let mut next = self.document.clone(); + next.profiles.retain(|p| p.id != id); + if let Some(old_ref) = old.and_then(|p| p.credential_ref) + && Some(&old_ref) != profile.credential_ref.as_ref() + { + next.pending_deletions.push(old_ref); + } + next.pending_deletions + .retain(|r| Some(r) != profile.credential_ref.as_ref()); + next.profiles.push(profile); + if let Err(error) = self.persist(&next) { + self.cleanup(); + return Err(error); + } + self.document = next; + if !input.keep_credential { + self.session.remove(&id); + } + if let Some(credential) = new_credential + && matches!(*credential, Credential::SessionOnly(_)) + { + self.session.insert(id.clone(), credential); + } + self.cleanup(); + self.views() + .into_iter() + .find(|p| p.id == id) + .ok_or(SafeError::Storage) + } + pub fn remove(&mut self, id: &str, revision: u32) -> Result<(), SafeError> { + let old = self + .document + .profiles + .iter() + .find(|p| p.id == id) + .ok_or(SafeError::NotFound)?; + if old.revision != revision { + return Err(SafeError::Conflict); + } + let mut next = self.document.clone(); + if let Some(reference) = &old.credential_ref { + next.pending_deletions.push(reference.clone()); + } + next.profiles.retain(|p| p.id != id); + self.persist(&next)?; + self.document = next; + self.session.remove(id); + self.cleanup(); + Ok(()) + } + fn persist(&self, document: &Document) -> Result<(), SafeError> { + if document.pending_deletions.len() > 100 { + return Err(SafeError::CredentialUnavailable); + } + let parent = self.path.parent().ok_or(SafeError::Storage)?; + std::fs::create_dir_all(parent).map_err(|_| SafeError::Storage)?; + let mut file = tempfile::NamedTempFile::new_in(parent).map_err(|_| SafeError::Storage)?; + let bytes = serde_json::to_vec_pretty(document).map_err(|_| SafeError::Storage)?; + if bytes.len() > 1024 * 1024 { + return Err(SafeError::InvalidInput); + } + file.write_all(&bytes) + .and_then(|_| file.as_file().sync_all()) + .map_err(|_| SafeError::Storage)?; + file.persist(&self.path).map_err(|_| SafeError::Storage)?; + Ok(()) + } + fn cleanup(&mut self) { + let mut next = self.document.clone(); + next.pending_deletions.retain(|id| { + CredentialId::new(id.clone()) + .and_then(|id| self.vault.delete(&id)) + .is_err() + }); + if next.pending_deletions != self.document.pending_deletions && self.persist(&next).is_ok() + { + self.document = next; + } + } +} +fn new_id() -> String { + format!("desktop-{}", uuid::Uuid::new_v4()) +} +fn valid_id(id: &str) -> bool { + id.strip_prefix("desktop-") + .is_some_and(|id| uuid::Uuid::parse_str(id).is_ok()) +} +fn validate_fields( + name: &str, + endpoint: &str, + authentication: Authentication, + ca: Option<&str>, +) -> Result { + if name.trim().is_empty() || name.chars().count() > 128 || name.chars().any(char::is_control) { + return Err(SafeError::InvalidInput); + } + let endpoint = Endpoint::parse(endpoint)?; + if authentication == Authentication::UnauthenticatedLoopback && !endpoint.is_loopback() { + return Err(SafeError::InvalidCredential); + } + crate::transport::Transport::new(ca.map(str::as_bytes))?; + Ok(endpoint.as_str().into()) +} diff --git a/desktop/src-tauri/src/connections/session.rs b/desktop/src-tauri/src/connections/session.rs new file mode 100644 index 000000000..58e1ea2fb --- /dev/null +++ b/desktop/src-tauri/src/connections/session.rs @@ -0,0 +1,680 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Native-owned connection and Scope context. Viewing a profile never activates it. +use super::profiles::{ProfileInput, ProfileRepository, ProfileView}; +use crate::{ + error::SafeError, + transport::{ApiFailure, ServerApi, wire::*}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::BTreeMap, + future::Future, + sync::{Arc, Mutex}, +}; +use tokio::sync::watch; +use ts_rs::TS; + +#[derive(Clone, Serialize, TS)] +pub struct Fact { + pub value: Option, + pub error: Option, +} +impl From> for Fact { + fn from(result: Result) -> Self { + match result { + Ok(value) => Self { + value: Some(value), + error: None, + }, + Err(error) => Self { + value: None, + error: Some(error), + }, + } + } +} +#[derive(Clone, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct CheckReport { + pub connection_id: String, + pub revision: u32, + pub checked_at: u64, + pub liveness: Fact, + pub readiness: Fact, + pub identity: Fact, + pub capabilities: Fact, + pub compatibility_verified: bool, + pub anonymous_access: bool, + pub supported_operations: Vec, +} +#[derive(Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CompatibilityProfile { + pub id: String, + pub server_commit: String, + pub contract_sha256: String, + pub artifact_sha256: String, + pub operations: Vec, + pub evidence: String, +} +#[derive(Clone, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct ActiveView { + pub connection_id: String, + pub generation: u32, + pub report: CheckReport, + pub scope: Option, +} +#[derive(Clone, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct DesktopState { + pub generation: u32, + pub profiles: Vec, + pub reports: Vec, + pub active: Option, + pub compatibility_profiles: Vec, + pub pending_credential_cleanup: usize, + pub last_write: Option, +} +#[derive(Clone, Debug, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct MemoryContext { + pub connection_id: String, + pub endpoint: String, + pub principal: Option, + pub generation: u32, + pub scope_id: String, +} +#[derive(Clone, Debug, PartialEq, Serialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum WriteStatus { + Pending, + Succeeded, + Failed, + Unknown, +} +#[derive(Clone, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct WriteRecord { + pub operation_id: String, + pub context: MemoryContext, + pub status: WriteStatus, + pub citation: Option, + pub error: Option, +} +#[derive(Clone, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct WriteOutcome { + pub record: WriteRecord, + pub result: Option, +} +struct WriteGuard<'a> { + manager: &'a ConnectionManager, + armed: bool, +} +impl Drop for WriteGuard<'_> { + fn drop(&mut self) { + if self.armed + && let Ok(mut inner) = self.manager.lock() + && let Some(record) = &mut inner.last_write + { + record.status = WriteStatus::Unknown; + } + } +} +struct ReadSnapshot { + api: Arc, + identity: Option, + cancelled: watch::Receiver, +} +struct Active { + view: ActiveView, + api: Arc, +} +struct Inner { + profiles: ProfileRepository, + reports: BTreeMap, + active: Option, + generation: u32, + last_write: Option, +} +pub struct ConnectionManager { + inner: Mutex, + changed: watch::Sender, + scope_reads: watch::Sender, + compatibility: Vec, + probes: tokio::sync::Semaphore, + writes: tokio::sync::Semaphore, + memory_reads: watch::Sender, +} +impl ConnectionManager { + pub fn new(profiles: ProfileRepository) -> Self { + let (changed, _) = watch::channel(0); + // No configuration becomes qualified merely because a health probe returned 200. + let compatibility = serde_json::from_str(include_str!("compatibility.json")) + .expect("bundled compatibility manifest"); + Self { + inner: Mutex::new(Inner { + profiles, + reports: BTreeMap::new(), + active: None, + generation: 0, + last_write: None, + }), + changed, + scope_reads: watch::channel(0).0, + compatibility, + probes: tokio::sync::Semaphore::new(1), + writes: tokio::sync::Semaphore::new(1), + memory_reads: watch::channel(0).0, + } + } + fn lock(&self) -> Result, SafeError> { + self.inner.lock().map_err(|_| SafeError::Storage) + } + fn invalidate(&self, inner: &mut Inner) -> Result<(), SafeError> { + inner.generation = inner.generation.checked_add(1).ok_or(SafeError::Storage)?; + self.changed.send_replace(inner.generation); + if let Some(active) = &mut inner.active { + active.view.generation = inner.generation; + } + Ok(()) + } + pub fn state(&self) -> Result { + let inner = self.lock()?; + Ok(DesktopState { + generation: inner.generation, + profiles: inner.profiles.views(), + reports: inner.reports.values().cloned().collect(), + active: inner.active.as_ref().map(|a| a.view.clone()), + compatibility_profiles: self.compatibility.clone(), + pending_credential_cleanup: inner.profiles.pending_cleanup(), + last_write: inner.last_write.clone(), + }) + } + pub fn save_profile(&self, input: ProfileInput) -> Result { + let mut inner = self.lock()?; + // Editing an active connection immediately drops its authorized context, including failed edits. + if input.id.as_ref().is_some_and(|id| { + inner + .active + .as_ref() + .is_some_and(|a| &a.view.connection_id == id) + }) { + inner.active = None; + } + self.invalidate(&mut inner)?; + if let Some(id) = &input.id { + inner.reports.remove(id); + } + inner.profiles.save(input)?; + drop(inner); + self.state() + } + pub fn remove_profile(&self, id: &str, revision: u32) -> Result { + let mut inner = self.lock()?; + inner.profiles.remove(id, revision)?; + if inner + .active + .as_ref() + .is_some_and(|a| a.view.connection_id == id) + { + inner.active = None; + } + inner.reports.remove(id); + self.invalidate(&mut inner)?; + drop(inner); + self.state() + } + pub fn disconnect(&self) -> Result { + let mut inner = self.lock()?; + inner.active = None; + self.invalidate(&mut inner)?; + drop(inner); + self.state() + } + pub fn invalidate_profile(&self, id: &str) -> Result { + let mut inner = self.lock()?; + inner.reports.remove(id); + if inner + .active + .as_ref() + .is_some_and(|a| a.view.connection_id == id) + { + inner.active = None; + } + self.invalidate(&mut inner)?; + drop(inner); + self.state() + } + pub async fn check(&self, id: &str, activate: bool) -> Result { + let _permit = self.probes.try_acquire().map_err(|_| SafeError::Busy)?; + let (api, profile, generation, mut cancelled, use_as_active) = { + let mut inner = self.lock()?; + let profile = inner + .profiles + .views() + .into_iter() + .find(|p| p.id == id) + .ok_or(SafeError::NotFound)?; + let use_as_active = activate + || inner + .active + .as_ref() + .is_some_and(|a| a.view.connection_id == id); + if use_as_active { + inner.active = None; + self.invalidate(&mut inner)?; + } + ( + Arc::new(inner.profiles.api(id)?), + profile, + inner.generation, + self.changed.subscribe(), + use_as_active, + ) + }; + let report = Self::cancellable(&mut cancelled, async { + let (live, ready, identity, capabilities) = tokio::join!( + api.live(), + api.readiness(), + api.principal(), + api.capabilities() + ); + let anonymous_access = profile.authentication + == super::profiles::Authentication::UnauthenticatedLoopback + && ready + .as_ref() + .is_ok_and(|r| r.checks.get("access_mode").is_some_and(|v| v == "disabled")) + && identity + .as_ref() + .is_err_and(|e| e.code == SafeError::RuntimeNotReady); + let contract: serde_json::Value = + serde_json::from_str(include_str!("../transport/operations.json")) + .map_err(|_| SafeError::InvalidResponse)?; + let compatible = self.compatibility.iter().find(|c| { + profile.compatibility.as_ref() == Some(&c.id) + && contract["contractSha256"].as_str() == Some(&c.contract_sha256) + && !identity + .as_ref() + .is_err_and(|e| e.code == SafeError::InvalidResponse) + && !capabilities + .as_ref() + .is_err_and(|e| e.code == SafeError::InvalidResponse) + }); + Ok(CheckReport { + anonymous_access, + connection_id: id.into(), + revision: profile.revision, + checked_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + liveness: live.into(), + readiness: ready.into(), + identity: identity.into(), + capabilities: capabilities.into(), + compatibility_verified: compatible.is_some(), + supported_operations: compatible.map(|c| c.operations.clone()).unwrap_or_default(), + }) + }) + .await?; + let mut inner = self.lock()?; + if inner.generation != generation { + return Err(SafeError::StaleContext.into()); + } + inner.reports.insert(id.into(), report.clone()); + // Activation can expose diagnostic facts while business operations remain independently gated. + if use_as_active { + inner.active = Some(Active { + view: ActiveView { + connection_id: id.into(), + generation, + report, + scope: None, + }, + api, + }); + } + drop(inner); + self.state().map_err(Into::into) + } + fn snapshot(&self, generation: u32, operation: &str) -> Result { + let inner = self.lock()?; + let active = inner.active.as_ref().ok_or(SafeError::NotConnected)?; + if generation != inner.generation { + return Err(SafeError::StaleContext); + } + if !active.view.report.compatibility_verified + || !active + .view + .report + .supported_operations + .iter() + .any(|op| op == operation) + { + return Err(SafeError::CompatibilityUnverified); + } + let principal = active.view.report.identity.value.clone(); + if principal.is_none() && !active.view.report.anonymous_access { + return Err(SafeError::Unauthorized); + } + Ok(ReadSnapshot { + api: active.api.clone(), + identity: principal, + cancelled: self.changed.subscribe(), + }) + } + async fn identity_unchanged( + &self, + api: &ServerApi, + original: &Option, + generation: u32, + ) -> Result<(), ApiFailure> { + let current = api.principal().await; + let unchanged = if let Some(original) = original { + current.as_ref().is_ok_and(|value| value == original) + } else { + current + .as_ref() + .is_err_and(|e| e.code == SafeError::RuntimeNotReady) + && api + .readiness() + .await + .is_ok_and(|r| r.checks.get("access_mode").is_some_and(|v| v == "disabled")) + }; + if unchanged { + return Ok(()); + } + let mut inner = self.lock()?; + if inner.generation == generation { + if let Some(active) = inner.active.take() { + inner.reports.remove(&active.view.connection_id); + } + self.invalidate(&mut inner)?; + } + match current { + Err(error) => Err(error), + Ok(_) => Err(SafeError::StaleContext.into()), + } + } + fn begin_scope_query(&self, generation: u32) -> Result, SafeError> { + let inner = self.lock()?; + if generation != inner.generation { + return Err(SafeError::StaleContext); + } + if inner.active.is_none() { + return Err(SafeError::NotConnected); + } + let next = self + .scope_reads + .borrow() + .checked_add(1) + .ok_or(SafeError::Storage)?; + self.scope_reads.send_replace(next); + Ok(self.scope_reads.subscribe()) + } + pub fn cancel_scope_reads(&self, generation: u32) -> Result<(), SafeError> { + self.begin_scope_query(generation).map(|_| ()) + } + pub async fn scopes( + &self, + generation: u32, + query: &str, + cursor: Option<&str>, + ) -> Result { + let ReadSnapshot { + api, + identity, + mut cancelled, + } = self.snapshot(generation, "list_scopes")?; + let mut query_cancelled = self.begin_scope_query(generation)?; + Self::cancellable( + &mut cancelled, + Self::cancellable(&mut query_cancelled, async { + self.identity_unchanged(&api, &identity, generation).await?; + api.scopes(query, cursor).await + }), + ) + .await + } + pub async fn default_scope(&self, generation: u32) -> Result { + let ReadSnapshot { + api, + identity, + mut cancelled, + } = self.snapshot(generation, "get_default_scope")?; + let mut query_cancelled = self.begin_scope_query(generation)?; + Self::cancellable( + &mut cancelled, + Self::cancellable(&mut query_cancelled, async { + self.identity_unchanged(&api, &identity, generation).await?; + api.default_scope().await + }), + ) + .await + } + pub async fn select_scope( + &self, + generation: u32, + id: &str, + ) -> Result { + let ReadSnapshot { + api, + identity, + mut cancelled, + } = self.snapshot(generation, "get_scope")?; + let scope = Self::cancellable(&mut cancelled, async { + self.identity_unchanged(&api, &identity, generation).await?; + api.scope(id).await + }) + .await?; + let mut inner = self.lock()?; + if generation != inner.generation { + return Err(SafeError::StaleContext.into()); + } + self.invalidate(&mut inner)?; + inner + .active + .as_mut() + .ok_or(SafeError::NotConnected)? + .view + .scope = Some(scope); + drop(inner); + self.state().map_err(Into::into) + } + fn memory_snapshot( + &self, + generation: u32, + operation: &str, + ) -> Result<(ReadSnapshot, MemoryContext), SafeError> { + let snapshot = self.snapshot(generation, operation)?; + let inner = self.lock()?; + if inner.generation != generation { + return Err(SafeError::StaleContext); + } + let active = inner.active.as_ref().ok_or(SafeError::NotConnected)?; + let scope = active.view.scope.as_ref().ok_or(SafeError::ScopeRequired)?; + let endpoint = inner + .profiles + .views() + .into_iter() + .find(|profile| profile.id == active.view.connection_id) + .ok_or(SafeError::NotConnected)? + .endpoint; + let principal = snapshot + .identity + .as_ref() + .map(|value| value.principal.clone()); + Ok(( + snapshot, + MemoryContext { + endpoint, + principal, + connection_id: active.view.connection_id.clone(), + generation, + scope_id: scope.scope_id.clone(), + }, + )) + } + fn begin_memory_read(&self, generation: u32) -> Result, SafeError> { + let inner = self.lock()?; + if inner.generation != generation { + return Err(SafeError::StaleContext); + } + let next = self + .memory_reads + .borrow() + .checked_add(1) + .ok_or(SafeError::Storage)?; + self.memory_reads.send_replace(next); + Ok(self.memory_reads.subscribe()) + } + pub fn cancel_memory_reads(&self, generation: u32) -> Result<(), SafeError> { + self.begin_memory_read(generation).map(|_| ()) + } + pub async fn search_memory( + &self, + generation: u32, + query: &str, + ) -> Result { + let ( + ReadSnapshot { + api, + identity, + mut cancelled, + }, + context, + ) = self.memory_snapshot(generation, "search_memory")?; + let mut query_cancelled = self.begin_memory_read(generation)?; + Self::cancellable( + &mut cancelled, + Self::cancellable(&mut query_cancelled, async { + self.identity_unchanged(&api, &identity, generation).await?; + api.search(&context.scope_id, query).await + }), + ) + .await + } + pub async fn memory_entry( + &self, + generation: u32, + citation: &MemoryCitation, + ) -> Result { + let ( + ReadSnapshot { + api, + identity, + mut cancelled, + }, + context, + ) = self.memory_snapshot(generation, "get_memory_entry")?; + let mut query_cancelled = self.begin_memory_read(generation)?; + Self::cancellable( + &mut cancelled, + Self::cancellable(&mut query_cancelled, async { + self.identity_unchanged(&api, &identity, generation).await?; + api.entry(&context.scope_id, citation).await + }), + ) + .await + } + pub async fn remember(&self, generation: u32, text: &str) -> Result { + let _permit = self.writes.try_acquire().map_err(|_| SafeError::Busy)?; + crate::transport::validate_text(text)?; + let ( + ReadSnapshot { + api, + identity, + mut cancelled, + }, + context, + ) = self.memory_snapshot(generation, "remember_memory")?; + Self::cancellable( + &mut cancelled, + self.identity_unchanged(&api, &identity, generation), + ) + .await?; + let mut record = WriteRecord { + operation_id: uuid::Uuid::new_v4().to_string(), + context, + status: WriteStatus::Pending, + citation: None, + error: None, + }; + { + let mut inner = self.lock()?; + if inner.generation != generation { + return Err(SafeError::StaleContext.into()); + } + inner.last_write = Some(record.clone()); + } + // No context cancellation after this point: the immutable API and Scope own the dispatched write. + let mut guard = WriteGuard { + manager: self, + armed: true, + }; + let response = api.remember(&record.context.scope_id, text).await; + let result = match response { + Ok(value) => { + record.status = WriteStatus::Succeeded; + record.citation = value.entry.as_ref().map(|e| e.citation.clone()); + Some(value) + } + Err(error) => { + record.status = if error.dispatched + && !matches!( + error.code, + SafeError::Unauthorized + | SafeError::Forbidden + | SafeError::InvalidInput + | SafeError::NotFound + | SafeError::Conflict + | SafeError::AuthenticationUnavailable + | SafeError::RuntimeNotReady + | SafeError::Redirect + ) { + WriteStatus::Unknown + } else { + WriteStatus::Failed + }; + record.error = Some(error); + None + } + }; + let mut inner = self.lock()?; + inner.last_write = Some(record.clone()); + guard.armed = false; + // A late response may report its original target, but may not disclose old body text in a new context. + let result = if inner.generation == generation { + result + } else { + None + }; + Ok(WriteOutcome { record, result }) + } + async fn cancellable( + cancelled: &mut watch::Receiver, + work: impl Future>, + ) -> Result { + tokio::select! { biased; + _ = cancelled.changed() => Err(SafeError::StaleContext.into()), + value = work => value, + } + } +} diff --git a/desktop/src-tauri/src/credentials.rs b/desktop/src-tauri/src/credentials.rs new file mode 100644 index 000000000..e8225511b --- /dev/null +++ b/desktop/src-tauri/src/credentials.rs @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Native-only secrets. No secret-bearing type implements Serialize or Debug. +use crate::error::SafeError; +use serde::{Deserialize, Deserializer, Serialize}; +use ts_rs::TS; +use zeroize::Zeroizing; + +const SERVICE: &str = "com.powercontext.desktop.preview"; +pub struct Secret(Zeroizing); +impl Secret { + pub fn new(value: String) -> Result { + let value = Zeroizing::new(value); + if value.is_empty() || value.len() > 2048 || !value.bytes().all(|c| c.is_ascii_graphic()) { + return Err(SafeError::InvalidCredential); + } + Ok(Self(value)) + } + pub(crate) fn expose(&self) -> &str { + &self.0 + } +} + +impl<'de> Deserialize<'de> for Secret { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + Self::new(value).map_err(|_| serde::de::Error::custom("invalid credential")) + } +} + +/// IDs must be native-generated opaque identifiers, never endpoints or account names. +pub struct CredentialId(String); +impl CredentialId { + pub fn as_str(&self) -> &str { + &self.0 + } + pub fn new(id: String) -> Result { + if id.is_empty() + || id.len() > 80 + || !id.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-') + { + return Err(SafeError::InvalidCredential); + } + Ok(Self(id)) + } +} + +pub trait Vault: Send + Sync { + fn put(&self, id: &CredentialId, secret: &Secret) -> Result<(), SafeError>; + fn read(&self, id: &CredentialId) -> Result; + fn delete(&self, id: &CredentialId) -> Result<(), SafeError>; +} + +pub struct WindowsVault; +#[cfg(windows)] +impl WindowsVault { + fn entry(id: &CredentialId) -> Result { + keyring::Entry::new(SERVICE, &id.0).map_err(|_| SafeError::CredentialUnavailable) + } +} +#[cfg(windows)] +impl Vault for WindowsVault { + fn put(&self, id: &CredentialId, secret: &Secret) -> Result<(), SafeError> { + Self::entry(id)? + .set_password(secret.expose()) + .map_err(|_| SafeError::CredentialUnavailable) + } + fn read(&self, id: &CredentialId) -> Result { + let value = Self::entry(id)?.get_password().map_err(|e| match e { + keyring::Error::NoEntry => SafeError::CredentialMissing, + _ => SafeError::CredentialUnavailable, + })?; + Secret::new(value) + } + fn delete(&self, id: &CredentialId) -> Result<(), SafeError> { + match Self::entry(id)?.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(_) => Err(SafeError::CredentialUnavailable), + } + } +} +#[cfg(not(windows))] +impl Vault for WindowsVault { + fn put(&self, _: &CredentialId, _: &Secret) -> Result<(), SafeError> { + Err(SafeError::CredentialUnavailable) + } + fn read(&self, _: &CredentialId) -> Result { + Err(SafeError::CredentialUnavailable) + } + fn delete(&self, _: &CredentialId) -> Result<(), SafeError> { + Err(SafeError::CredentialUnavailable) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum StorageChoice { + Persistent, + SessionOnly, +} +/// Renderer-to-native input only. There is deliberately no Serialize or Debug implementation. +/// S2 must resolve the native-owned connection ID before consuming this request. +#[derive(Deserialize, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CredentialWriteRequest { + #[ts(type = "string")] + secret: Secret, + storage: StorageChoice, +} + +/// The entire successful write response; neither a secret nor its vault identifier is exposed. +#[derive(Serialize, TS)] +pub struct CredentialWriteReceipt { + storage: StorageChoice, +} + +impl CredentialWriteRequest { + pub fn storage_choice(&self) -> StorageChoice { + self.storage + } + pub fn store( + self, + vault: &(impl Vault + ?Sized), + native_id: CredentialId, + ) -> Result<(Credential, CredentialWriteReceipt), SafeError> { + let credential = Credential::store(vault, native_id, self.secret, self.storage)?; + Ok(( + credential, + CredentialWriteReceipt { + storage: self.storage, + }, + )) + } +} + +pub enum Credential { + Persistent(CredentialId), + SessionOnly(Secret), +} +impl Credential { + /// Failure is returned to the caller; session storage requires a separate explicit choice. + pub fn store( + vault: &(impl Vault + ?Sized), + id: CredentialId, + secret: Secret, + choice: StorageChoice, + ) -> Result { + match choice { + StorageChoice::Persistent => { + vault.put(&id, &secret)?; + Ok(Self::Persistent(id)) + } + StorageChoice::SessionOnly => Ok(Self::SessionOnly(secret)), + } + } + pub fn load(&self, vault: &(impl Vault + ?Sized)) -> Result { + match self { + Self::Persistent(id) => vault.read(id), + Self::SessionOnly(secret) => Secret::new(secret.expose().to_owned()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + struct Unavailable; + impl Vault for Unavailable { + fn put(&self, _: &CredentialId, _: &Secret) -> Result<(), SafeError> { + Err(SafeError::CredentialUnavailable) + } + fn read(&self, _: &CredentialId) -> Result { + Err(SafeError::CredentialUnavailable) + } + fn delete(&self, _: &CredentialId) -> Result<(), SafeError> { + Err(SafeError::CredentialUnavailable) + } + } + #[test] + fn write_protocol_requires_explicit_storage_and_returns_only_metadata() { + for input in [ + r#"{"secret":"synthetic"}"#, + r#"{"secret":"synthetic","storage":"automatic"}"#, + r#"{"secret":"synthetic","storage":"session_only","nativeId":"chosen"}"#, + ] { + assert!(serde_json::from_str::(input).is_err()); + } + let invalid = serde_json::from_str::( + r#"{"secret":"private value","storage":"persistent"}"#, + ); + let error = invalid.err().unwrap().to_string(); + assert!(!error.contains("private value")); + let persistent: CredentialWriteRequest = + serde_json::from_str(r#"{"secret":"synthetic","storage":"persistent"}"#).unwrap(); + assert!(matches!( + persistent.store(&Unavailable, CredentialId::new("native".into()).unwrap()), + Err(SafeError::CredentialUnavailable) + )); + let session: CredentialWriteRequest = + serde_json::from_str(r#"{"secret":"synthetic","storage":"session_only"}"#).unwrap(); + let (credential, receipt) = session + .store(&Unavailable, CredentialId::new("native".into()).unwrap()) + .unwrap(); + assert_eq!(credential.load(&Unavailable).unwrap().expose(), "synthetic"); + assert_eq!( + serde_json::to_value(receipt).unwrap(), + serde_json::json!({"storage":"session_only"}) + ); + } + + #[test] + fn unavailable_vault_never_silently_downgrades() { + let result = Credential::store( + &Unavailable, + CredentialId::new("test".into()).unwrap(), + Secret::new("synthetic".into()).unwrap(), + StorageChoice::Persistent, + ); + assert!(matches!(result, Err(SafeError::CredentialUnavailable))); + let session = Credential::store( + &Unavailable, + CredentialId::new("test".into()).unwrap(), + Secret::new("synthetic".into()).unwrap(), + StorageChoice::SessionOnly, + ) + .unwrap(); + assert_eq!(session.load(&Unavailable).unwrap().expose(), "synthetic"); + } +} diff --git a/desktop/src-tauri/src/diagnostic_process.rs b/desktop/src-tauri/src/diagnostic_process.rs new file mode 100644 index 000000000..b9721c4da --- /dev/null +++ b/desktop/src-tauri/src/diagnostic_process.rs @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Windows helper containment: create suspended, assign to an owned job, then resume. +//! Dropping the job terminates only this invocation and its descendants. +#![cfg(windows)] +use crate::{diagnostics::OUTPUT_LIMIT, error::SafeError}; +use std::{ + fs::File, + io::Read, + os::windows::{ffi::OsStrExt, io::FromRawHandle}, + path::Path, + sync::{ + Arc, + atomic::{AtomicBool, AtomicUsize, Ordering}, + }, + time::{Duration, Instant}, +}; +use windows_sys::Win32::{ + Foundation::*, + Security::SECURITY_ATTRIBUTES, + System::{JobObjects::*, Pipes::CreatePipe, Threading::*}, +}; +struct OwnedHandle(HANDLE); +impl Drop for OwnedHandle { + fn drop(&mut self) { + unsafe { + CloseHandle(self.0); + } + } +} +fn wide(value: &std::ffi::OsStr) -> Vec { + value.encode_wide().chain(Some(0)).collect() +} +fn pipe() -> Result<(File, OwnedHandle), SafeError> { + let mut read = std::ptr::null_mut(); + let mut write = std::ptr::null_mut(); + let security = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: std::ptr::null_mut(), + bInheritHandle: 1, + }; + unsafe { + if CreatePipe(&mut read, &mut write, &security, 0) == 0 { + return Err(SafeError::Storage); + } + let reader = File::from_raw_handle(read); + let writer = OwnedHandle(write); + if SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0) == 0 { + return Err(SafeError::Storage); + } + Ok((reader, writer)) + } +} +fn collect( + mut file: File, + total: Arc, + exceeded: Arc, +) -> std::thread::JoinHandle, SafeError>> { + std::thread::spawn(move || { + let mut output = Vec::new(); + let mut bytes = [0; 4096]; + loop { + let size = file + .read(&mut bytes) + .map_err(|_| SafeError::InvalidResponse)?; + if size == 0 { + return Ok(output); + } + if total.fetch_add(size, Ordering::Relaxed) + size > OUTPUT_LIMIT { + exceeded.store(true, Ordering::Relaxed); + return Err(SafeError::ResponseTooLarge); + } + output.extend_from_slice(&bytes[..size]); + } + }) +} +pub struct ProcessOutput { + pub stdout: Vec, + pub exit_code: i32, +} +/// Call only after native executable provenance checks. Arguments must come from the fixed command enum. +pub(crate) fn run( + path: &Path, + args: &[&str], + deadline: Duration, + cancelled: Arc, +) -> Result { + if !path.is_absolute() + || path.as_os_str().to_string_lossy().contains('"') + || args + .iter() + .any(|arg| !arg.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-')) + { + return Err(SafeError::InvalidInput); + } + let application = wide(path.as_os_str()); + let mut command = wide(std::ffi::OsStr::new(&format!( + "\"{}\" {}", + path.display(), + args.join(" ") + ))); + let directory = wide(std::env::temp_dir().as_os_str()); + let (stdout, out_write) = pipe()?; + let (stderr, err_write) = pipe()?; + unsafe { + let job = OwnedHandle(CreateJobObjectW(std::ptr::null(), std::ptr::null())); + if job.0.is_null() { + return Err(SafeError::Storage); + } + let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if SetInformationJobObject( + job.0, + JobObjectExtendedLimitInformation, + &limits as *const _ as *const _, + std::mem::size_of_val(&limits) as u32, + ) == 0 + { + return Err(SafeError::Storage); + } + let mut startup: STARTUPINFOW = std::mem::zeroed(); + startup.cb = std::mem::size_of_val(&startup) as u32; + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdOutput = out_write.0; + startup.hStdError = err_write.0; + let mut info: PROCESS_INFORMATION = std::mem::zeroed(); + if CreateProcessW( + application.as_ptr(), + command.as_mut_ptr(), + std::ptr::null(), + std::ptr::null(), + 1, + CREATE_SUSPENDED | CREATE_NO_WINDOW, + std::ptr::null(), + directory.as_ptr(), + &startup, + &mut info, + ) == 0 + { + return Err(SafeError::NotFound); + } + let process = OwnedHandle(info.hProcess); + let thread = OwnedHandle(info.hThread); + if AssignProcessToJobObject(job.0, process.0) == 0 { + TerminateProcess(process.0, 1); + WaitForSingleObject(process.0, 5000); + return Err(SafeError::Storage); + } + drop(out_write); + drop(err_write); + let total = Arc::new(AtomicUsize::new(0)); + let exceeded = Arc::new(AtomicBool::new(false)); + let out = collect(stdout, total.clone(), exceeded.clone()); + let err = collect(stderr, total, exceeded.clone()); + let started = Instant::now(); + let result = if ResumeThread(thread.0) == u32::MAX { + Err(SafeError::Storage) + } else { + loop { + if cancelled.load(Ordering::Relaxed) { + break Err(SafeError::StaleContext); + } + if exceeded.load(Ordering::Relaxed) { + break Err(SafeError::ResponseTooLarge); + } + if started.elapsed() >= deadline { + break Err(SafeError::Timeout); + } + match WaitForSingleObject(process.0, 20) { + WAIT_OBJECT_0 => { + let mut code = 0; + if GetExitCodeProcess(process.0, &mut code) == 0 { + break Err(SafeError::Storage); + } + break Ok(code as i32); + } + WAIT_TIMEOUT => {} + _ => break Err(SafeError::Storage), + } + } + }; + drop(job); + WaitForSingleObject(process.0, 5000); + let stdout = out.join().map_err(|_| SafeError::Storage)?; + let stderr = err.join().map_err(|_| SafeError::Storage)?; + let exit_code = result?; + let stdout = stdout?; + stderr?; + Ok(ProcessOutput { stdout, exit_code }) + } +} diff --git a/desktop/src-tauri/src/diagnostics.rs b/desktop/src-tauri/src/diagnostics.rs new file mode 100644 index 000000000..d16327e30 --- /dev/null +++ b/desktop/src-tauri/src/diagnostics.rs @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Allowlisted projections of local CLI diagnostics; raw output never crosses IPC. +use crate::error::SafeError; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use ts_rs::TS; + +pub const OUTPUT_LIMIT: usize = 256 * 1024; +#[derive(Clone, Copy, Deserialize, Serialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum DiagnosticKind { + Service, + Integrations, +} +impl DiagnosticKind { + pub fn arguments(self) -> [&'static str; 3] { + match self { + Self::Service => ["service", "status", "--json"], + Self::Integrations => ["doctor", "integrations", "--json"], + } + } +} +#[derive(Clone, Debug, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticItem { + // Both strings are selected from the static vocabulary below, never arbitrary CLI text. + pub field: String, + pub status: String, +} +#[derive(Clone, Debug, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct HostDiagnostic { + pub host: String, + pub presence: String, + pub checks: Vec, +} +#[derive(Clone, Debug, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticReport { + pub checked_at: u64, + pub exit_code: i32, + pub items: Vec, + pub hosts: Vec, +} +fn selected(value: &Value, allowed: &[&str]) -> Result { + let value = value.as_str().ok_or(SafeError::InvalidResponse)?; + allowed + .contains(&value) + .then(|| value.to_owned()) + .ok_or(SafeError::InvalidResponse) +} +/// Exit 1 can carry valid, useful unhealthy diagnostics. Process failure and malformed JSON remain distinct. +pub fn project( + kind: DiagnosticKind, + output: &[u8], + exit_code: i32, +) -> Result { + if output.len() > OUTPUT_LIMIT { + return Err(SafeError::ResponseTooLarge); + } + let value: Value = serde_json::from_slice(output).map_err(|_| SafeError::InvalidResponse)?; + if !value.is_object() { + return Err(SafeError::InvalidResponse); + } + let mut report = DiagnosticReport { + checked_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + exit_code, + items: vec![], + hosts: vec![], + }; + match kind { + DiagnosticKind::Service => { + for (field, allowed) in [ + ("support", &["supported", "unsupported"][..]), + ( + "registration", + &["installed", "not_installed", "invalid", "unknown"][..], + ), + ( + "definition", + &["current", "stale", "missing_executable", "unknown"][..], + ), + ( + "manager_ownership", + &["not_loaded", "owned", "foreign", "unknown"][..], + ), + ("manager", &["active", "inactive", "failed", "unknown"][..]), + ("server_liveness", &["live", "unreachable", "unknown"][..]), + ] { + report.items.push(DiagnosticItem { + field: field.into(), + status: selected(&value[field], allowed)?, + }); + } + } + DiagnosticKind::Integrations => { + let status = selected(&value["status"], &["ok", "failed"])?; + if value["ok"].as_bool() != Some(status == "ok") { + return Err(SafeError::InvalidResponse); + } + report.items.push(DiagnosticItem { + field: "integrations".into(), + status, + }); + let hosts = value["hosts"] + .as_object() + .ok_or(SafeError::InvalidResponse)?; + if hosts.len() > 8 { + return Err(SafeError::InvalidResponse); + } + for (name, host) in hosts { + if ![ + "codex", + "claude-code", + "dsh", + "openclaw", + "opencode", + "pi", + "hermes", + "workbuddy", + ] + .contains(&name.as_str()) + { + return Err(SafeError::InvalidResponse); + } + let mut row = HostDiagnostic { + host: name.clone(), + presence: selected(&host["presence"], &["present", "missing"])?, + checks: vec![], + }; + let fields = host.as_object().ok_or(SafeError::InvalidResponse)?; + for (field, check) in fields { + if field == "presence" { + continue; + } + if ![ + "codex", + "claude_code", + "dsh", + "openclaw", + "opencode", + "pi", + "hermes", + "hooks", + "plugin", + "package", + "skill", + "settings", + "mcp", + "transport", + ] + .contains(&field.as_str()) + { + return Err(SafeError::InvalidResponse); + } + let status = + selected(&check["status"], &["ok", "degraded", "failed", "skipped"])?; + if check["ok"].as_bool() != Some(status == "ok") { + return Err(SafeError::InvalidResponse); + } + row.checks.push(DiagnosticItem { + field: field.clone(), + status, + }); + } + if row.checks.is_empty() { + return Err(SafeError::InvalidResponse); + } + report.hosts.push(row); + } + } + } + Ok(report) +} + +#[cfg(windows)] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CliRegistration { + executable: std::path::PathBuf, + sha256: String, + version: String, + source: String, +} +/// Native-only registration. A renderer cannot supply a program, path, arguments, or environment. +#[cfg(windows)] +pub struct LocalDiagnostics { + registration: std::path::PathBuf, + running: std::sync::Arc, +} +#[cfg(windows)] +impl LocalDiagnostics { + pub fn new(registration: std::path::PathBuf) -> Self { + Self { + registration, + running: std::sync::Arc::new(tokio::sync::Semaphore::new(1)), + } + } + pub async fn run(&self, kind: DiagnosticKind) -> Result { + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + let permit = self + .running + .clone() + .try_acquire_owned() + .map_err(|_| SafeError::Busy)?; + struct Cancel(Arc); + impl Drop for Cancel { + fn drop(&mut self) { + self.0.store(true, Ordering::Relaxed); + } + } + let cancelled = Cancel(Arc::new(AtomicBool::new(false))); + let flag = cancelled.0.clone(); + let registration = self.registration.clone(); + tauri::async_runtime::spawn_blocking(move || { + let _permit = permit; + execute(®istration, kind, flag) + }) + .await + .map_err(|_| SafeError::Storage)? + } +} +#[cfg(windows)] +fn execute( + registration: &std::path::Path, + kind: DiagnosticKind, + cancelled: std::sync::Arc, +) -> Result { + use sha2::{Digest, Sha256}; + use std::{io::Read, os::windows::fs::OpenOptionsExt, time::Duration}; + let file = std::fs::File::open(registration).map_err(|_| SafeError::NotFound)?; + let mut bytes = Vec::new(); + file.take(16385) + .read_to_end(&mut bytes) + .map_err(|_| SafeError::Storage)?; + if bytes.len() > 16384 { + return Err(SafeError::InvalidInput); + } + let config: CliRegistration = + serde_json::from_slice(&bytes).map_err(|_| SafeError::InvalidInput)?; + // This is an explicit local-installation pin, not publisher-signature verification. + if config.source != "explicit_local_installation" + || !((config.version == "1.0.1" || config.version.starts_with("1.0.1.dev")) + || (config.version == "1.1.1" || config.version.starts_with("1.1.1.dev"))) + || config.version.len() > 128 + || !config + .version + .bytes() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'+' | b'-')) + || config.sha256.len() != 64 + || !config.sha256.bytes().all(|v| v.is_ascii_hexdigit()) + || !config.executable.is_absolute() + || config.executable.file_name().and_then(|v| v.to_str()) != Some("powercontext.exe") + { + return Err(SafeError::CompatibilityUnverified); + } + // Keep a read-only, no-delete/no-write-sharing handle open through both invocations. + let mut executable = std::fs::OpenOptions::new() + .read(true) + .share_mode(1) + .open(&config.executable) + .map_err(|_| SafeError::NotFound)?; + if executable.metadata().map_err(|_| SafeError::Storage)?.len() > 64 * 1024 * 1024 { + return Err(SafeError::InvalidInput); + } + let mut digest = Sha256::new(); + let mut buf = [0; 8192]; + loop { + let n = executable.read(&mut buf).map_err(|_| SafeError::Storage)?; + if n == 0 { + break; + } + digest.update(&buf[..n]); + } + if format!("{:x}", digest.finalize()) != config.sha256.to_ascii_lowercase() { + return Err(SafeError::CompatibilityUnverified); + } + // A fixed version probe validates the pinned CLI before either diagnostic command runs. + let version = crate::diagnostic_process::run( + &config.executable, + &["--version"], + Duration::from_secs(15), + cancelled.clone(), + )?; + if version.exit_code != 0 + || std::str::from_utf8(&version.stdout).map(str::trim) != Ok(config.version.as_str()) + { + return Err(SafeError::CompatibilityUnverified); + } + let timeout = match kind { + DiagnosticKind::Service => 20, + DiagnosticKind::Integrations => 60, + }; + let result = crate::diagnostic_process::run( + &config.executable, + &kind.arguments(), + Duration::from_secs(timeout), + cancelled, + )?; + project(kind, &result.stdout, result.exit_code) +} + +pub struct DiagnosticHost { + #[cfg(windows)] + pub local: Option, +} +impl DiagnosticHost { + pub fn new(directory: Option) -> Self { + #[cfg(windows)] + { + Self { + local: directory + .map(|path| LocalDiagnostics::new(path.join("diagnostic-cli.json"))), + } + } + #[cfg(not(windows))] + { + let _ = directory; + Self {} + } + } +} diff --git a/desktop/src-tauri/src/error.rs b/desktop/src-tauri/src/error.rs new file mode 100644 index 000000000..d27ce6363 --- /dev/null +++ b/desktop/src-tauri/src/error.rs @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use serde::Serialize; +use ts_rs::TS; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)] +#[serde(rename_all = "snake_case")] +pub enum SafeError { + UnauthorizedWindow, + InvalidEndpoint, + InsecureTransport, + InvalidCertificate, + CredentialUnavailable, + CredentialMissing, + InvalidCredential, + Timeout, + Tls, + Network, + Redirect, + Unauthorized, + Forbidden, + Server, + InvalidResponse, + ResponseTooLarge, + Busy, + InvalidInput, + NotFound, + CursorExpired, + Conflict, + AuthenticationUnavailable, + RuntimeNotReady, + StaleContext, + CompatibilityUnverified, + Storage, + ProfileCorrupt, + DuplicateName, + NotConnected, + ScopeRequired, +} diff --git a/desktop/src-tauri/src/ipc.rs b/desktop/src-tauri/src/ipc.rs new file mode 100644 index 000000000..4c4258ab5 --- /dev/null +++ b/desktop/src-tauri/src/ipc.rs @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::error::SafeError; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct FoundationInfo { + pub version: String, + pub phase: String, + pub credential_backend: String, + pub server_connected: bool, +} + +pub fn authorize_window(label: &str) -> Result<(), SafeError> { + if label == "main" { + Ok(()) + } else { + Err(SafeError::UnauthorizedWindow) + } +} + +#[tauri::command] +pub fn foundation_info( + window: tauri::WebviewWindow, +) -> Result { + authorize_window(window.label())?; + Ok(FoundationInfo { + version: env!("CARGO_PKG_VERSION").into(), + phase: "S3".into(), + credential_backend: if cfg!(windows) { + "windows_credential_manager" + } else { + "unavailable" + } + .into(), + server_connected: false, + }) +} + +pub fn allowed_navigation(url: &url::Url) -> bool { + let packaged = + url.scheme() == "http" && url.host_str() == Some("tauri.localhost") && url.port().is_none(); + let development = + cfg!(debug_assertions) && url.origin().ascii_serialization() == "http://127.0.0.1:1420"; + (packaged || development) && url.username().is_empty() && url.password().is_none() +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 000000000..46bf910d0 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod commands; +pub mod connections; +pub mod credentials; +#[cfg(windows)] +mod diagnostic_process; +pub mod diagnostics; +pub mod error; +pub mod ipc; +pub mod transport; + +use tauri::Manager; + +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + ipc::foundation_info, + commands::local_diagnostics, + commands::remember_memory, + commands::search_memory, + commands::memory_entry, + commands::cancel_memory_reads, + commands::desktop_state, + commands::save_profile, + commands::remove_profile, + commands::check_connection, + commands::disconnect, + commands::invalidate_profile, + commands::list_scopes, + commands::cancel_scope_reads, + commands::default_scope, + commands::select_scope + ]) + .setup(|app| { + let manager = app + .path() + .app_data_dir() + .map_err(|_| error::SafeError::Storage) + .and_then(|dir| { + connections::profiles::ProfileRepository::open( + dir.join("profiles.json"), + std::sync::Arc::new(credentials::WindowsVault), + ) + }) + .map(connections::session::ConnectionManager::new); + app.manage(commands::HostState { manager }); + app.manage(diagnostics::DiagnosticHost::new( + app.path().app_data_dir().ok(), + )); + let config = &app.config().app.windows[0]; + tauri::WebviewWindowBuilder::from_config(app, config)? + .on_navigation(ipc::allowed_navigation) + .build()?; + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("desktop host failed"); +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 000000000..d59c8e5e2 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +fn main() { + powercontext_desktop::run(); +} diff --git a/desktop/src-tauri/src/transport/api.rs b/desktop/src-tauri/src/transport/api.rs new file mode 100644 index 000000000..1ff038d40 --- /dev/null +++ b/desktop/src-tauri/src/transport/api.rs @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Operation-specific adapters. No renderer-supplied route, method or header is accepted. +use super::{Endpoint, MAX_RESPONSE_BYTES, Transport, safe_network_error, wire::*}; +use crate::{credentials::Secret, error::SafeError}; +use reqwest::{ + Method, + header::{AUTHORIZATION, HeaderValue}, +}; +use serde::{Serialize, de::DeserializeOwned}; +use ts_rs::TS; + +#[derive(Clone, Debug, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct ApiFailure { + pub code: SafeError, + pub request_id: Option, + /// Conservatively true once handed to the HTTP client, even if delivery is uncertain. + pub dispatched: bool, +} +impl ApiFailure { + pub fn before(code: SafeError) -> Self { + Self { + code, + request_id: None, + dispatched: false, + } + } +} +impl From for ApiFailure { + fn from(code: SafeError) -> Self { + Self::before(code) + } +} + +pub struct ServerApi { + transport: Transport, + endpoint: Endpoint, + credential: Option, +} +impl ServerApi { + pub fn new( + endpoint: Endpoint, + ca_pem: Option<&[u8]>, + credential: Option, + ) -> Result { + Ok(Self { + transport: Transport::new(ca_pem)?, + endpoint, + credential, + }) + } + async fn execute( + &self, + operation: &str, + scope: Option<&str>, + query: &[(String, String)], + body: Option, + allow_not_ready: bool, + ) -> Result { + let _permit = self + .transport + .slots + .try_acquire() + .map_err(|_| ApiFailure::before(SafeError::Busy))?; + let manifest: serde_json::Value = serde_json::from_str(include_str!("operations.json")) + .map_err(|_| ApiFailure::before(SafeError::InvalidResponse))?; + let descriptor = &manifest["operations"][operation]; + let method = descriptor["method"] + .as_str() + .ok_or(SafeError::InvalidResponse)?; + let method = + Method::from_bytes(method.as_bytes()).map_err(|_| SafeError::InvalidResponse)?; + let mut url = self.endpoint.operation_url(operation)?; + if let Some(id) = scope { + validate_scope(id)?; + // Replace the generated placeholder using URL path-segment encoding, never string interpolation. + if !descriptor["path"] + .as_str() + .is_some_and(|p| p.ends_with("/{scope_id}")) + { + return Err(SafeError::InvalidResponse.into()); + } + url.path_segments_mut() + .map_err(|_| SafeError::InvalidEndpoint)? + .pop() + .push(id); + } + if !query.is_empty() { + url.query_pairs_mut() + .extend_pairs(query.iter().map(|(k, v)| (k.as_str(), v.as_str()))); + } + let mut request = self.transport.client.request(method, url); + if let Some(secret) = &self.credential { + let mut value = HeaderValue::from_str(&format!("Bearer {}", secret.expose())) + .map_err(|_| SafeError::InvalidCredential)?; + value.set_sensitive(true); + request = request.header(AUTHORIZATION, value); + } + if let Some(body) = body { + request = request.json(&body); + } + let request = request.build().map_err(|_| SafeError::InvalidInput)?; + let failure = |code, request_id| ApiFailure { + code, + request_id, + dispatched: true, + }; + let mut response = self + .transport + .client + .execute(request) + .await + .map_err(|e| failure(safe_network_error(e), None))?; + let request_id = response + .headers() + .get("X-PowerContext-Request-ID") + .and_then(|v| v.to_str().ok()) + .filter(|v| { + !v.is_empty() + && v.len() <= 128 + && v.bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') + }) + .map(str::to_owned); + let status = response.status().as_u16(); + if status != 200 && !(allow_not_ready && status == 503) { + let code = match status { + 300..=399 => SafeError::Redirect, + 401 => SafeError::Unauthorized, + 403 => SafeError::Forbidden, + 404 => SafeError::NotFound, + 409 => SafeError::Conflict, + 410 => SafeError::CursorExpired, + 400 | 422 => SafeError::InvalidInput, + 503 => server_error_code(&mut response).await, + 500..=599 => SafeError::Server, + _ => SafeError::InvalidResponse, + }; + return Err(failure(code, request_id)); + } + if response + .content_length() + .is_some_and(|n| n > MAX_RESPONSE_BYTES as u64) + { + return Err(failure(SafeError::ResponseTooLarge, request_id)); + } + let mut bytes = Vec::new(); + while let Some(chunk) = response + .chunk() + .await + .map_err(|e| failure(safe_network_error(e), request_id.clone()))? + { + if bytes.len() + chunk.len() > MAX_RESPONSE_BYTES { + return Err(failure(SafeError::ResponseTooLarge, request_id)); + } + bytes.extend_from_slice(&chunk); + } + serde_json::from_slice(&bytes).map_err(|_| failure(SafeError::InvalidResponse, request_id)) + } + pub async fn live(&self) -> Result { + let result: HealthResponse = self.execute("get_liveness", None, &[], None, false).await?; + if result.status != "ok" { + return Err(SafeError::InvalidResponse.into()); + } + Ok(result) + } + pub async fn readiness(&self) -> Result { + self.execute("get_readiness", None, &[], None, true).await + } + pub async fn principal(&self) -> Result { + let result: AccessMeResponse = self + .execute("get_access_principal", None, &[], None, false) + .await?; + if result.principal.id.is_empty() + || result.principal.id.chars().count() > 255 + || !matches!(result.principal.r#type.as_str(), "user" | "service") + { + return Err(SafeError::InvalidResponse.into()); + } + Ok(result) + } + pub async fn capabilities(&self) -> Result { + self.execute("get_capabilities", None, &[], None, false) + .await + } + pub async fn scopes( + &self, + search: &str, + cursor: Option<&str>, + ) -> Result { + if search.chars().count() > 256 || cursor.is_some_and(|v| v.is_empty() || v.len() > 4096) { + return Err(SafeError::InvalidInput.into()); + } + let mut query = vec![("limit".into(), "50".into())]; + if !search.is_empty() { + query.extend([ + ("query".into(), search.into()), + ("query_field".into(), "title".into()), + ]); + } + if let Some(cursor) = cursor { + query.push(("cursor".into(), cursor.into())); + } + let result: ScopePage = self + .execute("list_scopes", None, &query, None, false) + .await?; + if result.items.len() > 50 + || result + .next_cursor + .as_ref() + .is_some_and(|c| c.is_empty() || c.len() > 4096) + { + return Err(SafeError::InvalidResponse.into()); + } + for scope in &result.items { + validate_scope(&scope.scope_id).map_err(|_| SafeError::InvalidResponse)?; + } + Ok(result) + } + pub async fn scope(&self, id: &str) -> Result { + let result: ScopeDescriptor = self + .execute("get_scope", Some(id), &[], None, false) + .await?; + if result.scope_id != id { + return Err(SafeError::InvalidResponse.into()); + } + Ok(result) + } + pub async fn default_scope(&self) -> Result { + self.execute("get_default_scope", None, &[], None, false) + .await + } + pub async fn remember( + &self, + scope: &str, + text: &str, + ) -> Result { + validate_scope(scope)?; + validate_text(text)?; + let result: MemoryMutationResponse = self + .execute( + "remember_memory", + None, + &[], + Some(serde_json::json!({"scope_id":scope,"kind":"note","text":text})), + false, + ) + .await?; + if !valid_reference(&result.memory) + || result.memory.family != "memory" + || result.entry.as_ref().is_some_and(|entry| { + !valid_entry(entry) || entry.citation.memory_ref != result.memory + }) + { + return Err(invalid_received()); + } + Ok(result) + } + + pub async fn search( + &self, + scope: &str, + query: &str, + ) -> Result { + validate_scope(scope)?; + validate_text(query)?; + let result: SearchMemoryResponse = self + .execute( + "search_memory", + None, + &[], + Some(serde_json::json!({"scope_id":scope,"query":query,"mode":"fts","limit":10})), + false, + ) + .await?; + if result.hits.len() > 10 + || result + .hits + .iter() + .any(|hit| validate_citation(&hit.citation).is_err()) + || result + .mode + .as_ref() + .is_some_and(|mode| *mode != MemoryUsedSearchMode::Fts) + { + return Err(SafeError::InvalidResponse.into()); + } + Ok(result) + } + pub async fn entry( + &self, + scope: &str, + citation: &MemoryCitation, + ) -> Result { + validate_scope(scope)?; + validate_citation(citation)?; + let result: MemoryEntry = self + .execute( + "get_memory_entry", + None, + &[], + Some(serde_json::json!({"scope_id":scope,"citation":citation})), + false, + ) + .await?; + if result.citation != *citation || !valid_entry(&result) { + return Err(SafeError::InvalidResponse.into()); + } + Ok(result) + } +} +pub fn validate_scope(id: &str) -> Result<(), SafeError> { + if id.trim().is_empty() || id.chars().count() > 256 || matches!(id, "." | "..") { + return Err(SafeError::InvalidInput); + } + Ok(()) +} +pub fn validate_text(text: &str) -> Result<(), SafeError> { + // Conservative raw UTF-8 budget; do not alter or truncate user content before the Server normalizes it. + if text.trim().is_empty() || text.len() > 8192 { + return Err(SafeError::InvalidInput); + } + Ok(()) +} +fn validate_citation(c: &MemoryCitation) -> Result<(), SafeError> { + if c.memory_ref.family != "memory" + || !valid_reference(&c.memory_ref) + || [&c.memory_ref.artifact_id, &c.entry_id, &c.entry_version_id] + .iter() + .any(|v| v.is_empty() || v.len() > 128 || !v.bytes().all(|b| b.is_ascii_graphic())) + { + return Err(SafeError::InvalidInput); + } + Ok(()) +} + +fn valid_reference(reference: &ArtifactReference) -> bool { + (1..=9_007_199_254_740_991).contains(&reference.revision) + && !reference.family.is_empty() + && reference.family.len() <= 128 + && !reference.artifact_id.is_empty() + && reference.artifact_id.len() <= 128 + && reference.artifact_id.bytes().all(|b| b.is_ascii_graphic()) +} +fn valid_entry(entry: &MemoryEntry) -> bool { + validate_citation(&entry.citation).is_ok() + && (1..=9_007_199_254_740_991).contains(&entry.version) + && entry.artifact_refs.iter().all(valid_reference) +} +fn invalid_received() -> ApiFailure { + ApiFailure { + code: SafeError::InvalidResponse, + request_id: None, + dispatched: true, + } +} + +async fn server_error_code(response: &mut reqwest::Response) -> SafeError { + // Project only allowlisted machine codes, never error messages or details. + let mut bytes = Vec::new(); + while let Ok(Some(chunk)) = response.chunk().await { + if bytes.len() + chunk.len() > 8192 { + return SafeError::Server; + } + bytes.extend_from_slice(&chunk); + } + match serde_json::from_slice::(&bytes) + .ok() + .as_ref() + .and_then(|v| v.get("error")) + .and_then(|v| v.get("code")) + .and_then(|v| v.as_str()) + { + Some("authentication_unavailable") => SafeError::AuthenticationUnavailable, + Some("runtime_not_ready") => SafeError::RuntimeNotReady, + _ => SafeError::Server, + } +} diff --git a/desktop/src-tauri/src/transport/mod.rs b/desktop/src-tauri/src/transport/mod.rs new file mode 100644 index 000000000..c8c16399f --- /dev/null +++ b/desktop/src-tauri/src/transport/mod.rs @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +mod api; +pub mod wire; +pub use api::{ApiFailure, ServerApi, validate_text}; + +// Only native adapters use this transport; it is not an IPC fetch primitive. +use crate::{credentials::Secret, error::SafeError}; +use reqwest::{ + Certificate, Client, + header::{AUTHORIZATION, HeaderValue}, + redirect::Policy, +}; +use std::{error::Error, sync::Arc, time::Duration}; +use tokio::sync::Semaphore; +use url::{Host, Url}; + +pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(5); +pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(15); +pub const MAX_RESPONSE_BYTES: usize = 1024 * 1024; +pub const MAX_CONCURRENT_READS: usize = 4; + +#[derive(Clone)] +pub struct Endpoint(Url); +impl Endpoint { + pub fn parse(raw: &str) -> Result { + // Reject ambiguous input before WHATWG URL normalization can erase it. + if raw.len() > 2048 + || raw.trim() != raw + || raw.contains(['\\', '?', '#']) + || raw.bytes().any(|c| c.is_ascii_control()) + { + return Err(SafeError::InvalidEndpoint); + } + let scheme_end = raw.find("://").ok_or(SafeError::InvalidEndpoint)? + 3; + let tail = &raw[scheme_end..]; + if tail.split('/').next().unwrap_or("").contains('@') { + return Err(SafeError::InvalidEndpoint); + } + let raw_path = tail.find('/').map(|i| &tail[i..]).unwrap_or(""); + if raw_path.split('/').any(|s| !safe_segment(s)) { + return Err(SafeError::InvalidEndpoint); + } + let mut url = Url::parse(raw).map_err(|_| SafeError::InvalidEndpoint)?; + if !url.username().is_empty() || url.password().is_some() || url.host().is_none() { + return Err(SafeError::InvalidEndpoint); + } + // HTTP trust is based on the literal host, not WHATWG aliases such as 127.1 or integer IPv4. + let authority = tail.split('/').next().unwrap_or(""); + let literal_host = if authority.starts_with('[') { + authority + .split(']') + .next() + .unwrap_or("") + .trim_start_matches('[') + } else { + authority.split(':').next().unwrap_or("") + }; + let literal_loopback = literal_host.eq_ignore_ascii_case("localhost") + || literal_host + .parse::() + .is_ok_and(|ip| ip.is_loopback()); + let loopback = literal_loopback + && match url.host() { + Some(Host::Domain(host)) => host.eq_ignore_ascii_case("localhost"), + Some(Host::Ipv4(ip)) => ip.is_loopback(), + Some(Host::Ipv6(ip)) => ip.is_loopback(), + _ => false, + }; + match url.scheme() { + "https" => (), + "http" if loopback => (), + "http" => return Err(SafeError::InsecureTransport), + _ => return Err(SafeError::InvalidEndpoint), + } + let path = format!("{}/", url.path().trim_end_matches('/')); + url.set_path(&path); + Ok(Self(url)) + } + pub fn is_loopback(&self) -> bool { + match self.0.host() { + Some(Host::Domain(host)) => host.eq_ignore_ascii_case("localhost"), + Some(Host::Ipv4(ip)) => ip.is_loopback(), + Some(Host::Ipv6(ip)) => ip.is_loopback(), + _ => false, + } + } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + fn operation_url(&self, operation: &str) -> Result { + let manifest: serde_json::Value = serde_json::from_str(include_str!("operations.json")) + .map_err(|_| SafeError::InvalidResponse)?; + let path = manifest["operations"][operation]["path"] + .as_str() + .ok_or(SafeError::InvalidResponse)?; + self.0 + .join(path.trim_start_matches('/')) + .map_err(|_| SafeError::InvalidEndpoint) + } +} + +#[derive(Clone)] +pub struct Transport { + client: Client, + slots: Arc, +} +impl Transport { + pub fn new(ca_pem: Option<&[u8]>) -> Result { + let mut builder = Client::builder() + .no_proxy() + .retry(reqwest::retry::never()) + .redirect(Policy::none()) + .connect_timeout(CONNECT_TIMEOUT) + .timeout(REQUEST_TIMEOUT) + .referer(false); + if let Some(pem) = ca_pem { + if pem.len() > 64 * 1024 || String::from_utf8_lossy(pem).contains("PRIVATE KEY") { + return Err(SafeError::InvalidCertificate); + } + let cert = Certificate::from_pem(pem).map_err(|_| SafeError::InvalidCertificate)?; + builder = builder.add_root_certificate(cert); + } + Ok(Self { + client: builder.build().map_err(|_| SafeError::Tls)?, + slots: Arc::new(Semaphore::new(MAX_CONCURRENT_READS)), + }) + } + /// S1 feasibility probe. Does not infer compatibility, authorization or readiness. + pub async fn liveness( + &self, + endpoint: &Endpoint, + credential: Option<&Secret>, + ) -> Result<(), SafeError> { + let _permit = self.slots.try_acquire().map_err(|_| SafeError::Busy)?; + let mut request = self.client.get(endpoint.operation_url("get_liveness")?); + if let Some(secret) = credential { + let mut header = HeaderValue::from_str(&format!("Bearer {}", secret.expose())) + .map_err(|_| SafeError::InvalidCredential)?; + header.set_sensitive(true); + request = request.header(AUTHORIZATION, header); + } + let mut response = request.send().await.map_err(safe_network_error)?; + match response.status().as_u16() { + 200 => (), + 300..=399 => return Err(SafeError::Redirect), + 401 => return Err(SafeError::Unauthorized), + 403 => return Err(SafeError::Forbidden), + 500..=599 => return Err(SafeError::Server), + _ => return Err(SafeError::InvalidResponse), + } + if response + .content_length() + .is_some_and(|n| n > MAX_RESPONSE_BYTES as u64) + { + return Err(SafeError::ResponseTooLarge); + } + let mut body = Vec::new(); + while let Some(chunk) = response.chunk().await.map_err(safe_network_error)? { + if body.len() + chunk.len() > MAX_RESPONSE_BYTES { + return Err(SafeError::ResponseTooLarge); + } + body.extend_from_slice(&chunk); + } + let value: serde_json::Value = + serde_json::from_slice(&body).map_err(|_| SafeError::InvalidResponse)?; + if value.get("status").and_then(|s| s.as_str()) != Some("ok") { + return Err(SafeError::InvalidResponse); + } + Ok(()) + } +} +fn safe_network_error(error: reqwest::Error) -> SafeError { + if error.is_timeout() { + return SafeError::Timeout; + } + // native-tls preserves its typed error in the source chain; never serialize its text. + let mut source = error.source(); + while let Some(cause) = source { + if cause.is::() { + return SafeError::Tls; + } + source = cause.source(); + } + SafeError::Network +} + +fn safe_segment(segment: &str) -> bool { + let bytes = segment.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' { + if i + 2 >= bytes.len() { + return false; + } + let Some(hi) = (bytes[i + 1] as char).to_digit(16) else { + return false; + }; + let Some(lo) = (bytes[i + 2] as char).to_digit(16) else { + return false; + }; + decoded.push((hi * 16 + lo) as u8); + i += 3; + } else { + decoded.push(bytes[i]); + i += 1; + } + } + std::str::from_utf8(&decoded).is_ok() + && decoded != b"." + && decoded != b".." + && !decoded + .iter() + .any(|b| b.is_ascii_control() || matches!(b, b'/' | b'\\' | b'%')) +} diff --git a/desktop/src-tauri/src/transport/operations.json b/desktop/src-tauri/src/transport/operations.json new file mode 100644 index 000000000..b0bd8b19b --- /dev/null +++ b/desktop/src-tauri/src/transport/operations.json @@ -0,0 +1,45 @@ +{ + "contractSha256": "915866dd26fd2bdce6f987a52fed7febaa859eaf506412765e7c847ea530a6ee", + "operations": { + "get_liveness": { + "method": "GET", + "path": "/health/live" + }, + "get_readiness": { + "method": "GET", + "path": "/health/ready" + }, + "get_capabilities": { + "method": "GET", + "path": "/v1/capabilities" + }, + "list_scopes": { + "method": "GET", + "path": "/v1/scopes" + }, + "get_scope": { + "method": "GET", + "path": "/v1/scopes/{scope_id}" + }, + "get_default_scope": { + "method": "GET", + "path": "/v1/scopes/default" + }, + "remember_memory": { + "method": "POST", + "path": "/v1/memory/remember" + }, + "search_memory": { + "method": "POST", + "path": "/v1/memory/search" + }, + "get_memory_entry": { + "method": "POST", + "path": "/v1/memory/entries/get" + }, + "get_access_principal": { + "method": "GET", + "path": "/v1/access/me" + } + } +} diff --git a/desktop/src-tauri/src/transport/wire.rs b/desktop/src-tauri/src/transport/wire.rs new file mode 100644 index 000000000..9e3efd740 --- /dev/null +++ b/desktop/src-tauri/src/transport/wire.rs @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Generated from openapi/powercontext.yaml. Do not edit. +// rustfmt uses this generated layout verbatim. +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum AccessAction { + #[serde(rename = "server.observe")] + ServerObserve, + #[serde(rename = "server.admin")] + ServerAdmin, + #[serde(rename = "scope.read")] + ScopeRead, + #[serde(rename = "scope.contribute")] + ScopeContribute, + #[serde(rename = "scope.review")] + ScopeReview, + #[serde(rename = "scope.delegate")] + ScopeDelegate, + #[serde(rename = "scope.admin")] + ScopeAdmin, + #[serde(rename = "artifact.read")] + ArtifactRead, + #[serde(rename = "artifact.write")] + ArtifactWrite, + #[serde(rename = "artifact.share")] + ArtifactShare, + #[serde(rename = "handoff.evidence.inspect")] + HandoffEvidenceInspect, + #[serde(rename = "handoff.acknowledge")] + HandoffAcknowledge, + #[serde(rename = "prompt.use")] + PromptUse, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum AccessControlMode { + #[serde(rename = "disabled")] + Disabled, + #[serde(rename = "enforced")] + Enforced, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct AccessMeResponse { + pub r#principal: AccessPrincipal, + pub r#mode: AccessControlMode, + pub r#resource_kinds: Vec, + pub r#provider_capabilities: AccessProviderCapabilities, + pub r#artifact_families: Vec, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct AccessPrincipal { + pub r#type: String, + pub r#id: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#description: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct AccessProviderCapabilities { + pub r#safe_resource_filtering: bool, + pub r#multi_requirement_check: bool, + pub r#relationship_management: bool, + pub r#group_subjects: bool, + pub r#multi_principal: bool, + pub r#max_direct_resource_keys: i64, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum AccessResourceType { + #[serde(rename = "server")] + Server, + #[serde(rename = "scope")] + Scope, + #[serde(rename = "artifact")] + Artifact, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum AccessRole { + #[serde(rename = "handoff.viewer")] + HandoffViewer, + #[serde(rename = "handoff.receiver")] + HandoffReceiver, + #[serde(rename = "artifact.viewer")] + ArtifactViewer, + #[serde(rename = "prompt.user")] + PromptUser, + #[serde(rename = "artifact.owner")] + ArtifactOwner, + #[serde(rename = "scope.viewer")] + ScopeViewer, + #[serde(rename = "scope.contributor")] + ScopeContributor, + #[serde(rename = "scope.reviewer")] + ScopeReviewer, + #[serde(rename = "scope.delegator")] + ScopeDelegator, + #[serde(rename = "scope.admin")] + ScopeAdmin, + #[serde(rename = "server.observer")] + ServerObserver, + #[serde(rename = "server.admin")] + ServerAdmin, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct ArtifactFamilyAccessCapability { + pub r#family: String, + pub r#enabled: bool, + pub r#share_unit: String, + pub r#actions: Vec, + pub r#grantable_roles: Vec, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct ArtifactReference { + pub r#family: String, + pub r#artifact_id: String, + pub r#revision: i64, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct Capabilities { + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#prompts: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#artifact_dreaming: Option, + pub r#source_types: Vec, + pub r#artifact_families: Vec, + pub r#memory_extraction: bool, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#experience_generation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#managed_skill_generation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#external_skill_registry: Option, + pub r#handoff_generation: bool, + pub r#search_modes: Vec, + pub r#context_versions: Vec, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct GetMemoryEntryRequest { + pub r#scope_id: String, + pub r#citation: MemoryCitation, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct HealthResponse { + pub r#status: String, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct MemoryCitation { + pub r#memory_ref: ArtifactReference, + pub r#entry_id: String, + pub r#entry_version_id: String, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct MemoryEntry { + pub r#citation: MemoryCitation, + pub r#version: i64, + pub r#kind: String, + pub r#text: String, + pub r#state: MemoryEntryState, + pub r#source_refs: Vec, + pub r#artifact_refs: Vec, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum MemoryEntryState { + #[serde(rename = "active")] + Active, + #[serde(rename = "inactive")] + Inactive, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum MemoryMatchedBy { + #[serde(rename = "fts")] + Fts, + #[serde(rename = "vector")] + Vector, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct MemoryMutationResponse { + pub r#memory: ArtifactReference, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#entry: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum MemorySearchMode { + #[serde(rename = "auto")] + Auto, + #[serde(rename = "fts")] + Fts, + #[serde(rename = "vector")] + Vector, + #[serde(rename = "hybrid")] + Hybrid, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum MemoryUsedSearchMode { + #[serde(rename = "fts")] + Fts, + #[serde(rename = "vector")] + Vector, + #[serde(rename = "hybrid")] + Hybrid, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum PreparedContextSchema { + #[serde(rename = "powercontext.prepared-context.v1")] + PowercontextPreparedContextV1, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct PromptCapability { + pub r#status: String, + pub r#reason: Option, + pub r#definition_version: String, + pub r#builtin_version: String, + pub r#builtin_profile: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct ReadinessResponse { + pub r#status: ReadinessStatus, + pub r#checks: std::collections::BTreeMap, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum ReadinessStatus { + #[serde(rename = "ready")] + Ready, + #[serde(rename = "degraded")] + Degraded, + #[serde(rename = "not_ready")] + NotReady, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct RememberMemoryRequest { + pub r#scope_id: String, + pub r#kind: String, + pub r#text: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#expected_revision: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct ScopeDescriptor { + pub r#scope_id: String, + pub r#title: String, + pub r#summary: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#parent_scope_id: Option, + pub r#context_references: Vec, + pub r#external_references: Vec, + pub r#version: i64, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct ScopeExternalReference { + pub r#kind: String, + pub r#value: String, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct ScopePage { + pub r#items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#next_cursor: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum ScopeQueryField { + #[serde(rename = "scope_id")] + ScopeId, + #[serde(rename = "title")] + Title, + #[serde(rename = "summary")] + Summary, + #[serde(rename = "external_reference_value")] + ExternalReferenceValue, + #[serde(rename = "binding_external_id")] + BindingExternalId, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct SearchMemoryHit { + pub r#citation: MemoryCitation, + pub r#text: String, + pub r#score: f64, + pub r#matched_by: Vec, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct SearchMemoryRequest { + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#tag_filter: Option, + pub r#scope_id: String, + pub r#query: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#mode: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct SearchMemoryResponse { + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#memory: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#mode: Option, + pub r#hits: Vec, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct SourceReference { + pub r#name: String, + pub r#source_id: String, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +#[serde(deny_unknown_fields)] +pub struct TagFilter { + pub r#tags: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub r#match: Option, +} + +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)] +pub enum TagMatch { + #[serde(rename = "all")] + All, + #[serde(rename = "any")] + Any, +} + +#[rustfmt::skip] +pub fn declarations(config: &ts_rs::Config) -> Vec { + vec![ + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ::decl(config), + ] +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 000000000..9333965a8 --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "PowerContext Desktop Preview", + "version": "0.1.0", + "identifier": "com.powercontext.desktop.preview", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://127.0.0.1:1420", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "PowerContext Desktop", + "width": 1180, + "height": 800, + "minWidth": 400, + "minHeight": 400, + "dragDropEnabled": false, + "zoomHotkeysEnabled": true, + "create": false + } + ], + "security": { + "capabilities": [ + "main" + ], + "csp": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src ipc: http://ipc.localhost; font-src 'self'; object-src 'none'; base-uri 'none'; frame-src 'none'; form-action 'none'", + "devCsp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost ws://127.0.0.1:1420; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'" + } + }, + "bundle": { + "active": true, + "targets": [ + "nsis" + ], + "icon": [ + "icons/icon.ico" + ], + "publisher": "PowerContext", + "shortDescription": "Internal desktop foundation preview", + "windows": { + "nsis": { + "installMode": "currentUser", + "languages": [ + "English", + "SimpChinese" + ], + "displayLanguageSelector": true + }, + "webviewInstallMode": { + "type": "downloadBootstrapper", + "silent": true + } + } + } +} diff --git a/desktop/src-tauri/tests/api.rs b/desktop/src-tauri/tests/api.rs new file mode 100644 index 000000000..e5f5997a9 --- /dev/null +++ b/desktop/src-tauri/tests/api.rs @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + error::SafeError, + transport::{Endpoint, ServerApi, wire::ReadinessStatus}, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; + +async fn fixture(status: u16, body: &'static str) -> (ServerApi, tokio::task::JoinHandle) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = Endpoint::parse(&format!( + "http://{}/proxy/%E4%B8%AD%E6%96%87", + listener.local_addr().unwrap() + )) + .unwrap(); + let task = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut bytes = vec![]; + loop { + let mut buf = [0; 1024]; + let n = socket.read(&mut buf).await.unwrap(); + if n == 0 { + break; + } + bytes.extend_from_slice(&buf[..n]); + if let Some(end) = bytes.windows(4).position(|v| v == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&bytes[..end]); + let size: usize = headers + .lines() + .find_map(|l| { + l.to_lowercase() + .strip_prefix("content-length: ") + .map(str::to_owned) + }) + .map(|s| s.parse().unwrap()) + .unwrap_or(0); + if bytes.len() >= end + 4 + size { + break; + } + } + } + socket.write_all(format!("HTTP/1.1 {status} Result\r\nContent-Type: application/json\r\nX-PowerContext-Request-ID: safe-request-1\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap(); + String::from_utf8(bytes).unwrap() + }); + (ServerApi::new(endpoint, None, None).unwrap(), task) +} +#[tokio::test] +async fn scope_paging_is_bounded_and_preserves_opaque_cursor_and_prefix() { + let (api, request) = fixture(200, r#"{"items":[],"next_cursor":null}"#).await; + api.scopes("中文 & title", Some("opaque+/=?")) + .await + .unwrap(); + let request = request.await.unwrap(); + assert!(request.starts_with("GET /proxy/%E4%B8%AD%E6%96%87/v1/scopes?limit=50&")); + assert!(request.contains("query_field=title")); + assert!(request.contains("cursor=opaque%2B%2F%3D%3F")); +} +#[tokio::test] +async fn exact_scope_uses_one_encoded_path_segment() { + let (api, request) = fixture(404, "private body").await; + assert_eq!( + api.scope("scope/中文?x").await.err().unwrap().code, + SafeError::NotFound + ); + assert!( + request + .await + .unwrap() + .starts_with("GET /proxy/%E4%B8%AD%E6%96%87/v1/scopes/scope%2F%E4%B8%AD%E6%96%87%3Fx ") + ); +} +#[tokio::test] +async fn readiness_failure_is_a_fact_and_write_failure_keeps_dispatch_uncertainty() { + let (api, request) = fixture( + 503, + r#"{"status":"not_ready","checks":{"database":"not_ready"}}"#, + ) + .await; + assert_eq!( + api.readiness().await.unwrap().status, + ReadinessStatus::NotReady + ); + request.await.unwrap(); + let (api, request) = fixture(500, "private body synthetic-secret").await; + let error = api + .remember("selected-scope", "synthetic note") + .await + .err() + .unwrap(); + assert!(error.dispatched); + assert_eq!(error.code, SafeError::Server); + assert_eq!(error.request_id.as_deref(), Some("safe-request-1")); + assert!( + !serde_json::to_string(&error) + .unwrap() + .contains("private body") + ); + let request = request.await.unwrap(); + let body: serde_json::Value = + serde_json::from_str(request.split("\r\n\r\n").nth(1).unwrap()).unwrap(); + assert_eq!( + body, + serde_json::json!({"scope_id":"selected-scope","kind":"note","text":"synthetic note"}) + ); +} +#[tokio::test] +async fn errors_are_distinct_without_anonymous_fallback() { + for (status, code) in [ + (401, SafeError::Unauthorized), + (403, SafeError::Forbidden), + (410, SafeError::CursorExpired), + ] { + let (api, task) = fixture(status, "sensitive failure details").await; + assert_eq!(api.scopes("", None).await.err().unwrap().code, code); + task.await.unwrap(); + } + let (api, task) = fixture( + 503, + r#"{"error":{"code":"authentication_unavailable","message":"sensitive failure details"}}"#, + ) + .await; + assert_eq!( + api.principal().await.err().unwrap().code, + SafeError::AuthenticationUnavailable + ); + task.await.unwrap(); +} +#[test] +fn encoded_base_paths_reject_escape_aliases_and_support_unicode() { + for endpoint in [ + "http://127.1/", + "http://2130706433/", + "http://localhost/a/%2e%2e/b", + "http://localhost/%252e", + "http://localhost/a%2fb", + "http://localhost/%xx", + "http://localhost/%ff", + ] { + assert!(Endpoint::parse(endpoint).is_err(), "{endpoint}"); + } + assert_eq!( + Endpoint::parse("http://localhost/中文/").unwrap().as_str(), + "http://localhost/%E4%B8%AD%E6%96%87/" + ); + assert_eq!( + Endpoint::parse("http://localhost/%E4%B8%AD%E6%96%87/") + .unwrap() + .as_str(), + "http://localhost/%E4%B8%AD%E6%96%87/" + ); +} + +#[tokio::test] +async fn invalid_successful_write_response_keeps_dispatch_uncertainty() { + let (api, request) = fixture(200, r#"{"memory":{"family":"memory","artifact_id":"m","revision":9007199254740992},"entry":null}"#).await; + let error = api + .remember("scope-a", "synthetic note") + .await + .err() + .unwrap(); + assert_eq!(error.code, SafeError::InvalidResponse); + assert!(error.dispatched); + request.await.unwrap(); +} diff --git a/desktop/src-tauri/tests/diagnostics.rs b/desktop/src-tauri/tests/diagnostics.rs new file mode 100644 index 000000000..5987a9b29 --- /dev/null +++ b/desktop/src-tauri/tests/diagnostics.rs @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + diagnostics::{DiagnosticKind, OUTPUT_LIMIT, project}, + error::SafeError, +}; +#[test] +fn unhealthy_service_json_survives_exit_one_without_leaking_private_details() { + let input = serde_json::json!({"support":"supported","registration":"installed","definition":"current", + "manager_ownership":"owned","manager":"inactive","server_liveness":"unreachable", + "endpoint":"https://private.test","log_location":"C:/private/account","recovery_action":"private-command", "detail":"secret-token"}); + let result = project( + DiagnosticKind::Service, + &serde_json::to_vec(&input).unwrap(), + 1, + ) + .unwrap(); + assert_eq!(result.exit_code, 1); + assert_eq!(result.items.len(), 6); + let output = serde_json::to_string(&result).unwrap(); + assert!(!output.contains("private")); + assert!(!output.contains("secret-token")); + assert!(output.contains("unreachable")); +} +#[test] +fn integration_status_is_projected_without_details_or_checks_text() { + let result = project(DiagnosticKind::Integrations, br#"{"ok":false,"status":"failed","hosts":{"codex":{"presence":"present","codex":{"ok":true,"status":"ok","detail":"private-path"},"mcp":{"ok":false,"status":"failed","detail":"secret","checks":{"token":"secret"}}}}}"#, 1).unwrap(); + assert_eq!(result.hosts[0].host, "codex"); + assert_eq!(result.hosts[0].checks.len(), 2); + let output = serde_json::to_string(&result).unwrap(); + assert!(!output.contains("private")); + assert!(!output.contains("secret")); +} +#[test] +fn malformed_unknown_and_oversized_diagnostics_are_rejected() { + for output in [ + b"not json".as_slice(), + br#"{}"#, + br#"{"ok":true,"status":"secret","hosts":{}}"#, + br#"{"ok":false,"status":"ok","hosts":{}}"#, + ] { + assert_eq!( + project(DiagnosticKind::Integrations, output, 1).unwrap_err(), + SafeError::InvalidResponse + ); + } + assert_eq!( + project(DiagnosticKind::Service, &vec![b' '; OUTPUT_LIMIT + 1], 0).unwrap_err(), + SafeError::ResponseTooLarge + ); +} + +#[cfg(windows)] +#[tokio::test] +async fn missing_or_mismatched_cli_registration_never_runs_a_program() { + use powercontext_desktop::diagnostics::LocalDiagnostics; + let directory = tempfile::tempdir().unwrap(); + let registration = directory.path().join("diagnostic-cli.json"); + let diagnostics = LocalDiagnostics::new(registration.clone()); + assert_eq!( + diagnostics.run(DiagnosticKind::Service).await.unwrap_err(), + SafeError::NotFound + ); + let executable = directory.path().join("powercontext.exe"); + std::fs::write(&executable, b"not executable").unwrap(); + let value = serde_json::json!({"executable":executable,"sha256":"0".repeat(64),"version":"1.0.1.dev61+g63f918b7e.d20260919","source":"explicit_local_installation"}); + std::fs::write(®istration, serde_json::to_vec(&value).unwrap()).unwrap(); + assert_eq!( + diagnostics.run(DiagnosticKind::Service).await.unwrap_err(), + SafeError::CompatibilityUnverified + ); +} diff --git a/desktop/src-tauri/tests/ipc.rs b/desktop/src-tauri/tests/ipc.rs new file mode 100644 index 000000000..665d36a0a --- /dev/null +++ b/desktop/src-tauri/tests/ipc.rs @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::ipc::allowed_navigation; +use tauri::{ + ipc::{CallbackFn, InvokeBody}, + test::{INVOKE_KEY, get_ipc_response, mock_builder}, + webview::InvokeRequest, +}; + +#[test] +fn permissions_reject_untrusted_window_origin_and_arbitrary_commands() { + let dir = tempfile::tempdir().unwrap(); + let repository = powercontext_desktop::connections::profiles::ProfileRepository::open( + dir.path().join("profiles.json"), + std::sync::Arc::new(powercontext_desktop::credentials::WindowsVault), + ) + .unwrap(); + let app = mock_builder() + .manage(powercontext_desktop::commands::HostState { + manager: Ok( + powercontext_desktop::connections::session::ConnectionManager::new(repository), + ), + }) + .invoke_handler(tauri::generate_handler![ + powercontext_desktop::ipc::foundation_info, + powercontext_desktop::commands::desktop_state, + powercontext_desktop::commands::disconnect + ]) + .build(tauri::generate_context!()) + .unwrap(); + for (label, origin, command, allowed) in [ + ("main", "http://tauri.localhost", "foundation_info", true), + ("other", "http://tauri.localhost", "foundation_info", false), + ("main", "https://evil.example", "foundation_info", false), + ("main", "http://tauri.localhost", "desktop_state", true), + ("other", "http://tauri.localhost", "desktop_state", false), + ("main", "https://evil.example", "desktop_state", false), + ("main", "http://tauri.localhost", "disconnect", true), + ("other", "http://tauri.localhost", "disconnect", false), + ("main", "https://evil.example", "disconnect", false), + ( + "main", + "http://tauri.localhost", + "plugin:shell|execute", + false, + ), + ("main", "http://tauri.localhost", "fetch", false), + ("main", "http://tauri.localhost", "credential_read", false), + ] { + use tauri::Manager; + let window = app.get_webview_window(label).unwrap_or_else(|| { + tauri::WebviewWindowBuilder::new(&app, label, Default::default()) + .build() + .unwrap() + }); + let result = get_ipc_response( + &window, + InvokeRequest { + cmd: command.into(), + callback: CallbackFn(0), + error: CallbackFn(1), + url: origin.parse().unwrap(), + body: InvokeBody::default(), + headers: Default::default(), + invoke_key: INVOKE_KEY.into(), + }, + ); + assert_eq!( + result.is_ok(), + allowed, + "{label} {origin} {command}: {result:?}" + ); + } +} +#[test] +fn blocks_remote_navigation_and_userinfo() { + for url in [ + "https://evil.example", + "file:///C:/secret", + "javascript:alert(1)", + "http://tauri.localhost.evil", + "http://user@tauri.localhost", + ] { + assert!(!allowed_navigation(&url.parse().unwrap())); + } + assert!(allowed_navigation( + &"http://tauri.localhost/".parse().unwrap() + )); +} + +#[test] +fn diagnostic_command_is_narrow_and_requires_the_trusted_window() { + let app = mock_builder() + .manage(powercontext_desktop::diagnostics::DiagnosticHost::new(None)) + .invoke_handler(tauri::generate_handler![ + powercontext_desktop::commands::local_diagnostics + ]) + .build(tauri::generate_context!()) + .unwrap(); + for (label, origin, kind, expected_missing) in [ + ("main", "http://tauri.localhost", "service", true), + ("main", "http://tauri.localhost", "integrations", true), + ("other", "http://tauri.localhost", "service", false), + ("main", "https://evil.example", "service", false), + ("main", "http://tauri.localhost", "shell", false), + ] { + use tauri::Manager; + let window = app.get_webview_window(label).unwrap_or_else(|| { + tauri::WebviewWindowBuilder::new(&app, label, Default::default()) + .build() + .unwrap() + }); + let result = get_ipc_response( + &window, + InvokeRequest { + cmd: "local_diagnostics".into(), + callback: CallbackFn(0), + error: CallbackFn(1), + url: origin.parse().unwrap(), + body: InvokeBody::Json(serde_json::json!({"kind":kind})), + headers: Default::default(), + invoke_key: INVOKE_KEY.into(), + }, + ); + let error = result.expect_err("missing CLI or rejected caller"); + assert_eq!( + error == serde_json::json!("not_found"), + expected_missing, + "{label} {origin} {kind}: {error}" + ); + } +} + +#[test] +fn memory_commands_reject_untrusted_callers_before_accessing_the_connection() { + use powercontext_desktop::{commands, error::SafeError}; + use tauri::Manager; + let app = mock_builder() + .manage(commands::HostState { + manager: Err(SafeError::NotConnected), + }) + .invoke_handler(tauri::generate_handler![ + commands::remember_memory, + commands::search_memory, + commands::memory_entry, + commands::cancel_memory_reads + ]) + .build(tauri::generate_context!()) + .unwrap(); + let args = serde_json::json!({ + "generation": 0, + "text": "synthetic permission test", + "query": "synthetic", + "citation": { + "memory_ref": {"family":"memory", "artifact_id":"test", "revision":1}, + "entry_id":"test", "entry_version_id":"v1" + } + }); + for command in [ + "remember_memory", + "search_memory", + "memory_entry", + "cancel_memory_reads", + ] { + for (label, origin) in [ + ("main", "http://tauri.localhost"), + ("other", "http://tauri.localhost"), + ("main", "https://evil.example"), + ] { + let window = app.get_webview_window(label).unwrap_or_else(|| { + tauri::WebviewWindowBuilder::new(&app, label, Default::default()) + .build() + .unwrap() + }); + let error = get_ipc_response( + &window, + InvokeRequest { + cmd: command.into(), + callback: CallbackFn(0), + error: CallbackFn(1), + url: origin.parse().unwrap(), + body: InvokeBody::Json(args.clone()), + headers: Default::default(), + invoke_key: INVOKE_KEY.into(), + }, + ) + .expect_err("no connection or forbidden caller"); + assert_eq!( + error.get("code").and_then(serde_json::Value::as_str) == Some("not_connected"), + label == "main" && origin == "http://tauri.localhost", + "{command} {label} {origin}: {error}" + ); + } + } +} diff --git a/desktop/src-tauri/tests/memory.rs b/desktop/src-tauri/tests/memory.rs new file mode 100644 index 000000000..36d1be2fa --- /dev/null +++ b/desktop/src-tauri/tests/memory.rs @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + connections::{ + profiles::ProfileRepository, + session::{ConnectionManager, WriteStatus}, + }, + credentials::WindowsVault, + error::SafeError, +}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicU16, Ordering}, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, + sync::Notify, +}; +struct Fixture { + manager: Arc, + a: String, + b: String, + started: Arc, + release: Arc, + requests: Arc>>, + fail: Arc, + task: tokio::task::JoinHandle<()>, + _dir: tempfile::TempDir, +} +impl Drop for Fixture { + fn drop(&mut self) { + self.task.abort(); + self.release.notify_waiters(); + } +} +async fn setup() -> Fixture { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let started = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let fail = Arc::new(AtomicU16::new(200)); + let requests = Arc::new(Mutex::new(vec![])); + let (start, finish, bodies, failure) = ( + started.clone(), + release.clone(), + requests.clone(), + fail.clone(), + ); + let task = tokio::spawn(async move { + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + let (start, finish, bodies, failure) = ( + start.clone(), + finish.clone(), + bodies.clone(), + failure.clone(), + ); + tokio::spawn(async move { + let mut bytes = vec![]; + let end = loop { + let mut chunk = [0; 4096]; + let n = socket.read(&mut chunk).await.unwrap(); + if n == 0 { + return; + } + bytes.extend_from_slice(&chunk[..n]); + if let Some(end) = bytes.windows(4).position(|p| p == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&bytes[..end]); + let length: usize = headers + .lines() + .find_map(|l| { + l.to_lowercase() + .strip_prefix("content-length: ") + .map(str::to_owned) + }) + .map(|v| v.parse().unwrap()) + .unwrap_or(0); + if bytes.len() >= end + 4 + length { + break end; + } + } + }; + let path = String::from_utf8_lossy(&bytes[..end]) + .split_whitespace() + .nth(1) + .unwrap() + .to_owned(); + let (status, value) = match path.as_str() { + "/v1/access/me" => ( + 503, + serde_json::json!({"error":{"code":"runtime_not_ready"}}), + ), + "/health/ready" => ( + 200, + serde_json::json!({"status":"ready","checks":{"access_mode":"disabled"}}), + ), + "/v1/capabilities" => (403, serde_json::json!({})), + "/v1/memory/remember" => { + bodies + .lock() + .unwrap() + .push(serde_json::from_slice(&bytes[end + 4..]).unwrap()); + start.notify_one(); + finish.notified().await; + let status = failure.load(Ordering::Relaxed); + if status == 0 { + // The fixture accepted the write but the response connection is lost. + return; + } + if status != 200 { + (status, serde_json::json!({"detail":"must not leak"})) + } else { + ( + 200, + serde_json::json!({"memory":{"family":"memory","artifact_id":"mem-a","revision":1},"entry":null}), + ) + } + } + path if path.starts_with("/v1/scopes/") => ( + 200, + serde_json::json!({"scope_id":path.rsplit('/').next().unwrap(),"title":"Synthetic scope","summary":"","context_references":[],"external_references":[],"version":1}), + ), + _ => (200, serde_json::json!({"status":"ok"})), + }; + let body = value.to_string(); + let response = format!( + "HTTP/1.1 {status} Result\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + let _ = socket.write_all(response.as_bytes()).await; + }); + } + }); + let dir = tempfile::tempdir().unwrap(); + let manager = Arc::new(ConnectionManager::new( + ProfileRepository::open(dir.path().join("profiles.json"), Arc::new(WindowsVault)).unwrap(), + )); + let compatibility = manager.state().unwrap().compatibility_profiles[0] + .id + .clone(); + let mut ids = vec![]; + for name in ["A", "B"] { + let state = manager.save_profile(serde_json::from_value(serde_json::json!({"id":null,"revision":null,"name":name,"endpoint":endpoint,"authentication":"unauthenticated_loopback","caPem":null,"compatibility":compatibility,"keepCredential":false,"credential":null})).unwrap()).unwrap(); + ids.push( + state + .profiles + .iter() + .find(|p| p.name == name) + .unwrap() + .id + .clone(), + ); + } + let generation = manager.check(&ids[0], true).await.unwrap().generation; + manager.select_scope(generation, "scope-a").await.unwrap(); + Fixture { + manager, + a: ids[0].clone(), + b: ids[1].clone(), + started, + release, + requests, + fail, + task, + _dir: dir, + } +} +#[tokio::test] +async fn dispatched_write_keeps_its_original_target_and_rejects_duplicate_clicks() { + let fixture = setup().await; + let generation = fixture.manager.state().unwrap().generation; + let manager = fixture.manager.clone(); + let write = + tokio::spawn(async move { manager.remember(generation, "中文 synthetic note").await }); + tokio::time::timeout( + std::time::Duration::from_secs(3), + fixture.started.notified(), + ) + .await + .unwrap(); + assert_eq!( + fixture + .manager + .remember(generation, "duplicate") + .await + .err() + .unwrap() + .code, + SafeError::Busy + ); + fixture.manager.check(&fixture.b, true).await.unwrap(); + fixture.release.notify_one(); + let outcome = write.await.unwrap().unwrap(); + assert_eq!(outcome.record.status, WriteStatus::Succeeded); + assert_eq!(outcome.record.context.connection_id, fixture.a); + assert_eq!(outcome.record.context.scope_id, "scope-a"); + assert!(outcome.result.is_none()); + assert!(outcome.record.citation.is_none()); + let requests = fixture.requests.lock().unwrap(); + assert_eq!( + requests.as_slice(), + &[serde_json::json!({"scope_id":"scope-a","kind":"note","text":"中文 synthetic note"})] + ); +} +#[tokio::test] +async fn dispatched_server_failure_is_unknown_and_is_never_replayed() { + let fixture = setup().await; + fixture.fail.store(500, Ordering::Relaxed); + fixture.release.notify_one(); + let outcome = fixture + .manager + .remember(fixture.manager.state().unwrap().generation, "test note") + .await + .unwrap(); + assert_eq!(outcome.record.status, WriteStatus::Unknown); + assert!(outcome.result.is_none()); + assert_eq!(fixture.requests.lock().unwrap().len(), 1); + let stored = fixture.manager.state().unwrap().last_write.unwrap(); + assert_eq!(stored.status, WriteStatus::Unknown); + assert!( + !serde_json::to_string(&stored) + .unwrap() + .contains("test note") + ); +} +#[tokio::test] +async fn nullable_entry_success_does_not_invent_a_citation() { + let fixture = setup().await; + fixture.release.notify_one(); + let result = fixture + .manager + .remember(fixture.manager.state().unwrap().generation, "test note") + .await + .unwrap(); + assert_eq!(result.record.status, WriteStatus::Succeeded); + assert!(result.record.citation.is_none()); + assert!(result.result.unwrap().entry.is_none()); +} + +#[tokio::test] +async fn dropping_a_dispatched_write_preserves_unknown_metadata_without_body() { + let fixture = setup().await; + let generation = fixture.manager.state().unwrap().generation; + let manager = fixture.manager.clone(); + let write = + tokio::spawn(async move { manager.remember(generation, "private synthetic body").await }); + tokio::time::timeout( + std::time::Duration::from_secs(3), + fixture.started.notified(), + ) + .await + .unwrap(); + write.abort(); + assert!(write.await.err().unwrap().is_cancelled()); + let record = fixture.manager.state().unwrap().last_write.unwrap(); + assert_eq!(record.status, WriteStatus::Unknown); + assert!( + !serde_json::to_string(&record) + .unwrap() + .contains("private synthetic body") + ); + fixture.release.notify_one(); +} +#[tokio::test] +async fn absent_scope_never_uses_the_server_default_for_writing() { + let fixture = setup().await; + let generation = fixture + .manager + .check(&fixture.a, true) + .await + .unwrap() + .generation; + assert_eq!( + fixture + .manager + .remember(generation, "no scope") + .await + .err() + .unwrap() + .code, + SafeError::ScopeRequired + ); + assert!(fixture.requests.lock().unwrap().is_empty()); +} + +#[tokio::test] +async fn explicit_write_rejections_are_failures_without_replay() { + for (status, code) in [(409, SafeError::Conflict), (422, SafeError::InvalidInput)] { + let fixture = setup().await; + fixture.fail.store(status, Ordering::Relaxed); + fixture.release.notify_one(); + let result = fixture + .manager + .remember( + fixture.manager.state().unwrap().generation, + "synthetic rejected", + ) + .await + .unwrap(); + assert_eq!(result.record.status, WriteStatus::Failed); + assert_eq!(result.record.error.unwrap().code, code); + assert!(result.result.is_none()); + assert_eq!(fixture.requests.lock().unwrap().len(), 1); + } +} + +#[tokio::test] +async fn accepted_write_with_lost_response_remains_unknown_without_replay() { + let fixture = setup().await; + fixture.fail.store(0, Ordering::Relaxed); + fixture.release.notify_one(); + let result = fixture + .manager + .remember( + fixture.manager.state().unwrap().generation, + "synthetic accepted", + ) + .await + .unwrap(); + assert_eq!(result.record.status, WriteStatus::Unknown); + assert!(result.result.is_none()); + let accepted = fixture.requests.lock().unwrap(); + assert_eq!(accepted.len(), 1); + assert_eq!(accepted[0]["text"], "synthetic accepted"); +} diff --git a/desktop/src-tauri/tests/profiles.rs b/desktop/src-tauri/tests/profiles.rs new file mode 100644 index 000000000..e1ea5b9e7 --- /dev/null +++ b/desktop/src-tauri/tests/profiles.rs @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + connections::profiles::{CredentialState, ProfileInput, ProfileRepository}, + credentials::{CredentialId, Secret, Vault}, + error::SafeError, +}; +use std::{ + collections::BTreeSet, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, +}; + +#[derive(Default)] +struct TestVault { + ids: Mutex>, + unavailable: AtomicBool, +} +impl Vault for TestVault { + fn put(&self, id: &CredentialId, _: &Secret) -> Result<(), SafeError> { + if self.unavailable.load(Ordering::Relaxed) { + return Err(SafeError::CredentialUnavailable); + } + self.ids.lock().unwrap().insert(id.as_str().into()); + Ok(()) + } + fn read(&self, id: &CredentialId) -> Result { + if self.ids.lock().unwrap().contains(id.as_str()) { + Secret::new("synthetic-private-marker".into()) + } else { + Err(SafeError::CredentialMissing) + } + } + fn delete(&self, id: &CredentialId) -> Result<(), SafeError> { + if self.unavailable.load(Ordering::Relaxed) { + return Err(SafeError::CredentialUnavailable); + } + self.ids.lock().unwrap().remove(id.as_str()); + Ok(()) + } +} +fn input(name: &str, storage: &str) -> ProfileInput { + serde_json::from_value(serde_json::json!({ + "id":null,"revision":null,"name":name,"endpoint":"http://localhost:8000/prefix", + "authentication":"bearer","caPem":null,"compatibility":null,"keepCredential":false, + "credential":{"secret":"synthetic-private-marker","storage":storage} + })) + .unwrap() +} +#[test] +fn persistence_restart_and_remove_never_store_or_return_secrets() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("profiles.json"); + let vault = Arc::new(TestVault::default()); + let mut repo = ProfileRepository::open(path.clone(), vault.clone()).unwrap(); + let profile = repo.save(input("中文配置", "persistent")).unwrap(); + assert!(matches!(profile.credential_state, CredentialState::Stored)); + let view = serde_json::to_string(&profile).unwrap(); + assert!(!view.contains("synthetic-private-marker")); + assert!(!view.contains("credentialRef")); + assert!( + !std::fs::read_to_string(&path) + .unwrap() + .contains("synthetic-private-marker") + ); + drop(repo); + let mut repo = ProfileRepository::open(path, vault.clone()).unwrap(); + assert!(repo.api(&profile.id).is_ok()); + repo.remove(&profile.id, profile.revision).unwrap(); + assert!(repo.views().is_empty()); + assert!(vault.ids.lock().unwrap().is_empty()); +} +#[test] +fn explicit_session_works_without_vault_and_expires_on_restart() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("profiles.json"); + let vault = Arc::new(TestVault::default()); + vault.unavailable.store(true, Ordering::Relaxed); + let mut repo = ProfileRepository::open(path.clone(), vault.clone()).unwrap(); + assert!(matches!( + repo.save(input("Persistent", "persistent")), + Err(SafeError::CredentialUnavailable) + )); + assert!(repo.views().is_empty()); + let session = repo.save(input("Session", "session_only")).unwrap(); + assert!(matches!( + session.credential_state, + CredentialState::SessionOnly + )); + assert!(repo.api(&session.id).is_ok()); + drop(repo); + let repo = ProfileRepository::open(path, vault).unwrap(); + assert!(matches!( + repo.views()[0].credential_state, + CredentialState::Missing + )); + assert!(matches!( + repo.api(&session.id), + Err(SafeError::CredentialMissing) + )); +} +#[test] +fn retargeting_cannot_keep_a_credential_and_edits_require_current_revision() { + let dir = tempfile::tempdir().unwrap(); + let vault = Arc::new(TestVault::default()); + let mut repo = + ProfileRepository::open(dir.path().join("profiles.json"), vault.clone()).unwrap(); + let original = repo.save(input("One", "persistent")).unwrap(); + let mut changed = input("One", "persistent"); + changed.id = Some(original.id.clone()); + changed.revision = Some(original.revision); + changed.endpoint = "https://another.example".into(); + changed.keep_credential = true; + changed.credential = None; + assert!(matches!( + repo.save(changed), + Err(SafeError::InvalidCredential) + )); + assert_eq!(repo.views()[0].endpoint, original.endpoint); + let mut changed = input("One", "persistent"); + changed.id = Some(original.id.clone()); + changed.revision = Some(original.revision); + changed.endpoint = "https://another.example".into(); + changed.credential = None; + let updated = repo.save(changed).unwrap(); + assert!(matches!(updated.credential_state, CredentialState::Missing)); + assert!(vault.ids.lock().unwrap().is_empty()); + assert!(matches!( + repo.remove(&original.id, original.revision), + Err(SafeError::Conflict) + )); + assert!(matches!( + repo.save(input("one", "session_only")), + Err(SafeError::DuplicateName) + )); +} +#[test] +fn failed_cleanup_is_retained_and_completed_on_reopen() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("profiles.json"); + let vault = Arc::new(TestVault::default()); + let mut repo = ProfileRepository::open(path.clone(), vault.clone()).unwrap(); + let profile = repo.save(input("One", "persistent")).unwrap(); + vault.unavailable.store(true, Ordering::Relaxed); + repo.remove(&profile.id, profile.revision).unwrap(); + assert_eq!(repo.pending_cleanup(), 1); + drop(repo); + vault.unavailable.store(false, Ordering::Relaxed); + let repo = ProfileRepository::open(path, vault.clone()).unwrap(); + assert_eq!(repo.pending_cleanup(), 0); + assert!(vault.ids.lock().unwrap().is_empty()); +} +#[test] +fn corrupt_configuration_is_reported_without_overwriting_it() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("profiles.json"); + std::fs::write(&path, "broken configuration").unwrap(); + assert!(matches!( + ProfileRepository::open(path.clone(), Arc::new(TestVault::default())), + Err(SafeError::ProfileCorrupt) + )); + assert_eq!( + std::fs::read_to_string(path).unwrap(), + "broken configuration" + ); +} diff --git a/desktop/src-tauri/tests/session.rs b/desktop/src-tauri/tests/session.rs new file mode 100644 index 000000000..be08df88b --- /dev/null +++ b/desktop/src-tauri/tests/session.rs @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + connections::{profiles::ProfileRepository, session::ConnectionManager}, + credentials::WindowsVault, + error::SafeError, +}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, + sync::Notify, +}; +struct Fixture { + endpoint: String, + scope_started: Arc, + release: Arc, + changed_identity: Arc, + task: tokio::task::JoinHandle<()>, +} +impl Drop for Fixture { + fn drop(&mut self) { + self.task.abort(); + self.release.notify_waiters(); + } +} +async fn fixture() -> Fixture { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let scope_started = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let changed_identity = Arc::new(AtomicBool::new(false)); + let (started, ready, changed) = ( + scope_started.clone(), + release.clone(), + changed_identity.clone(), + ); + let task = tokio::spawn(async move { + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + let (started, ready, changed) = (started.clone(), ready.clone(), changed.clone()); + tokio::spawn(async move { + let mut request = vec![]; + loop { + let mut buf = [0; 1024]; + let n = socket.read(&mut buf).await.unwrap(); + if n == 0 { + return; + } + request.extend_from_slice(&buf[..n]); + if request.windows(4).any(|v| v == b"\r\n\r\n") { + break; + } + } + let text = String::from_utf8(request).unwrap(); + let path = text.split_whitespace().nth(1).unwrap(); + let (status, body) = if path.contains("/access/me") { + if changed.load(Ordering::Relaxed) { + (401, r#"{}"#) + } else { + (503, r#"{"error":{"code":"runtime_not_ready"}}"#) + } + } else if path.contains("/ready") { + ( + 200, + r#"{"status":"ready","checks":{"access_mode":"disabled"}}"#, + ) + } else if path.contains("/capabilities") { + (403, r#"{}"#) + } else if path.contains("/scopes?") { + started.notify_one(); + ready.notified().await; + (200, r#"{"items":[],"next_cursor":null}"#) + } else { + (200, r#"{"status":"ok"}"#) + }; + let response = format!( + "HTTP/1.1 {status} Result\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + let _ = socket.write_all(response.as_bytes()).await; + }); + } + }); + Fixture { + endpoint, + scope_started, + release, + changed_identity, + task, + } +} +fn add(manager: &ConnectionManager, name: &str, endpoint: &str) -> String { + let compatibility = manager.state().unwrap().compatibility_profiles[0] + .id + .clone(); + let state = manager.save_profile(serde_json::from_value(serde_json::json!({ + "id":null,"revision":null,"name":name,"endpoint":endpoint, + "authentication":"unauthenticated_loopback","caPem":null,"compatibility":compatibility, + "keepCredential":false,"credential":null + })).unwrap()).unwrap(); + state + .profiles + .iter() + .find(|p| p.name == name) + .unwrap() + .id + .clone() +} +fn manager(dir: &tempfile::TempDir) -> Arc { + Arc::new(ConnectionManager::new( + ProfileRepository::open(dir.path().join("profiles.json"), Arc::new(WindowsVault)).unwrap(), + )) +} +#[tokio::test] +async fn checking_another_profile_does_not_activate_it_and_capability_denial_is_independent() { + let server = fixture().await; + let dir = tempfile::tempdir().unwrap(); + let manager = manager(&dir); + let a = add(&manager, "A", &server.endpoint); + let b = add(&manager, "B", &server.endpoint); + manager.check(&a, true).await.unwrap(); + let state = manager.check(&b, false).await.unwrap(); + let active = state.active.unwrap(); + assert_eq!(active.connection_id, a); + assert!(active.report.anonymous_access); + assert!(active.report.compatibility_verified); + assert_eq!( + active.report.capabilities.error.unwrap().code, + SafeError::Forbidden + ); + server.release.notify_one(); + assert!(manager.scopes(active.generation, "", None).await.is_ok()); +} +#[tokio::test] +async fn switching_connection_cancels_a_slow_scope_read_before_it_can_return_old_data() { + let server = fixture().await; + let dir = tempfile::tempdir().unwrap(); + let manager = manager(&dir); + let a = add(&manager, "A", &server.endpoint); + let b = add(&manager, "B", &server.endpoint); + let generation = manager.check(&a, true).await.unwrap().generation; + let reader = manager.clone(); + let read = tokio::spawn(async move { reader.scopes(generation, "", None).await }); + tokio::time::timeout( + std::time::Duration::from_secs(3), + server.scope_started.notified(), + ) + .await + .unwrap(); + manager.check(&b, true).await.unwrap(); + let result = tokio::time::timeout(std::time::Duration::from_secs(1), read) + .await + .unwrap() + .unwrap(); + assert_eq!(result.err().unwrap().code, SafeError::StaleContext); + assert_eq!(manager.state().unwrap().active.unwrap().connection_id, b); + server.release.notify_one(); +} +#[tokio::test] +async fn changed_authentication_drops_the_active_context_before_scope_access() { + let server = fixture().await; + let dir = tempfile::tempdir().unwrap(); + let manager = manager(&dir); + let id = add(&manager, "A", &server.endpoint); + let generation = manager.check(&id, true).await.unwrap().generation; + server.changed_identity.store(true, Ordering::Relaxed); + assert_eq!( + manager + .scopes(generation, "", None) + .await + .err() + .unwrap() + .code, + SafeError::Unauthorized + ); + assert!(manager.state().unwrap().active.is_none()); +} + +#[tokio::test] +async fn editing_a_scope_query_cancels_its_read_without_disconnecting() { + let server = fixture().await; + let dir = tempfile::tempdir().unwrap(); + let manager = manager(&dir); + let id = add(&manager, "A", &server.endpoint); + let generation = manager.check(&id, true).await.unwrap().generation; + let reader = manager.clone(); + let read = tokio::spawn(async move { reader.scopes(generation, "old", None).await }); + tokio::time::timeout( + std::time::Duration::from_secs(3), + server.scope_started.notified(), + ) + .await + .unwrap(); + manager.cancel_scope_reads(generation).unwrap(); + let result = tokio::time::timeout(std::time::Duration::from_secs(1), read) + .await + .unwrap() + .unwrap(); + assert_eq!(result.err().unwrap().code, SafeError::StaleContext); + assert_eq!(manager.state().unwrap().active.unwrap().connection_id, id); + server.release.notify_one(); +} diff --git a/desktop/src-tauri/tests/tls.rs b/desktop/src-tauri/tests/tls.rs new file mode 100644 index 000000000..a52d59eaa --- /dev/null +++ b/desktop/src-tauri/tests/tls.rs @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + error::SafeError, + transport::{Endpoint, Transport}, +}; +use rcgen::{ + BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, + KeyUsagePurpose, +}; +use rustls::pki_types::PrivatePkcs8KeyDer; +use std::sync::Arc; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; +use tokio_rustls::TlsAcceptor; + +async fn https_fixture(hostname: &str) -> (Endpoint, String, tokio::task::JoinHandle<()>) { + let _ = rustls::crypto::ring::default_provider().install_default(); + let mut ca = CertificateParams::new(vec![]).unwrap(); + ca.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca.distinguished_name + .push(rcgen::DnType::CommonName, "S1 fixture CA"); + ca.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; + let ca_key = KeyPair::generate().unwrap(); + let ca_cert = ca.self_signed(&ca_key).unwrap(); + let issuer = Issuer::new(ca, ca_key); + let mut leaf = CertificateParams::new(vec![hostname.into()]).unwrap(); + leaf.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + let leaf_key = KeyPair::generate().unwrap(); + let leaf_cert = leaf.signed_by(&leaf_key, &issuer).unwrap(); + let config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![leaf_cert.der().clone(), ca_cert.der().clone()], + PrivatePkcs8KeyDer::from(leaf_key.serialize_der()).into(), + ) + .unwrap(); + let acceptor = TlsAcceptor::from(Arc::new(config)); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = Endpoint::parse(&format!( + "https://127.0.0.1:{}", + listener.local_addr().unwrap().port() + )) + .unwrap(); + let task = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + if let Ok(mut tls) = acceptor.accept(stream).await { + let mut request = [0; 4096]; + if tls.read(&mut request).await.is_ok() { + let body = r#"{"status":"ok"}"#; + let _ = tls.write_all(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await; + let _ = tls.shutdown().await; + } + } + }); + (endpoint, ca_cert.pem(), task) +} + +#[tokio::test] +async fn explicit_ca_is_connection_local_and_hostname_is_still_verified() { + let (endpoint, ca, task) = https_fixture("127.0.0.1").await; + Transport::new(Some(ca.as_bytes())) + .unwrap() + .liveness(&endpoint, None) + .await + .unwrap(); + task.await.unwrap(); + + let (endpoint, _, task) = https_fixture("127.0.0.1").await; + assert_eq!( + Transport::new(None) + .unwrap() + .liveness(&endpoint, None) + .await, + Err(SafeError::Tls) + ); + task.await.unwrap(); + + let (endpoint, ca, task) = https_fixture("wrong.example").await; + assert_eq!( + Transport::new(Some(ca.as_bytes())) + .unwrap() + .liveness(&endpoint, None) + .await, + Err(SafeError::Tls) + ); + task.await.unwrap(); +} diff --git a/desktop/src-tauri/tests/transport.rs b/desktop/src-tauri/tests/transport.rs new file mode 100644 index 000000000..dd9a4ca7e --- /dev/null +++ b/desktop/src-tauri/tests/transport.rs @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use powercontext_desktop::{ + credentials::Secret, + error::SafeError, + transport::{Endpoint, MAX_RESPONSE_BYTES, Transport}, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; + +async fn fixture(response: String) -> (Endpoint, tokio::task::JoinHandle) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = Endpoint::parse(&format!( + "http://{}/proxy/powercontext", + listener.local_addr().unwrap() + )) + .unwrap(); + let task = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut request = vec![0; 8192]; + let count = stream.read(&mut request).await.unwrap(); + stream.write_all(response.as_bytes()).await.unwrap(); + String::from_utf8_lossy(&request[..count]).into_owned() + }); + (endpoint, task) +} +fn response(status: &str, body: &str) -> String { + format!( + "HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) +} + +#[tokio::test] +async fn read_budget_rejects_excess_work_and_recovers_after_cancellation() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = Endpoint::parse(&format!("http://{}", listener.local_addr().unwrap())).unwrap(); + let (accepted, mut observed) = tokio::sync::mpsc::channel(4); + let server = tokio::spawn(async move { + let mut sockets = Vec::new(); + loop { + let (socket, _) = listener.accept().await.unwrap(); + sockets.push(socket); + if accepted.send(()).await.is_err() { + break; + } + } + }); + let transport = Transport::new(None).unwrap(); + let mut requests = Vec::new(); + for _ in 0..4 { + let client = transport.clone(); + let endpoint = endpoint.clone(); + requests.push(tokio::spawn(async move { + client.liveness(&endpoint, None).await + })); + tokio::time::timeout(std::time::Duration::from_secs(5), observed.recv()) + .await + .unwrap() + .unwrap(); + } + assert_eq!( + transport.liveness(&endpoint, None).await, + Err(SafeError::Busy) + ); + for request in requests { + request.abort(); + let _ = request.await; + } + server.abort(); + let (endpoint, server) = fixture(response("200 OK", r#"{"status":"ok"}"#)).await; + transport.liveness(&endpoint, None).await.unwrap(); + server.await.unwrap(); +} + +#[tokio::test] +async fn ignores_proxy_environment() { + if let Ok(url) = std::env::var("S1_CHILD_ENDPOINT") { + Transport::new(None) + .unwrap() + .liveness(&Endpoint::parse(&url).unwrap(), None) + .await + .unwrap(); + return; + } + let (endpoint, server) = fixture(response("200 OK", r#"{"status":"ok"}"#)).await; + let url = endpoint.as_str().to_owned(); + let child = tokio::task::spawn_blocking(move || { + std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "ignores_proxy_environment"]) + .env("S1_CHILD_ENDPOINT", url) + .env("HTTP_PROXY", "http://127.0.0.1:1") + .env("HTTPS_PROXY", "http://127.0.0.1:1") + .env("ALL_PROXY", "http://127.0.0.1:1") + .env("NO_PROXY", "") + .status() + .unwrap() + }); + assert!(child.await.unwrap().success()); + server.await.unwrap(); +} + +#[tokio::test] +async fn stalled_response_times_out() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let endpoint = Endpoint::parse(&format!("http://{}", listener.local_addr().unwrap())).unwrap(); + let server = tokio::spawn(async move { + let (_stream, _) = listener.accept().await.unwrap(); + std::future::pending::<()>().await; + }); + assert_eq!( + Transport::new(None) + .unwrap() + .liveness(&endpoint, None) + .await, + Err(SafeError::Timeout) + ); + server.abort(); +} + +#[tokio::test] +async fn rejects_chunked_overflow_without_silent_truncation() { + let body = "x".repeat(MAX_RESPONSE_BYTES + 1); + let (endpoint, server) = fixture(format!( + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{:x}\r\n{}\r\n0\r\n\r\n", + body.len(), + body + )) + .await; + assert_eq!( + Transport::new(None) + .unwrap() + .liveness(&endpoint, None) + .await, + Err(SafeError::ResponseTooLarge) + ); + let _ = server.await; +} +#[test] +fn shared_loopback_policy_and_unsafe_addresses() { + let vectors: serde_json::Value = serde_json::from_str(include_str!( + "../../../tests/fixtures/transport_loopback_vectors.json" + )) + .unwrap(); + for host in vectors["loopback"].as_array().unwrap() { + assert!(Endpoint::parse(&format!("http://{}:8000", host.as_str().unwrap())).is_ok()); + } + for host in vectors["non_loopback"].as_array().unwrap() { + assert!(matches!( + Endpoint::parse(&format!("http://{}:8000", host.as_str().unwrap())), + Err(SafeError::InsecureTransport) + )); + } + for bad in [ + "https://user:secret@example.com", + "https://@example.com", + "https://example.com?x=1", + "https://example.com/#hash", + "https://example.com/a/../b", + "https://example.com/%2e%2e/b", + "https://example.com/a\\b", + "file:///tmp/x", + " https://example.com", + "https://example.com/\n", + ] { + assert!(Endpoint::parse(bad).is_err(), "{bad}"); + } +} +#[tokio::test] +async fn preserves_reverse_proxy_path_and_keeps_auth_in_headers() { + let (endpoint, server) = fixture(response("200 OK", r#"{"status":"ok"}"#)).await; + Transport::new(None) + .unwrap() + .liveness( + &endpoint, + Some(&Secret::new("synthetic-test-only".into()).unwrap()), + ) + .await + .unwrap(); + let request = server.await.unwrap(); + assert!(request.starts_with("GET /proxy/powercontext/health/live HTTP/1.1")); + assert!( + request + .to_lowercase() + .contains("authorization: bearer synthetic-test-only") + ); + assert!(!request.lines().next().unwrap().contains("synthetic")); +} +#[tokio::test] +async fn redirects_are_not_followed_even_with_a_credential() { + let destination = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let redirect = format!( + "HTTP/1.1 302 Found\r\nLocation: http://{}/capture\r\nContent-Length: 0\r\n\r\n", + destination.local_addr().unwrap() + ); + let (endpoint, server) = fixture(redirect).await; + assert_eq!( + Transport::new(None) + .unwrap() + .liveness(&endpoint, Some(&Secret::new("synthetic".into()).unwrap())) + .await, + Err(SafeError::Redirect) + ); + server.await.unwrap(); + assert!( + tokio::time::timeout(std::time::Duration::from_millis(100), destination.accept()) + .await + .is_err() + ); +} +#[tokio::test] +async fn projects_errors_without_server_body_or_secrets() { + for (status, error) in [ + ("401 Unauthorized", SafeError::Unauthorized), + ("403 Forbidden", SafeError::Forbidden), + ("503 Unavailable", SafeError::Server), + ] { + let (endpoint, server) = fixture(response(status, "private-body-and-token")).await; + let result = Transport::new(None) + .unwrap() + .liveness(&endpoint, None) + .await + .unwrap_err(); + assert_eq!(result, error); + assert!(!serde_json::to_string(&result).unwrap().contains("private")); + server.await.unwrap(); + } +} +#[tokio::test] +async fn rejects_oversized_and_invalid_responses() { + for body in [ + "x".repeat(MAX_RESPONSE_BYTES + 1), + r#"{"status":"degraded"}"#.into(), + "".into(), + ] { + let (endpoint, server) = fixture(response("200 OK", &body)).await; + let result = Transport::new(None) + .unwrap() + .liveness(&endpoint, None) + .await; + assert!(matches!( + result, + Err(SafeError::ResponseTooLarge | SafeError::InvalidResponse) + )); + // Client may close before the fixture finishes writing an oversized body. + let _ = server.await; + } +} diff --git a/desktop/tests/installed_boundaries.py b/desktop/tests/installed_boundaries.py new file mode 100644 index 000000000..f2f8b6925 --- /dev/null +++ b/desktop/tests/installed_boundaries.py @@ -0,0 +1,77 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Installed note budget and bounded search acceptance against a real Server.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import httpx +from real_server import HarnessFailure + +if TYPE_CHECKING: + from installed_workflow import InstalledPage + + +def exercise_note_budget(page: InstalledPage, server: httpx.Client, scope: str) -> None: + prefix = "desktopbudgetci " + note = prefix + "é" * ((8192 - len(prefix)) // 2) + note += "x" * (8192 - len(note.encode("utf-8"))) + page.type("记忆内容", note + "é", "textarea") + page.wait_text("正文超过 8192 UTF-8 字节") + disabled = page.observe("""return [...document.querySelectorAll('button')].find( + b => b.textContent.trim() === '保存记忆')?.disabled;""") + if disabled is not True: + raise HarnessFailure("installed_over_budget_save_enabled") + matches = server.post( + "/v1/memory/search", json={"scope_id": scope, "query": "desktopbudgetci", "mode": "fts", "limit": 10} + ) + matches.raise_for_status() + if matches.json()["hits"]: + raise HarnessFailure("installed_over_budget_note_submitted") + page.clear_note() + page.type("记忆内容", note, "textarea") + page.wait_text("8192 / 8192") + page.button("保存记忆") + page.wait_text("保存成功。") + matches = server.post( + "/v1/memory/search", json={"scope_id": scope, "query": "desktopbudgetci", "mode": "fts", "limit": 10} + ) + matches.raise_for_status() + hits = matches.json()["hits"] + if len(hits) != 1: + raise HarnessFailure("installed_boundary_note_missing_or_duplicated") + entry = server.post("/v1/memory/entries/get", json={"scope_id": scope, "citation": hits[0]["citation"]}) + entry.raise_for_status() + if entry.json()["text"] != note or len(entry.json()["text"].encode("utf-8")) != 8192: + raise HarnessFailure("installed_boundary_note_truncated") + + +def exercise_search_limit(page: InstalledPage, server: httpx.Client, scope: str) -> None: + for index in range(11): + response = server.post( + "/v1/memory/remember", + json={"scope_id": scope, "kind": "note", "text": f"desktoplimitci independent result {index}"}, + ) + response.raise_for_status() + for query, expected in [("desktopnonexistentci", 0), ("desktoplimitci", 10)]: + page.type("全文搜索关键词", "\ue009a\ue000\ue003" + query) + page.button("搜索") + page.wait_text("没有匹配的记忆。" if expected == 0 else "已返回本次上限 10 条") + count = page.observe("return document.querySelectorAll('.memory-hits li').length;") + if count != expected: + raise HarnessFailure("installed_search_result_count_mismatch") + if page.observe("return !!document.querySelector('.reader');"): + raise HarnessFailure("installed_search_kept_old_reader") diff --git a/desktop/tests/installed_fixture.py b/desktop/tests/installed_fixture.py new file mode 100644 index 000000000..a38d8a804 --- /dev/null +++ b/desktop/tests/installed_fixture.py @@ -0,0 +1,114 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Isolated no-model Server fixture for installed application acceptance.""" + +from __future__ import annotations + +import hashlib +import json +import os +import socket +import subprocess +import sys +import tempfile +import time +import zipfile +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path + +import httpx +from real_server import HarnessFailure, control_pipe + + +def wait_ready(client: httpx.Client, process: subprocess.Popen[bytes]) -> None: + for _ in range(150): + if process.poll() is not None: + raise HarnessFailure("installed_server_exited") + try: + if client.get("/health/ready").is_success: + return + except httpx.HTTPError: + pass + time.sleep(0.2) + raise HarnessFailure("installed_server_readiness_timeout") + + +@contextmanager +def isolated_server(response_loss_counter: Path | None = None) -> Iterator[tuple[httpx.Client, str, str]]: + desktop = Path(__file__).resolve().parents[1] + wheels = list((desktop / ".artifacts/server-wheel").glob("*.whl")) + if len(wheels) != 1: + raise HarnessFailure("installed_server_wheel_count") + wheel = wheels[0] + with tempfile.TemporaryDirectory(prefix="desktop-installed-server-") as directory: + temp = Path(directory) + wheel_root = temp / "wheel" + with zipfile.ZipFile(wheel) as archive: + archive.extractall(wheel_root) + with socket.socket() as reservation: + reservation.bind(("127.0.0.1", 0)) + port = reservation.getsockname()[1] + config = { + "wheel_root": str(wheel_root), + "workspace": str(temp), + "database": f"sqlite+aiosqlite:///{temp / 'data.db'}", + "token": None, + "prefix": "", + "port": port, + "tls": False, + "response_loss_path": str(response_loss_counter) if response_loss_counter else None, + } + config_path = temp / "server.json" + config_path.write_text(json.dumps(config), encoding="utf-8") + environment = { + k: v + for k, v in os.environ.items() + if k.upper() + in {"PATH", "SYSTEMROOT", "WINDIR", "TEMP", "TMP", "COMSPEC", "USERPROFILE", "APPDATA", "LOCALAPPDATA"} + } + with (temp / "server.log").open("w", encoding="utf-8") as log: + process = subprocess.Popen( # noqa: S603 - own interpreter and fixed fixture script + [sys.executable, "-I", str(desktop / "tests/real_server.py"), "--serve", str(config_path)], + stdin=subprocess.PIPE, + stdout=log, + stderr=log, + env=environment, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + ) + control = control_pipe(process) + try: + with httpx.Client(base_url=f"http://127.0.0.1:{port}", trust_env=False, timeout=10) as client: + wait_ready(client, process) + response = client.post( + "/v1/scopes", + json={ + "title": "Desktop installed CI", + "summary": "Synthetic installed UI acceptance", + "idempotency_key": "desktop-installed-ui-scope", + }, + ) + response.raise_for_status() + yield client, response.json()["scope_id"], hashlib.sha256(wheel.read_bytes()).hexdigest() + finally: + if process.poll() is None: + control.write(b"stop\n") + control.flush() + try: + process.wait(timeout=30) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=10) + control.close() diff --git a/desktop/tests/installed_lifecycle.py b/desktop/tests/installed_lifecycle.py new file mode 100644 index 000000000..204d52244 --- /dev/null +++ b/desktop/tests/installed_lifecycle.py @@ -0,0 +1,71 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Verify independent Server availability after forced installed-app termination.""" + +from __future__ import annotations + +import subprocess + +import httpx +from installed_fixture import isolated_server +from installed_workflow import InstalledPage +from real_server import HarnessFailure + + +def exercise_forced_exit(client: httpx.Client, prefix: str, app: subprocess.Popen[bytes]) -> dict[str, object]: + page = InstalledPage(client, prefix) + note = "desktoplifecyclectest 异常关闭前保存的记忆" + with isolated_server() as (server, scope, wheel_digest): + page.connect("Desktop CI lifecycle", str(server.base_url).rstrip("/")) + page.select_scope(scope) + page.type("记忆内容", note, "textarea") + page.button("保存记忆") + # The preceding workflow intentionally leaves an unknown last write. A new + # explicit write must still pass the product's duplicate-risk confirmation. + alert = client.get(prefix + "/alert/text") + alert.raise_for_status() + expected_prompt = "上次提交结果未知,再次保存可能产生重复记录。仍要提交这次输入吗?" # noqa: RUF001 - exact localized UI + if alert.json()["value"] != expected_prompt: + raise HarnessFailure("installed_unknown_retry_confirmation_missing") + page.post("/alert/accept", {}) + page.wait_text("保存成功。") + citation = page.search_read(note, "desktoplifecyclectest") + if app.poll() is not None: + raise HarnessFailure("installed_app_exited_before_forced_exit") + # Kill only the exact application process launched by this harness. + app.kill() + app.wait(timeout=15) + server.get("/health/ready").raise_for_status() + original = server.post("/v1/memory/entries/get", json={"scope_id": scope, "citation": citation}) + original.raise_for_status() + if original.json()["text"] != note or original.json()["citation"] != citation: + raise HarnessFailure("installed_forced_exit_changed_saved_memory") + after_text = "Independent write after Desktop forced exit" + written = server.post("/v1/memory/remember", json={"scope_id": scope, "kind": "note", "text": after_text}) + written.raise_for_status() + after_citation = written.json()["entry"]["citation"] + read_back = server.post("/v1/memory/entries/get", json={"scope_id": scope, "citation": after_citation}) + read_back.raise_for_status() + if read_back.json()["text"] != after_text or read_back.json()["citation"] != after_citation: + raise HarnessFailure("installed_forced_exit_independent_write_unreadable") + return { + "serverWheelSha256": wheel_digest, + "applicationExitCode": app.returncode, + "explicitSaveAfterUnknownConfirmed": True, + "forcedOwnedApplicationExit": True, + "serverReadyAfterExit": True, + "originalExactReadAfterExit": True, + "independentWriteAndExactReadAfterExit": True, + } diff --git a/desktop/tests/installed_ui.py b/desktop/tests/installed_ui.py new file mode 100644 index 000000000..9d7e7c6c4 --- /dev/null +++ b/desktop/tests/installed_ui.py @@ -0,0 +1,247 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exercise the installed WebView2 application only on a disposable Windows CI runner.""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import socket +import subprocess +import sys +import time +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path + +import httpx +from installed_lifecycle import exercise_forced_exit +from installed_workflow import exercise_memory +from real_server import HarnessFailure + + +def free_port() -> int: + with socket.socket() as reservation: + reservation.bind(("127.0.0.1", 0)) + return reservation.getsockname()[1] + + +@contextmanager +def owned_process(command: list[str], environment: dict[str, str], log_path: Path) -> Iterator[subprocess.Popen[bytes]]: + with log_path.open("w", encoding="utf-8") as log: + process = subprocess.Popen( # noqa: S603 - exact installed app or verified driver, only on CI + command, + stdout=log, + stderr=log, + env=environment, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + ) + try: + yield process + finally: + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=10) + + +def wait_endpoint(client: httpx.Client, path: str, process: subprocess.Popen[bytes], stage: str) -> None: + for _ in range(150): + if process.poll() is not None: + raise HarnessFailure(stage + "_process_exited", str(process.returncode)) + try: + if client.get(path, timeout=1).is_success: + return + except httpx.HTTPError: + pass + time.sleep(0.2) + raise HarnessFailure(stage + "_readiness_timeout") + + +def wait_packaged_page(client: httpx.Client, prefix: str) -> str: + for _ in range(100): + response = client.post( + prefix + "/execute/sync", + json={ + "script": "return {url:location.href,text:document.body.innerText};", + "args": [], + }, + ) + response.raise_for_status() + page = response.json()["value"] + if "总览" in page["text"]: + if not page["url"].startswith("http://tauri.localhost"): + raise HarnessFailure("installed_resources_not_packaged") + return page["url"] + time.sleep(0.2) + raise HarnessFailure("installed_overview_not_rendered") + + +def screenshot(client: httpx.Client, prefix: str, artifacts: Path) -> bool: + try: + response = client.get(prefix + "/screenshot", timeout=10) + response.raise_for_status() + (artifacts / "installed-ui.png").write_bytes(base64.b64decode(response.json()["value"])) + except (httpx.HTTPError, ValueError, KeyError): + return False + else: + return True + + +def capture_process(process: subprocess.Popen[bytes], artifacts: Path) -> bool: + script = Path(__file__).resolve().parents[1] / "scripts/capture-installed-process.ps1" + shell = Path(os.environ["PROGRAMFILES"]) / "PowerShell/7/pwsh.exe" + try: + result = subprocess.run( # noqa: S603 - fixed diagnostic script, own CI app PID + [ + str(shell), + "-NoProfile", + "-File", + str(script), + "-ApplicationPid", + str(process.pid), + "-ArtifactDirectory", + str(artifacts), + ], + capture_output=True, + timeout=20, + check=False, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, + ) + except (OSError, subprocess.TimeoutExpired): + return False + else: + return result.returncode == 0 + + +@contextmanager +def debug_policy(arguments: str) -> Iterator[None]: + if os.name != "nt" or os.environ.get("GITHUB_ACTIONS") != "true": + raise HarnessFailure("disposable_windows_github_runner_required") + import winreg + + path = r"SOFTWARE\Policies\Microsoft\Edge\WebView2\AdditionalBrowserArguments" + name = "powercontext-desktop.exe" + with winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, path, access=winreg.KEY_QUERY_VALUE | winreg.KEY_SET_VALUE + ) as key: + try: + previous = winreg.QueryValueEx(key, name) + except FileNotFoundError: + previous = None + # Elevated WebView2 hosts ignore user environment overrides. Limit this temporary + # machine override to the exact CI app; never alter the wildcard/default policy. + winreg.SetValueEx(key, name, 0, winreg.REG_SZ, arguments) + try: + yield + finally: + if previous is None: + winreg.DeleteValue(key, name) + else: + winreg.SetValueEx(key, name, 0, previous[1], previous[0]) + + +def run_ui(executable: Path, driver: Path, artifacts: Path, report: dict[str, object]) -> None: + debug_address = f"127.0.0.1:{free_port()}" + driver_port = free_port() + environment = dict(os.environ, TAURI_WEBVIEW_AUTOMATION="true") + debug_arguments = f"--remote-debugging-port={debug_address.rsplit(':', 1)[1]} --remote-debugging-address=127.0.0.1" + environment.pop("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", None) + with ( + debug_policy(debug_arguments), + owned_process([str(executable)], environment, artifacts / "installed-ui-app.log") as app, + httpx.Client(base_url=f"http://{debug_address}", trust_env=False) as debug, + ): + report["stage"] = "application_start" + try: + wait_endpoint(debug, "/json/version", app, "installed_webview") + except Exception: + report["processSnapshotCaptured"] = capture_process(app, artifacts) + raise + report["webviewDebugEndpointReady"] = True + with ( + owned_process( + [str(driver), f"--port={driver_port}", "--host=127.0.0.1", "--verbose"], + environment, + artifacts / "installed-ui-driver.log", + ) as process, + httpx.Client(base_url=f"http://127.0.0.1:{driver_port}", trust_env=False, timeout=60) as client, + ): + report["stage"] = "driver_start" + wait_endpoint(client, "/status", process, "webdriver") + report["stage"] = "session_attach" + created = client.post( + "/session", + json={ + "capabilities": { + "alwaysMatch": { + "browserName": "webview2", + "ms:edgeChromium": True, + "ms:edgeOptions": {"debuggerAddress": debug_address}, + } + } + }, + timeout=120, + ) + created.raise_for_status() + value = created.json()["value"] + prefix = f"/session/{value['sessionId']}" + report["browserVersion"] = value["capabilities"].get("browserVersion") + try: + report["stage"] = "packaged_page" + report["packagedUrl"] = wait_packaged_page(client, prefix) + report["stage"] = "memory_workflow" + report["workflow"] = exercise_memory(client, prefix) + report["screenshotCaptured"] = screenshot(client, prefix, artifacts) + report["stage"] = "forced_exit" + report["lifecycle"] = exercise_forced_exit(client, prefix, app) + finally: + if app.poll() is None: + report["screenshotCaptured"] = screenshot(client, prefix, artifacts) + client.delete(prefix).raise_for_status() + report["stage"] = "complete" + report["result"] = "passed" + + +def main() -> None: + if os.name != "nt" or os.environ.get("GITHUB_ACTIONS") != "true": + raise HarnessFailure("disposable_windows_github_runner_required") + executable = Path(sys.argv[1]).resolve(strict=True) + driver = Path(os.environ["DESKTOP_EDGE_DRIVER"]).resolve(strict=True) + artifacts = Path(__file__).resolve().parents[1] / ".artifacts" + report: dict[str, object] = { + "commit": os.environ.get("GITHUB_SHA"), + "sourceInstallerCommit": os.environ.get("DESKTOP_INSTALLER_COMMIT", os.environ.get("GITHUB_SHA")), + "debugConfiguration": "Temporary machine WebView2 policy for powercontext-desktop.exe on disposable runner; restored on exit", + "installedExecutableSha256": hashlib.sha256(executable.read_bytes()).hexdigest(), + "driverSha256": hashlib.sha256(driver.read_bytes()).hexdigest(), + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "failed", + } + try: + run_ui(executable, driver, artifacts, report) + finally: + (artifacts / "installed-ui.json").write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main() diff --git a/desktop/tests/installed_workflow.py b/desktop/tests/installed_workflow.py new file mode 100644 index 000000000..e2c6f9490 --- /dev/null +++ b/desktop/tests/installed_workflow.py @@ -0,0 +1,316 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""W3C WebDriver interactions with the real installed application and Server.""" + +from __future__ import annotations + +import json +import tempfile +import time +from pathlib import Path + +import httpx +from installed_boundaries import exercise_note_budget, exercise_search_limit +from installed_fixture import isolated_server +from real_server import HarnessFailure + +ELEMENT = "element-6066-11e4-a52e-4f735466cecf" +NOTE = "desktopinstalledci 中文安装后验收\n纯文本 café " + + +class InstalledPage: + def __init__(self, client: httpx.Client, prefix: str) -> None: + self.client = client + self.prefix = prefix + + def post(self, path: str, payload: dict[str, object]): + response = self.client.post(self.prefix + path, json=payload) + response.raise_for_status() + return response.json()["value"] + + def element(self, xpath: str) -> str: + for _ in range(100): + response = self.client.post(self.prefix + "/element", json={"using": "xpath", "value": xpath}) + if response.is_success: + identifier = response.json()["value"][ELEMENT] + enabled = self.client.get(self.prefix + f"/element/{identifier}/enabled") + enabled.raise_for_status() + if enabled.json()["value"]: + return identifier + elif response.json().get("value", {}).get("error") not in {"no such element", "stale element reference"}: + response.raise_for_status() + time.sleep(0.2) + raise HarnessFailure("installed_element_timeout", xpath) + + def click_element(self, identifier: str) -> None: + # Scroll through the browser before a real pointer click. WebDriver's + # automatic edge alignment can put the target behind a wrapping sticky bar. + self.observe( + "arguments[0].scrollIntoView({block: 'center', inline: 'nearest'});", + [{ELEMENT: identifier}], + ) + self.post(f"/element/{identifier}/click", {}) + + def click(self, xpath: str) -> None: + self.click_element(self.element(xpath)) + + def button(self, text: str) -> None: + self.click(f"//button[normalize-space(.)='{text}']") + + def field(self, label: str, tag: str = "input") -> str: + return self.element(f"//label[normalize-space(text())='{label}']//{tag}") + + def type(self, label: str, value: str, tag: str = "input") -> None: + self.post(f"/element/{self.field(label, tag)}/value", {"text": value}) + + def observe(self, script: str, args: list[object] | None = None): + return self.post("/execute/sync", {"script": script, "args": args or []}) + + def wait(self, script: str, args: list[object] | None = None) -> None: + for _ in range(100): + if self.observe(script, args): + return + time.sleep(0.2) + raise HarnessFailure("installed_observation_timeout") + + def wait_text(self, text: str) -> None: + self.wait("return document.body.innerText.includes(arguments[0]);", [text]) + + def profile(self, name: str) -> None: + self.click( + f"//ul[@class='profile-list']//span[@class='profile-name'][normalize-space(.)='{name}']/ancestor::button" + ) + + def open_connection_menu(self) -> None: + self.click("//div[contains(@class,'topbar')]/div[contains(@class,'menu-wrap')][1]/button") + + def activate(self, name: str) -> None: + self.button("使用此连接") + self.element( + f"//ul[@class='profile-list']/li[.//span[@class='profile-name'][normalize-space(.)='{name}']]//span[contains(@class,'badge')]" + ) + self.button("记忆") + + def connect(self, name: str, endpoint: str) -> None: + self.button("连接") + self.type("连接名称", name) + self.type("Server 地址", endpoint) + self.click("//label[normalize-space(text())='已验证兼容配置']/select/option[@value='sqlite-1.1.1-v1']") + self.button("保存配置") + self.activate(name) + + def select_scope(self, scope_id: str) -> None: + self.button("精确范围") + self.click("//summary[normalize-space(.)='精确 Scope ID']") + self.type("精确 Scope ID", scope_id) + self.button("选择范围") + self.wait_text("当前范围: Desktop installed CI") + self.button("关闭") + + def expect_empty_context(self) -> None: + observed = self.observe("""return { + reader: !!document.querySelector('.reader'), + hits: document.querySelectorAll('.memory-hits li').length, + draft: document.querySelector('.memory-workspace textarea')?.value, + query: [...document.querySelectorAll('label')].find( + label => label.textContent.trim() === '全文搜索关键词')?.querySelector('input')?.value + };""") + if observed != {"reader": False, "hits": 0, "draft": "", "query": ""}: + raise HarnessFailure("installed_previous_context_not_cleared") + + def search_read(self, text: str, keyword: str = "desktopinstalledci") -> dict[str, object]: + self.type("全文搜索关键词", keyword) + self.button("搜索") + self.button("阅读精确版本") + self.wait_text("记忆详情") + if self.observe("return document.querySelector('.reader > .plain-text')?.textContent;") != text: + raise HarnessFailure("installed_exact_body_mismatch") + return json.loads(self.observe("return document.querySelector('.reader pre')?.textContent;")) + + def paste(self) -> str: + field = self.field("记忆内容", "textarea") + self.click_element(field) + self.post(f"/element/{field}/value", {"text": "\ue009v\ue000"}) + for _ in range(100): + value = self.observe("return arguments[0].value;", [{ELEMENT: field}]) + if value: + return value + time.sleep(0.2) + raise HarnessFailure("installed_clipboard_paste_timeout") + + def clear_note(self) -> None: + field = self.field("记忆内容", "textarea") + # Keys preserve React input events and do not invoke product internals. + self.post(f"/element/{field}/value", {"text": "\ue009a\ue000\ue003"}) + if self.observe("return arguments[0].value;", [{ELEMENT: field}]) != "": + raise HarnessFailure("installed_note_not_cleared") + + +def exercise_memory(client: httpx.Client, prefix: str) -> dict[str, object]: + page = InstalledPage(client, prefix) + with isolated_server() as (server, scope_id, wheel_digest): + page.connect("Desktop CI synthetic", str(server.base_url).rstrip("/")) + page.select_scope(scope_id) + exercise_note_budget(page, server, scope_id) + page.type("记忆内容", NOTE, "textarea") + before_submit = server.post( + "/v1/memory/search", json={"scope_id": scope_id, "query": "desktopinstalledci", "mode": "fts", "limit": 10} + ) + before_submit.raise_for_status() + if before_submit.json()["hits"]: + raise HarnessFailure("installed_enter_submitted_without_button") + page.button("保存记忆") + page.wait_text("保存成功。") + citation = page.search_read(NOTE) + response = server.post("/v1/memory/entries/get", json={"scope_id": scope_id, "citation": citation}) + response.raise_for_status() + if response.json()["text"] != NOTE or response.json()["citation"] != citation: + raise HarnessFailure("installed_independent_exact_read_mismatch") + page.button("复制正文") + page.wait_text("已复制") + if page.paste() != NOTE: + raise HarnessFailure("installed_body_clipboard_mismatch") + page.clear_note() + page.button("复制精确引用") + if json.loads(page.paste()) != citation: + raise HarnessFailure("installed_citation_clipboard_mismatch") + page.clear_note() + exercise_search_limit(page, server, scope_id) + exercise_connection_isolation(page, server, scope_id, citation) + exercise_unknown_write(page) + return { + "serverWheelSha256": wheel_digest, + "mode": "anonymous loopback SQLite, no model", + "explicitConnectionAndScope": True, + "saveSearchExactRead": True, + "independentServerExactRead": True, + "bodyAndCitationClipboardPaste": True, + "twoServerConnectionIsolation": True, + "disconnectReconnectClearsContent": True, + "unsavedDraftCancelAndDiscard": True, + "inactiveProfileRemovalPreservesServerData": True, + "enterDoesNotSubmit": True, + "rawUtf8BudgetBoundary": True, + "emptyAndCappedSearchPresentation": True, + "committedLostResponseUnknownWithoutReplay": True, + } + + +def current_unchanged_entry(server: httpx.Client, scope: str, original: dict[str, object]) -> dict[str, object]: + current = server.post( + "/v1/memory/search", + json={"scope_id": scope, "query": "desktopinstalledci", "mode": "fts", "limit": 10}, + ) + current.raise_for_status() + current_hits = current.json()["hits"] + if len(current_hits) != 1: + raise HarnessFailure("installed_original_server_search_ambiguous") + current_citation_a = current_hits[0]["citation"] + # New independent notes advance the artifact revision, while this entry's + # version remains unchanged. Preserve the original citation for exact reads. + if any(current_citation_a[key] != original[key] for key in ("entry_id", "entry_version_id")): + raise HarnessFailure("installed_original_entry_version_changed") + return current_citation_a + + +def exercise_connection_isolation( + page: InstalledPage, + server_a: httpx.Client, + scope_a: str, + citation_a: dict[str, object], +) -> None: + current_citation_a = current_unchanged_entry(server_a, scope_a, citation_a) + text_b = "desktopinstalledci B 独立服务中的另一条记忆" + with isolated_server() as (server_b, scope_b, _): + seeded = server_b.post("/v1/memory/remember", json={"scope_id": scope_b, "kind": "note", "text": text_b}) + seeded.raise_for_status() + citation_b = seeded.json()["entry"]["citation"] + page.connect("Desktop CI B", str(server_b.base_url).rstrip("/")) + page.expect_empty_context() + page.select_scope(scope_b) + if page.search_read(text_b) != citation_b: + raise HarnessFailure("installed_second_server_citation_mismatch") + draft = "desktopunsavedci 不应写入的草稿" + page.type("记忆内容", draft, "textarea") + page.open_connection_menu() + page.button("断开桌面连接") + alert = page.client.get(page.prefix + "/alert/text") + alert.raise_for_status() + if alert.json()["value"] != "丢弃尚未保存的输入?": # noqa: RUF001 - exact localized UI + raise HarnessFailure("installed_disconnect_discard_confirmation_missing") + page.post("/alert/dismiss", {}) + if page.observe("return document.querySelector('.memory-workspace textarea')?.value;") != draft: + raise HarnessFailure("installed_cancel_disconnect_lost_draft") + if page.observe("return document.querySelector('.reader > .plain-text')?.textContent;") != text_b: + raise HarnessFailure("installed_cancel_disconnect_changed_reader") + page.open_connection_menu() + page.button("断开桌面连接") + page.post("/alert/accept", {}) + page.wait_text("尚未连接") + page.expect_empty_context() + page.button("连接") + page.profile("Desktop CI synthetic") + page.activate("Desktop CI synthetic") + page.expect_empty_context() + page.select_scope(scope_a) + if page.search_read(NOTE) != current_citation_a: + raise HarnessFailure("installed_reconnected_citation_mismatch") + page.button("连接") + page.profile("Desktop CI B") + page.button("移除连接") + page.post("/alert/accept", {}) + page.wait("""return ![...document.querySelectorAll('.profile-name')].some( + name => name.textContent.trim() === 'Desktop CI B');""") + for server, scope, citation, expected in ( + (server_a, scope_a, citation_a, NOTE), + (server_b, scope_b, citation_b, text_b), + ): + response = server.post("/v1/memory/entries/get", json={"scope_id": scope, "citation": citation}) + response.raise_for_status() + if response.json()["text"] != expected: + raise HarnessFailure("installed_profile_operation_changed_server_data") + page.button("记忆") + if page.search_read(NOTE) != current_citation_a: + raise HarnessFailure("installed_inactive_profile_removal_changed_active_connection") + + +def exercise_unknown_write(page: InstalledPage) -> None: + note = "desktoplostuici 已提交但响应丢失的中文记忆" + with tempfile.TemporaryDirectory(prefix="desktop-ui-response-loss-") as directory: + counter = Path(directory) / "remember-count" + counter.write_text("0", encoding="utf-8") + with isolated_server(response_loss_counter=counter) as (server, scope, _): + page.connect("Desktop CI response loss", str(server.base_url).rstrip("/")) + page.select_scope(scope) + page.type("记忆内容", note, "textarea") + page.button("保存记忆") + page.wait_text("提交结果未知。") + if page.observe("return document.querySelector('.memory-workspace textarea')?.value;") != note: + raise HarnessFailure("installed_unknown_write_lost_draft") + citation = page.search_read(note, "desktoplostuici") + matches = server.post( + "/v1/memory/search", + json={ + "scope_id": scope, + "query": "desktoplostuici", + "mode": "fts", + "limit": 10, + }, + ) + matches.raise_for_status() + hits = matches.json()["hits"] + if len(hits) != 1 or hits[0]["citation"] != citation or counter.read_text(encoding="utf-8") != "1": + raise HarnessFailure("installed_unknown_write_replayed_or_missing") + page.clear_note() diff --git a/desktop/tests/real_cli.py b/desktop/tests/real_cli.py new file mode 100644 index 000000000..8f2da4329 --- /dev/null +++ b/desktop/tests/real_cli.py @@ -0,0 +1,97 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exercise the registered native adapter against a real CLI with an isolated home and PATH.""" + +# Fixed local test programs; this harness never runs a renderer-supplied command. +# ruff: noqa: S603 +import hashlib +import json +import os +import shutil +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +def main(): + root = Path(__file__).resolve().parents[2] + artifacts = root / "desktop/.artifacts" + (wheel,) = (artifacts / "server-wheel").glob("*.whl") + with tempfile.TemporaryDirectory(prefix="desktop-real-cli-") as directory: + fixture = Path(directory) + package = fixture / "package" + with zipfile.ZipFile(wheel) as archive: + archive.extractall(package) + (metadata,) = package.glob("*.dist-info/METADATA") + version = next( + line.removeprefix("Version: ") + for line in metadata.read_text(encoding="utf-8").splitlines() + if line.startswith("Version: ") + ) + executable = fixture / "powercontext.exe" + shutil.copyfile(root / ".venv/Scripts/powercontext.exe", executable) + registration = fixture / "diagnostic-cli.json" + digest = hashlib.sha256(executable.read_bytes()).hexdigest() + registration.write_text( + json.dumps({ + "executable": str(executable), + "sha256": digest, + "version": version, + "source": "explicit_local_installation", + }), + encoding="utf-8", + ) + system = os.environ["SYSTEMROOT"] + environment = { + "SYSTEMROOT": system, + "WINDIR": system, + "PATH": str(Path(system) / "System32"), + "USERPROFILE": str(fixture), + "HOME": str(fixture), + "APPDATA": str(fixture / "roaming"), + "LOCALAPPDATA": str(fixture / "local"), + "TEMP": str(fixture), + "TMP": str(fixture), + "PYTHONPATH": str(package), + "PYTHONNOUSERSITE": "1", + "PYTHONUTF8": "1", + } + result = subprocess.run( + [str(root / "desktop/src-tauri/target/debug/examples/diagnostic_cli_probe.exe"), str(registration)], + env=environment, + capture_output=True, + text=True, + encoding="utf-8", + timeout=110, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + if result.returncode: + raise RuntimeError(result.stderr[-2000:]) + report = { + "version": version, + "wheelSha256": hashlib.sha256(wheel.read_bytes()).hexdigest(), + "launcherSha256": digest, + "environment": "Isolated home and system-only PATH; wheel extracted on explicit test PYTHONPATH", + "result": json.loads(result.stdout), + } + (artifacts / "real-cli.json").write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + print(json.dumps(report, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/desktop/tests/real_server.py b/desktop/tests/real_server.py new file mode 100644 index 000000000..bd761880c --- /dev/null +++ b/desktop/tests/real_server.py @@ -0,0 +1,370 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Run the native Desktop adapter against an isolated, built-wheel SQLite Server. + +No product command starts a Server. This explicit test harness owns its disposable processes and data. +""" + +# All subprocesses below are fixed local test executables, never renderer-provided commands. +# ruff: noqa: S603, S607 +from __future__ import annotations + +import asyncio +import hashlib +import json +import os +import platform +import secrets +import socket +import ssl +import subprocess +import sys +import tempfile +import threading +import time +import zipfile +from pathlib import Path +from typing import IO + + +class HarnessFailure(RuntimeError): + def __init__(self, code: str, detail: str = "") -> None: + super().__init__(f"Desktop isolated fixture failed ({code}): {detail}") + + +def probe_measurement(output: str) -> dict[str, object]: + measurement = json.loads(output) + if measurement.get("result") != "passed": + raise HarnessFailure("native_probe_result") + return measurement["performance"] + + +def fixture_provider(config, admin): + from powercontext.server.authentication import ( + AuthenticationRejectedError, + AuthenticationResult, + ProviderReadiness, + ) + from powercontext.server.authz import PrincipalRef + + class FixtureProvider: + async def authenticate(self, request): + if request.headers.get("authorization") == f"Bearer {config['reader_token']}": + return AuthenticationResult( + subject=PrincipalRef(type="user", id="desktop-fixture-reader"), + credential_id="desktop-test-reader", + ) + if request.headers.get("authorization") != f"Bearer {config['token']}": + raise AuthenticationRejectedError + subject = ( + PrincipalRef(type="user", id="desktop-fixture-changed") + if await asyncio.to_thread(Path(config["identity_change_path"]).exists) + else admin + ) + return AuthenticationResult(subject=subject, credential_id="desktop-test-provider") + + async def readiness(self): + return ProviderReadiness(ready=True) + + return FixtureProvider() + + +async def forward_with_response_loss(app, scope, receive, send, control_path): + """Commit the real request, then deliberately truncate its response on the wire.""" + control = Path(control_path) if control_path else None + if ( + scope["type"] != "http" + or scope["path"] != "/v1/memory/remember" + or control is None + or not await asyncio.to_thread(control.exists) + ): + await app(scope, receive, send) + return + count = int(await asyncio.to_thread(control.read_text, encoding="utf-8")) + await asyncio.to_thread(control.write_text, str(count + 1), encoding="utf-8") + messages = [] + + async def capture(message): + messages.append(message) + + await app(scope, receive, capture) + start = messages[0] + if start["status"] != 200: + for message in messages: + await send(message) + return + await send(start) + await send({"type": "http.response.body", "body": b"{", "more_body": True}) + raise ConnectionResetError + + +def serve(config_path: Path) -> None: + config = json.loads(config_path.read_text(encoding="utf-8")) + sys.path.insert(0, config["wheel_root"]) + import uvicorn + from pydantic import SecretStr + + from powercontext.builtin.persistence.sqlite import SQLiteConfig + from powercontext.builtin.runtime.config import ExternalSkillsConfig, InferenceConfig + from powercontext.server.factory import create_server_app + from powercontext.server.settings import BearerAuthConfig, McpConfig, ServerLoggingConfig, ServerSettings + + settings = ServerSettings( + workspace=Path(config["workspace"]), + database=SQLiteConfig(url=config["database"]), + inference=InferenceConfig(), + external_skills=ExternalSkillsConfig(), + auth=BearerAuthConfig( + enabled=config["token"] is not None, token=SecretStr(config["token"]) if config["token"] else None + ), + mcp=McpConfig(enabled=False), + logging=ServerLoggingConfig(level="CRITICAL", access=False), + ) + app = create_server_app(settings=settings) + + async def proxy(scope, receive, send): + if scope["type"] == "http": + prefix = config["prefix"] + if not scope["path"].startswith(prefix + "/"): + await send({"type": "http.response.start", "status": 404, "headers": []}) + await send({"type": "http.response.body", "body": b""}) + return + scope = dict(scope) + scope["path"] = scope["path"][len(prefix) :] + scope["raw_path"] = scope["path"].encode() + scope["root_path"] = prefix + await forward_with_response_loss(app, scope, receive, send, config.get("response_loss_path")) + + server = uvicorn.Server( + uvicorn.Config( + proxy, + host="127.0.0.1", + port=config["port"], + log_level="critical", + access_log=False, + ssl_keyfile=config["key"] if config["tls"] else None, + ssl_certfile=config["certificate"] if config["tls"] else None, + ) + ) + + def shutdown_on_input(): + sys.stdin.readline() + server.should_exit = True + + threading.Thread(target=shutdown_on_input, daemon=True).start() + + async def run(): + nonlocal app + if not config.get("provider"): + await server.serve() + return + from powercontext.server.authz import PrincipalRef + from powercontext.server.authz.composition import open_builtin_access_control + from powercontext.server.settings import AccessControlConfig + + admin = PrincipalRef(type="service", id="desktop-fixture-admin") + + async with open_builtin_access_control( + settings.database, bootstrap_administrators=(admin,), deployment_id="desktop-fixture" + ) as access: + app = create_server_app( + settings=settings.model_copy( + update={"access": AccessControlConfig(mode="enforced", deployment_id="desktop-fixture")} + ), + authentication_provider=fixture_provider(config, admin), + access_control=access, + ) + await server.serve() + + asyncio.run(run()) + + +def control_pipe(process: subprocess.Popen[bytes]) -> IO[bytes]: + if process.stdin is None: + process.kill() + process.wait(timeout=10) + raise HarnessFailure("missing_control_pipe") + return process.stdin + + +def main() -> None: + import httpx + + root = Path(__file__).resolve().parents[2] + desktop = root / "desktop" + artifact_dir = desktop / ".artifacts" + wheels = list((artifact_dir / "server-wheel").glob("*.whl")) + if len(wheels) != 1: + raise HarnessFailure("wheel_count") + wheel = wheels[0] + executable = ( + desktop / "src-tauri/target/debug/examples" / ("server_probe.exe" if os.name == "nt" else "server_probe") + ) + reports = [] + with tempfile.TemporaryDirectory(prefix="desktop-real-server-") as directory: + temp = Path(directory) + wheel_root = temp / "wheel" + with zipfile.ZipFile(wheel) as archive: + archive.extractall(wheel_root) + subprocess.run([str(executable), "certificates", str(temp)], check=True, capture_output=True) + for mode in ("loopback-anonymous", "loopback-bearer", "https-bearer-base-path", "loopback-provider"): + case = temp / mode + case.mkdir() + with socket.socket() as reservation: + reservation.bind(("127.0.0.1", 0)) + port = reservation.getsockname()[1] + tls = mode.startswith("https") + token = None if mode.endswith("anonymous") else secrets.token_urlsafe(32) + prefix = "/proxy" if tls else "" + endpoint = f"{'https' if tls else 'http'}://127.0.0.1:{port}{prefix}" + config = { + "response_loss_path": str(case / "response-loss-count"), + "reader_token": secrets.token_urlsafe(32), + "provider": mode == "loopback-provider", + "identity_change_path": str(case / "identity-changed"), + "wheel_root": str(wheel_root), + "workspace": str(case), + "database": f"sqlite+aiosqlite:///{case / 'data.db'}", + "token": token, + "prefix": prefix, + "port": port, + "tls": tls, + "key": str(temp / "server-key.pem"), + "certificate": str(temp / "server.pem"), + } + config_path = case / "server.json" + config_path.write_text(json.dumps(config), encoding="utf-8") + allowed_env = { + k: v + for k, v in os.environ.items() + if k.upper() + in {"PATH", "SYSTEMROOT", "WINDIR", "TEMP", "TMP", "COMSPEC", "USERPROFILE", "APPDATA", "LOCALAPPDATA"} + } + flags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 + with (case / "server.log").open("w", encoding="utf-8") as log: + process = subprocess.Popen( + [sys.executable, "-I", str(Path(__file__).resolve()), "--serve", str(config_path)], + stdin=subprocess.PIPE, + stdout=log, + stderr=log, + env=allowed_env, + creationflags=flags, + ) + control = control_pipe(process) + context = ssl.create_default_context(cafile=str(temp / "ca.pem")) if tls else True + try: + with httpx.Client( + base_url=endpoint, + verify=context, + trust_env=False, + timeout=3, + headers={"Authorization": f"Bearer {token}"} if token else {}, + ) as client: + for _ in range(100): + if process.poll() is not None: + raise HarnessFailure( + "startup", (case / "server.log").read_text(encoding="utf-8")[-3000:] + ) + try: + if client.get("/health/ready").status_code == 200: + break + except httpx.HTTPError: + pass + time.sleep(0.2) + else: + raise HarnessFailure("readiness_timeout") + result = client.post( + "/v1/scopes", + json={ + "title": "Desktop synthetic fixture", + "summary": "Dedicated no-model validation", + "idempotency_key": "desktop-fixture-scope", + }, + ) + result.raise_for_status() + scope_id = result.json()["scope_id"] + fixture = { + "response_loss_path": config["response_loss_path"], + "reader_token": config["reader_token"] if config["provider"] else None, + "identity_change_path": config["identity_change_path"] if config["provider"] else None, + "endpoint": endpoint, + "scope_id": scope_id, + "token": token, + "ca_pem": (temp / "ca.pem").read_text(encoding="utf-8") if tls else None, + } + fixture_path = case / "client.json" + fixture_path.write_text(json.dumps(fixture), encoding="utf-8") + probe = subprocess.run( + [str(executable), str(fixture_path)], + capture_output=True, + text=True, + timeout=120, + creationflags=flags, + ) + if probe.returncode: + raise HarnessFailure(mode, probe.stderr[-2000:]) + client.get("/health/live").raise_for_status() + reports.append({ + "mode": mode, + "result": "passed", + "serverAliveAfterClientExit": True, + "committedWriteWithLostResponseUnknownWithoutReplay": True, + "providerIdentityChangeInvalidatesContext": bool(config["provider"]), + "sameIdentityRevocationDeniesHistoricalCitation": bool(config["provider"]), + "sameTitleScopePagination51": bool(config["provider"]), + "performance": probe_measurement(probe.stdout), + }) + finally: + if process.poll() is None: + control.write(b"stop\n") + control.flush() + try: + process.wait(timeout=30) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=10) + control.close() + report = { + "environment": { + "os": platform.platform(), + "architecture": platform.machine(), + "python": platform.python_version(), + "clientBuildProfile": "debug", + "percentileMethod": "nearest rank", + "budget": "No approved numerical budget; observation only", + }, + "serverWheel": wheel.name, + "serverWheelSha256": hashlib.sha256(wheel.read_bytes()).hexdigest(), + "contractSha256": hashlib.sha256( + (root / "openapi/powercontext.yaml").read_text(encoding="utf-8").replace("\r\n", "\n").encode() + ).hexdigest(), + "desktopCommit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip(), + "desktopWorkingTreeDirty": bool( + subprocess.check_output(["git", "status", "--porcelain"], cwd=root, text=True).strip() + ), + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": reports, + } + (artifact_dir / "real-server.json").write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + print(json.dumps(report, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--serve": + serve(Path(sys.argv[2])) + else: + main() diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 000000000..a26918d49 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "types": [ + "vite/client", + "vitest/globals" + ], + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": [ + "ui", + "vite.config.ts" + ] +} diff --git a/desktop/ui/index.html b/desktop/ui/index.html new file mode 100644 index 000000000..861b138ea --- /dev/null +++ b/desktop/ui/index.html @@ -0,0 +1,17 @@ + + +PowerContext Desktop
diff --git a/desktop/ui/src/app/App.tsx b/desktop/ui/src/app/App.tsx new file mode 100644 index 000000000..06d32aece --- /dev/null +++ b/desktop/ui/src/app/App.tsx @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEffect, useRef, useState } from "react"; +import { messages, type Language } from "./messages"; +import { getFoundationInfo, desktopApi } from "../shared/ipc"; +import type { FoundationInfo, DesktopState } from "../generated/ipc"; +import logo from "../../../src-tauri/icons/brand.png"; +import homeIcon from "../assets/overview.svg"; +import connectionsIcon from "../assets/connections.svg"; +import memoryIcon from "../assets/memory.svg"; +import settingsIcon from "../assets/settings.svg"; + +import { TopBar } from "./TopBar"; +import { Overview } from "./Overview"; +import { MemoryWorkspace } from "./MemoryWorkspace"; +import { Diagnostics } from "./Diagnostics"; +import { Connections } from "./Connections"; +import { Scopes } from "./Scopes"; +import { connectionMessages, connectionError } from "./connection-messages"; + +type Page = "home" | "connections" | "memories" | "settings"; +type Theme = "light" | "dark" | "system"; +export function App() { + const [language, setLanguage] = useState("zh"); + const [theme, setTheme] = useState("system"); + const [page, setPage] = useState("home"); + const [menu, setMenu] = useState(false); + const [host, setHost] = useState(null); + const [desktop, setDesktop] = useState(null); + const [nativeError, setNativeError] = useState(null); + const [dirty, setDirty] = useState(false); + const [scopeOpen, setScopeOpen] = useState(false); + const [addSignal, setAddSignal] = useState(0); + const [refreshing, setRefreshing] = useState(false); + function receiveState(next: DesktopState) { + setDesktop((current) => + !current || next.generation >= current.generation ? next : current, + ); + } + function confirmSwitch() { + return !dirty || window.confirm(connectionMessages[language].discard); + } + const activeProfile = desktop?.profiles.find( + (p) => p.id === desktop.active?.connectionId, + ); + const activeReport = desktop?.active?.report; + const heading = useRef(null); + const t = messages[language]; + const ct = connectionMessages[language]; + useEffect(() => { + desktopApi.state().then(receiveState).catch(setNativeError); + getFoundationInfo() + .then(setHost) + .catch(() => setHost(null)); + }, []); + useEffect(() => { + document.documentElement.lang = language === "zh" ? "zh-CN" : "en"; + }, [language]); + useEffect(() => { + document.documentElement.dataset.theme = theme; + }, [theme]); + useEffect(() => { + if (!scopeOpen) return; + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") setScopeOpen(false); + } + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [scopeOpen]); + function navigate(next: Page) { + if (next === page || !confirmSwitch()) return; + setDirty(false); + setPage(next); + setMenu(false); + requestAnimationFrame(() => heading.current?.focus()); + } + function disconnect() { + if (!confirmSwitch()) return; + setDirty(false); + void desktopApi.disconnect().then(receiveState).catch(setNativeError); + } + async function refresh() { + setRefreshing(true); + setNativeError(null); + try { + receiveState( + desktop?.active + ? await desktopApi.check(desktop.active.connectionId, false) + : await desktopApi.state(), + ); + } catch (e) { + setNativeError(e); + } finally { + setRefreshing(false); + } + } + const subtitle = + page === "home" + ? t.tagline + : page === "connections" + ? t.connectionIntro + : page === "memories" + ? t.memoriesIntro + : t.settingsIntro; + const headingAction = + page === "home" ? ( + + ) : page === "connections" ? ( + + ) : null; + const ready = activeReport?.readiness.value?.status === "ready"; + return ( +
+ + {t.skip} + +
+ PowerContext + +
+ +
+ navigate("connections")} + onOpenScopes={() => setScopeOpen(true)} + onDisconnect={disconnect} + /> +
+
+
+

+ {t[page]} +

+

{subtitle}

+
+ {headingAction} +
+ {nativeError != null && ( +

{connectionError(nativeError, language)}

+ )} + {page === "memories" && desktop?.active && ( +
+ {activeProfile?.name} + + {activeProfile?.endpoint} + + {desktop.active.scope?.title ?? t.noScopeSelected} + + {ready ? t.serviceReady : t.unverified} + +
+ )} + {page === "home" && ( + + )} + {page === "memories" && ( + + )} + {page === "connections" && ( + + )} + {page === "settings" && ( +
+
+
+ + +
+
+ + +
+
+ +
+

{t.version}

+
+
+
{t.version}
+
{host?.version ?? "0.1.0"}
+
+
+
{t.native}
+
+ {host ? t.nativeReady : t.unavailable} +
+
+
+

{t.boundary}

+
+
+ )} +
{t.privacy}
+
+
+ {scopeOpen && ( +
{ + if (event.target === event.currentTarget) setScopeOpen(false); + }} + > +
+
+

{ct.scope}

+ +
+ +
+
+ )} +
+ ); +} diff --git a/desktop/ui/src/app/Connections.tsx b/desktop/ui/src/app/Connections.tsx new file mode 100644 index 000000000..8723dc4f1 --- /dev/null +++ b/desktop/ui/src/app/Connections.tsx @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2026 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEffect, useRef, useState } from "react"; +import type { + Authentication, + DesktopState, + ProfileView, + StorageChoice, + Fact, +} from "../generated/ipc"; +import { desktopApi } from "../shared/ipc"; +import { connectionMessages, connectionError } from "./connection-messages"; +import { messages, type Language } from "./messages"; +import connectionsIcon from "../assets/connections.svg"; + +type Props = { + state: DesktopState | null; + language: Language; + onState: (value: DesktopState) => void; + onDirty: (value: boolean) => void; + addSignal?: number; +}; +export function Connections({ + state, + language, + onState, + onDirty, + addSignal = 0, +}: Props) { + const t = connectionMessages[language]; + const [selected, setSelected] = useState(null); + const profile = state?.profiles.find((p) => p.id === selected); + const report = state?.reports.find((r) => r.connectionId === selected); + const [name, setName] = useState(""); + const [endpoint, setEndpoint] = useState(""); + const [authentication, setAuthentication] = useState( + "unauthenticated_loopback", + ); + const [secret, setSecret] = useState(""); + const [storage, setStorage] = useState("persistent"); + const [keep, setKeep] = useState(false); + const [ca, setCa] = useState(""); + const [compatibility, setCompatibility] = useState(""); + const [dirty, setDirty] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const invalidated = useRef(false); + function reset(p?: ProfileView) { + setName(p?.name ?? ""); + setEndpoint(p?.endpoint ?? ""); + setAuthentication(p?.authentication ?? "unauthenticated_loopback"); + setCa(p?.caPem ?? ""); + setCompatibility(p?.compatibility ?? ""); + setSecret(""); + setKeep( + p?.credentialState === "stored" || p?.credentialState === "session_only", + ); + setDirty(false); + onDirty(false); + setError(null); + invalidated.current = false; + } + useEffect(() => { + reset(profile); + }, [selected, profile?.revision]); + useEffect(() => { + if (addSignal > 0) select(null); + }, [addSignal]); + function change(target = false) { + setDirty(true); + onDirty(true); + if (target) { + setKeep(false); + setSecret(""); + if (profile && !invalidated.current) { + invalidated.current = true; + void desktopApi.invalidate(profile.id).then(onState).catch(setError); + } + } + } + function select(id: string | null) { + if (dirty && !window.confirm(t.discard)) return; + setSecret(""); + setSelected(id); + reset(state?.profiles.find((p) => p.id === id)); + } + async function action(work: () => Promise) { + setBusy(true); + setError(null); + try { + const next = await work(); + onState(next); + return next; + } catch (e) { + setError(e); + try { + onState(await desktopApi.state()); + } catch { + /* Keep the explicit failure visible. */ + } + } finally { + setBusy(false); + } + } + async function save() { + const input = { + id: profile?.id ?? null, + revision: profile?.revision ?? null, + name, + endpoint, + authentication, + caPem: ca || null, + compatibility: compatibility || null, + keepCredential: keep, + credential: secret ? { secret, storage } : null, + }; + setSecret(""); + const next = await action(() => desktopApi.save(input)); + if (next) { + const saved = next.profiles.find( + (p) => p.id === selected || p.name === name.trim(), + ); + if (saved) { + setSelected(saved.id); + reset(saved); + } + } + } + function fact( + value: Fact | undefined, + render: (value: T) => string, + ): string { + return value?.value != null + ? render(value.value) + : value?.error + ? connectionError(value.error, language) + : t.unverified; + } + const isActive = !!profile && state?.active?.connectionId === profile.id; + const readiness = report?.readiness.value?.status; + const hasCredential = + !!profile && + (profile.credentialState === "stored" || + profile.credentialState === "session_only") && + !invalidated.current; + return ( +
+
+
+

{t.savedConnections}

+
+ {state && state.profiles.length === 0 && ( +

{messages[language].noConnections}

+ )} +
    + {state?.profiles.map((p) => ( +
  • + +
  • + ))} +
+ {state?.pendingCredentialCleanup ? ( +

{t.cleanup}

+ ) : null} +

{t.selectHint}

+
+
+
+

{profile ? profile.name : t.add}

+ {isActive && {t.activeBadge}} +
+ {dirty &&

{t.dirty}

} + {error != null && ( +

{connectionError(error, language)}

+ )} +
+ + +

{t.endpointHint}

+ + {authentication === "bearer" && ( + <> + {hasCredential && keep ? ( +
+ +
+ {t[profile.credentialState]} +

{t.credentialNote}

+
+ +
+ ) : ( + <> + {profile && ( +

{t[profile.credentialState]}

+ )} + + + + )} + + )} + +

{t.compatibilityHint}

+
+ {t.ca} +