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..5ec058bba --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +src-tauri/target/ +src-tauri/gen/ +src-tauri/permissions/autogenerated/ +.artifacts/ 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..990a8b7ae --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,115 @@ +# 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 [qualification evidence](evidence/S1.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 [S2 evidence](evidence/S2.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 [S3 evidence](evidence/S3.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 its 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 [S2 evidence](evidence/S2.md). + +[Complete qualification matrix](evidence/S4.md) distinguishes passing checks from 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. diff --git a/desktop/README.zh.md b/desktop/README.zh.md new file mode 100644 index 000000000..45bf7e65c --- /dev/null +++ b/desktop/README.zh.md @@ -0,0 +1,97 @@ +# 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. 核对部署版本后选择已验证兼容配置,具体构建摘要和范围见 [S2 证据](evidence/S2.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)记录具体约束;[S1 验证记录](evidence/S1.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 及其开发构建;必须填写实际安装的精确版本,下列示例是已测试基线。其他版本系列需另行验证。不要把文件摘要匹配当作发布者签名认证;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 时,只报告操作成功,不编造引用。请求发出后的超时或中断可能意味着「结果未知」:先核对原连接和范围,再决定是否重新提交;相同正文不能证明属于这次操作。应用不会自动重试写入,也没有离线队列。切换连接或范围后隐藏旧结果,本次会话只保留最小操作状态和原目标。测试范围与剩余验收见 [S3 证据](evidence/S3.md)。 + +[完整验收矩阵](evidence/S4.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 条的提示,以及未保存草稿时取消或确认断开连接的行为。是否通过以对应远程报告为准。 diff --git a/desktop/SECURITY.md b/desktop/SECURITY.md new file mode 100644 index 000000000..1dfbffc85 --- /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. Packaged native and isolated machine results are tracked in [the evidence record](evidence/S1.md). + +Framework references: [Tauri capabilities](https://v2.tauri.app/security/capabilities/), [Windows installer options](https://v2.tauri.app/distribute/windows-installer/). diff --git a/desktop/evidence/S1.md b/desktop/evidence/S1.md new file mode 100644 index 000000000..6a1a57904 --- /dev/null +++ b/desktop/evidence/S1.md @@ -0,0 +1,116 @@ +# S1 foundation evidence — 2026-09-19 + +## Conclusion + +The S1 foundation is implemented and has developer-machine native smoke evidence. S1-08 and S1-09 are not complete. **S1 exit qualification and official P0 remain blocked.** There is no accepted Server compatibility profile and no claim of complete #1654 delivery. + +Implementation branch: `codex/desktop-preview`, based on upstream `aa697c5315204249e090acdf1a00d0582e851c5f` (fetched at task start). [source-manifest.json](source-manifest.json) identifies the source files used for this evidence. No backend or OpenAPI contract changes were made. + +## Remote CI scope + +The Windows workflow runs on pull requests and pushes to `codex/desktop-preview`. It uses locked dependencies, checks the UI and Rust code, verifies generated contracts, exercises the Windows vault across processes, builds the unsigned installer, and installs/uninstalls it in a Chinese path on the disposable runner. It uploads the installer and a JSON report containing package digest, signature status and lifecycle results. CI results must be read from the corresponding commit's Actions run; workflow configuration alone is not passing evidence. + +Hosted runners are developer images with preinstalled software and do not prove a clean standard-user desktop, absent WebView2, interactive rendering, notification activation or host integration. The external sentinel check proves only preservation of that synthetic file, not a real Server database. Those gates remain open independently of the draft PR. + +## Requirement coverage + +| Requirement | Evidence and remaining work | +| --- | --- | +| S1-01 | Independent worktree/branch and exact baseline recorded below; backend untouched. | +| S1-02 | Independent locks, build/CI entry points and real NSIS build; Python wheel and website regression passed. | +| S1-03 | Three-page native shell, bilingual resources/themes; installed keyboard and zoom observations below. Complete accessibility qualification remains open. | +| S1-04 | OpenAPI-generated operation manifest and schemas; Rust-derived IPC types; drift checks passed. | +| S1-05 | Explicit manifest/capability/CSP and navigation guard; native permission-resolution tests reject untrusted callers and unregistered commands. | +| S1-06 | Real Windows vault persistence across processes plus cleanup; explicit session and unavailable-vault tests. Write-only request/receipt protocol is generated from Rust and tested; live connection command belongs to S2. | +| S1-07 | Real HTTP/TLS fixtures cover loopback, base prefix, system/custom trust, hostname, proxy and authenticated redirect policy. | +| S1-08 | Internal unsigned installer and developer-machine Chinese-path smoke evidence only. Clean standard user, absent WebView2 and absent Python/Server are blocked. | +| S1-09 | Not executed: notification cold-start, independent service/login and selected Agent host require the isolated P0 environment and named responsible parties. | + +No row marked blocked is counted as completed. Existing developer-machine evidence cannot establish standard-user or complete P0 qualification. + +## Environment and package + +| Field | Observed value | +| --- | --- | +| Operator | Codex-assisted execution in the user's developer environment; no independent reviewer | +| OS | Windows 11 Pro x64, 10.0.26200 | +| CPU | AMD Ryzen 9 8945HX | +| Account | Existing developer administrator account; not a clean standard-user qualification | +| WebView2 | 153.0.4234.32, already installed | +| Node / pnpm | 24.14.1 / 11.13.1 | +| Rust | 1.95.0 (59807616e), x86_64-pc-windows-msvc | +| Toolchain | Visual Studio Build Tools, MSVC 14.44.35207 | +| Tauri / React / TypeScript / Vite | 2.11.0 / 19.2.8 / 5.9.3 / 8.3.0; exact transitives in lockfiles | +| Build | Release, packaged assets via custom-protocol, NSIS current-user installer | +| Installer | PowerContext Desktop Preview_0.1.0_x64-setup.exe | +| Installer size | 1,920,485 bytes | +| Installer SHA-256 | 9F29FF7ACDD706FB0A7B84A2BBFFBC673ECF2E705C3FE6E1318C178874FD8395 | +| Authenticode | NotSigned | +| Contract SHA-256 (LF normalized) | de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c | +| Server / Provider / Agent compatibility | None qualified in S1 | + +The installer is a local, ignored build artifact under `src-tauri/target/release/bundle/nsis/`, not a published release. The app namespace is isolated as `com.powercontext.desktop.preview`. + +## Automated validation + +| Command / check | Result | +| --- | --- | +| `pnpm --dir desktop install --frozen-lockfile` | Passed | +| `pnpm --dir desktop lint` | Passed: lint, OpenAPI drift, canonical brand drift, formatting | +| `pnpm --dir desktop typecheck` | Passed | +| `pnpm --dir desktop test` | 2 UI behavior tests passed | +| `pnpm --dir desktop build` | Passed; independently bundled UI | +| `pnpm --dir desktop ipc:check` | Passed; Rust-derived IPC types match | +| `cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check` | Passed | +| `cargo clippy --locked --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` | Passed | +| `cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml` | 14 behavior tests passed (2 vault/protocol, 2 IPC/navigation, 1 TLS, 9 transport) | +| `cargo run --locked --manifest-path desktop/src-tauri/Cargo.toml --example credential_probe` | Passed against the real Windows vault | +| `pnpm --dir desktop desktop:build` | Passed; NSIS installer created | +| `uv lock --locked` | Passed | +| Workflow immutable-action check | Passed, including the Desktop workflow | +| Integration manifest generation check | Passed | +| `uv run python -m pytest tests/test_integration_manifest.py tests/test_transport.py -q` | 85 passed | +| `uv build --wheel` and wheel file inspection | Passed; wheel contains no Desktop sources | +| Repository pre-commit checks | Formatting/lint checks passed after fixes; ty remains blocked by the seven existing Windows errors below | +| Pydantic AI integration ty check | Passed | +| Website lint/tests/build (underlying docs-test commands) | Passed; 820 public pages and their internal links verified | + +This host has no `make` executable. The underlying lock, workflow, integration-manifest, pre-commit and ty commands from `make check` were run directly. The seven existing Windows type errors are five PipeConnection/Connection mismatches and two POSIX WNOHANG references, matching the boundary tracked in #1658. Three additional deprecation warnings are not new Desktop failures. No backend ignores or patches were added. + +The native tests use actual loopback HTTP/TLS listeners: proxy-prefix preservation, auth header placement, 401/403/5xx safe projection, redirect target not contacted, known-length and chunked overflow, stalled-body total timeout, poisoned proxy environment, concurrent-read saturation/cancellation recovery, explicit CA trust, untrusted CA and hostname mismatch. IPC tests exercise Tauri's real manifest/capability resolution using its mock runtime, including an unauthorized window, remote origin, shell, fetch and secret-read attempts. They are not a substitute for an installed-package penetration test. + +## Native observations + +These installed-package observations apply to the 1,931,703-byte installer with SHA-256 `E50612678944E8BBA0AED5A712A516BDEDA659B619F55E8D43927C58E256F1A1`. The current package listed above was rebuilt successfully after the native-only write-protocol addition; its installed UI smoke has not been repeated. No renderer behavior, command registration, capability or installer configuration changed. + +1. Ran the unsigned NSIS installer with `/S` and a dedicated installation directory containing `安装验证`. It returned exit code 0. The installation contained the application and uninstaller, with no exporter/helper, Python or Server binary. +2. Launched the installed executable with a process-specific PATH containing only Windows System32. No Vite server or local PowerContext Server was started. The packaged homepage rendered; save/search were disabled and no sample business data was shown. +3. Windows UI Automation and window capture observed `http://tauri.localhost/`, version 0.1.0 and native-host status Ready. Switched to English and Dark using native UI and keyboard input; labels and semantic colors changed. +4. Exercised five zoom-in shortcuts from the default zoom. At approximately 200%, the navigation collapsed into a menu; it could be opened and used to reach Connections. Text flowed vertically without a fixed-height three-panel layout. Exact 800×600, screen-reader, high-contrast and complete keyboard/IME qualification remain outstanding. +5. Closed the window using Alt+F4 and verified the application process exited. Its observed process tree consisted of Desktop and WebView2 only; no Python or Server child appeared. +6. Ran the dedicated uninstaller with `/S`; exit code 0. The application executable was removed. A synthetic external data sentinel outside the installation directory remained byte-for-byte unchanged. This is not evidence for an actual Server service/database lifecycle. +7. The credential probe wrote a unique synthetic entry, launched a separate process that read it, removed it and verified it was absent. No secret was printed or returned to the renderer. The write-protocol test rejects missing/unknown storage choices and renderer-supplied IDs, checks safe invalid-secret errors, and verifies that the entire successful response contains only storage metadata. Unit tests separately showed that vault failure does not become session storage without an explicit session-only choice. + +Local machine-readable smoke observations are retained under ignored `desktop/.artifacts/`. Window observations are in the current Codex task's computer-use tool record. + +## Measurements and limits + +One process-launch sample reached a native window handle in **530 ms**; this is not time-to-interactive or a p50/p95 cold-start baseline. A later seven-process Desktop/WebView2 sample used 524,025,856 bytes summed working set and 241,205,248 bytes summed private memory. Its ten-second interval included UI automation and recorded 2.5 CPU seconds, so it **must not be reported as idle CPU**. Shared working sets may be double counted. + +No Server was included. No wakeup rate, search latency, data scale or statistically meaningful performance budget was measured. Full-product performance qualification remains open. + +## Outstanding gates and S2 handoff + +| Gate | Status / unblock condition | +| --- | --- | +| Clean Windows standard user; Python/Server absent | Blocked: disposable target environment needed. Restricted PATH is only a developer smoke test. | +| WebView2 absent, bootstrap/offline/error recovery | Blocked: disposable VM needed; existing developer runtime was not removed. | +| Signed package and release identity | Blocked: signing certificate and named Release responsibility not supplied. | +| Notification permission / installed cold-start activation | Not executed: isolated P0 experiment and responsible owner needed; no business notification feature added. | +| Independent service, login/reboot behavior | Not executed: isolated service environment and Server/Install owner needed. | +| Exact Agent host and real capture/recall | Not executed: qualified host/version and dedicated Server fixture needed. | +| Desktop / Server / Install / Release named ownership | Pending user/maintainer coordination; no commitments inferred. | +| Full performance and accessibility qualification | Not executed; developer observations above are partial. | +| Windows backend qualification (#1658) | Existing errors reproduced; follow the independent upstream fix. | + +S2 can consume the native vault, safe error types, transport budgets, generated API manifest and UI shell. It must wire the existing credential write-only request/receipt protocol into a context-checked IPC handler, native-owned connection IDs/generation, endpoint/trust-bound secret invalidation, compatibility evidence, identity/readiness and scope handling before enabling business actions. No Server baseline is automatically trusted. diff --git a/desktop/evidence/S2.md b/desktop/evidence/S2.md new file mode 100644 index 000000000..7fc092940 --- /dev/null +++ b/desktop/evidence/S2.md @@ -0,0 +1,58 @@ +# S2 connection evidence — 2026-09-19 + +S2 implementation is in progress. This report qualifies the listed native API adapter fixture only; it does not claim S2 exit qualification, a deployed remote Server, or a completed Desktop product. S1 package evidence remains historical and does not describe this working tree. + +## Tested Server fixture + +- Server source commit: `63f918b7ee8076965d488a3565020b70eee6a472`. +- Wheel: `powercontext-1.0.1.dev61+g63f918b7e.d20260919-py3-none-any.whl`. +- Wheel SHA-256: `de90495cf9cf66a00a0b063825b458557b63b805faf8ff64f33d2dcd8bb73ec9`. +- OpenAPI SHA-256 (LF): `de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c`. +- Client: modified Desktop working tree based on that commit, Windows developer machine. This is not evidence for the unchanged S1 commit or an installed S2 package. +- Explicit compatibility ID: `sqlite-63f918b7-v1`. Selecting it does not prove the remote binary identity. + +`tests/real_server.py` extracts the built wheel, starts disposable SQLite Server processes without model configuration, and runs the native `server_probe` example. The fixture creates its own Scope; the product does not create a Scope or start a Server. All three cases passed: explicit unauthenticated loopback HTTP, static Bearer loopback HTTP, and static Bearer HTTPS with explicit CA and reverse-proxy prefix. TLS validation remains enabled. These are local independent processes, not a remote deployment or an injected authorization-provider qualification. + +Each case exercises liveness, readiness, identity, capabilities, bounded Scope listing, default Scope suggestion, exact Scope lookup, note saving, FTS search and exact citation retrieval. Default Scope absence is accepted as a distinct 404. Bearer cases also reject a wrong token. Server liveness remains successful after the client exits. The machine-readable report is `desktop/.artifacts/real-server.json`; CI uploads its corresponding report when run. + +With access control disabled, the tested Server returns `runtime_not_ready` from `access/me`, without a Principal. The adapter preserves that fact. Explicit unauthenticated loopback access additionally requires readiness to report `access_mode=disabled`; it does not invent an identity or downgrade a Bearer failure. + +## Requirement coverage + +| Requirement | Current evidence and remaining work | +| --- | --- | +| S2-01 | Repository tests cover corruption without overwriting, duplicate names, revision conflicts, persistence and removal. UI tests cover canceling discard before deletion. Native rendered verification remains pending. | +| S2-02 | Native tests cover unavailable vault, explicit session storage and restart expiry, secret-free profile serialization, credential replacement and deferred cleanup. Existing S1 cross-process Windows vault evidence is separate. | +| S2-03 | API/transport/TLS tests cover loopback and encoded-prefix boundaries, bounded paging, safe errors, no redirect/proxy inheritance, hostname verification, response limits and timeouts. | +| S2-04 | Real fixture above exercises the selected adapter. Diagnostic facts remain separate. Capability denial does not itself deny Scope access. An injected-provider/enforced-access combination now has [real identity-change and revocation evidence](S4.md#real-injected-provider-identity-change); an external remote deployment and other provider combinations remain unverified. | +| S2-05 | Native session tests prove checking B preserves active A, activating B cancels A's slow Scope read, and authentication changes clear the active context. A further native test and UI test prove editing a query cancels its read and hides late results. Immutable write-context behavior is covered by the S3 native tests; full native interaction remains open. | +| S2-06 | Explicit limit 50 and opaque cursor encoding are tested. Exact IDs are encoded as one path segment. [Real 51-item same-title paging](S4.md#real-scope-pagination) now passes. Full cursor-expiry and installed switching acceptance remains pending. | +| S2-07 | Native CLI registration validates an explicit path, executable SHA-256 and exact version before fixed commands. Safe status projection and the Settings UI are implemented. Native process probes pass nonzero JSON output, output limits, timeout/cancellation, descendant cleanup and preservation of an unrelated process. Isolated real CLI service/integration checks pass, including valid exit code 1 with safe statuses. Missing/mismatched registration and trusted-window permissions pass. Installed native UI qualification remains open. | +| S2-08 | Connection and Scope UI are implemented with bilingual messages. The Settings diagnostic UI is implemented; complete native visual/accessibility verification remains pending. | + +## Verification + +- `pnpm test`: 9 UI tests passed. +- `pnpm lint` and `pnpm typecheck`: passed. +- `cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml -j 1`: 33 native tests passed. +- `uv run --no-sync python desktop/tests/real_server.py`: all 3 real Server cases passed after adding default Scope verification. +- Repository pre-commit checks pass except the 7 pre-existing Windows backend type errors tracked by #1658 (plus 3 deprecation warnings). Both Desktop Python harnesses pass their own Windows and Linux type checks; no baseline ignores or backend changes were introduced. +- CI configuration includes the real Server harness, wheel build and report upload; [the 0018e8a2 CI run](S3.md#remote-current-application-checkpoint) passed and provides immutable reports; later harness changes require their own run. + +## Remaining qualification + +This is an S2 implementation checkpoint, not a completed phase qualification. Installed native UI qualification, further race coverage, UI inspection and further acceptance coverage remain open. Usage instructions are in the English and Chinese READMEs. [Installed anonymous memory interactions](S3.md#completed-installed-window-workflow) pass; broader S3 interaction and S4 full qualification remain open. Signing, clean standard-user Windows/WebView2, interactive notifications/login, a named Agent host and maintenance responsibility retain their original acceptance requirements. + +## Real local CLI adapter + +`tests/real_cli.py` copies the existing Python launcher into a temporary fixture, extracts the exact Server wheel onto an explicit test-only `PYTHONPATH`, isolates home/configuration directories, and restricts PATH to Windows System32. It does not install or configure a real Agent host. The native adapter verifies the registered launcher digest and actual `--version` before executing either allowed diagnostic. Both commands returned valid JSON and exit code 1; service registration was absent and integration checks reported missing/unconfigured hosts. The CLI's WorkBuddy `presence=present` with failed hook/settings/MCP/skill checks is retained as reported, not reclassified as a working integration. + +The local launcher SHA-256 was `5bcfed8ab220479c35ac76fdefc3764c99a05cd58dc3d37b12ce513fd63780be`. The exact wheel version and digest are listed above. `desktop/.artifacts/real-cli.json` records the safe projection. This proves the adapter on the isolated fixture, not a user's configured Agent capture/recall path. CI builds the probe, runs this harness and uploads its own report. + +## CI version provenance + +The first S2 fork run (`35446440584`, commit `66c47abe`) passed frontend, Rust, process containment, vault and real Server checks, but rejected the CLI fixture version. The fork lacked the upstream package tag and produced `0.1.dev691+g66c47abe8` instead of the local `1.0.1.dev…` series. The workflow now fetches the specific upstream `powercontext-v1.0.0` tag and requires object `ec97efba5f26fc972e9d8bf5027d8233acab3ca1` before building. It does not relax the CLI version gate. The resulting CI run must be observed before claiming remote success for this checkpoint. + +## Scope recovery regression + +The UI test first reproduced a missing native-state refresh after failed Scope identity verification. The error path now propagates the disconnected native state, so the parent clears protected context. Additional UI cases verify cursor expiry removes the old page and restarts only on an explicit first-page request, and directory denial does not block an independently authorized exact Scope selection. All 20 UI tests pass. These simulated UI cases supplement, rather than replace, the installed-native and real-provider acceptance requirements. diff --git a/desktop/evidence/S3.md b/desktop/evidence/S3.md new file mode 100644 index 000000000..4568967e3 --- /dev/null +++ b/desktop/evidence/S3.md @@ -0,0 +1,60 @@ +# S3 memory workflow evidence — 2026-09-19 + +This is an implementation checkpoint, not completed S3 or Windows qualification. The native memory commands and React workflows are implemented. The installed anonymous-loopback save/search/read/copy workflow passed. Broader installed-window interaction, actual Chinese IME, screen-reader, 800×600 and exact 200% scaling acceptance remain open. + +## Requirement coverage + +| Requirement | Evidence | Remaining qualification | +| --- | --- | --- | +| S3-01 | Shared Home/Add-note form, raw UTF-8 budget, no truncation, nonblank validation, Enter/newline and synthetic composition tests. | Actual Windows Chinese IME and assistive technology. | +| S3-02 | Native tests cover fixed original connection/Scope, duplicate dispatch rejection, nullable entry and unknown results. No idempotency key is invented. | Installed anonymous save passed; failure-path presentation remains open. | +| S3-03 | Native adapter fixes FTS/limit 10; UI tests discard late results after query changes. No total count or history is inferred. | Installed anonymous search loop passed; full boundary presentation remains open. | +| S3-04 | Full citation is passed to exact reads; UI tests render hostile markup literally and verify explicit text/reference copies. Missing sources remain missing. | Installed body/reference clipboard passed; real revoked-reference/permission qualification remains open. | +| S3-05 | Native tests switch connections during dispatched writes, preserve original metadata, hide old returned bodies, mark aborted writes unknown and never replay. No body is stored in the operation record. | Broader platform/network interruption acceptance. | +| S3-06 | Chinese/English messages, plain-text wrapping, reader focus and return focus are implemented. | Small window, zoom, actual IME and screen-reader qualification. | + +## Real Server fixture + +The exact Server wheel and contract are listed in [S2](S2.md). The real SQLite harness now uses the product's `ConnectionManager`, explicitly qualifies a profile, selects the fixture Scope, saves a synthetic note containing Chinese, a decomposed accent, a newline and outer spaces, performs FTS search, and reads the exact returned citation. It verifies the Server's normalized response (`café`, trimmed outer whitespace) without changing the submitted input. All three cases passed: anonymous loopback HTTP, Bearer loopback HTTP and Bearer HTTPS with explicit CA/proxy prefix. These are independently running local Servers, not external deployments. + +The report at `desktop/.artifacts/real-server.json` records base commit `3c9597c4` with a dirty working tree; it is local implementation evidence, not an immutable released-build identity. CI runs the same expanded probe for each pushed head and uploads its own report. + +## Native window observation + +A development Tauri/WebView2 window completed explicit connection activation, exact Scope selection, real note saving, FTS search, exact reading and both clipboard actions on the existing Windows developer machine. Chinese text, a combining accent and newlines were exercised by accessibility input, not an actual IME. Normal window exit was followed by independent exact reading and writing against the same Server. The detailed workflow record below distinguishes this development result from installed-release qualification. No production Server or real personal memory was used. + +## Automated verification + +Frontend lint, TypeScript checks and all 20 UI tests passed. The preceding complete 40-test native suite passed; the expanded 7-test memory suite also passed, including memory IPC rejection for untrusted windows/origins and the existing transport timeout case. The expanded memory suite adds explicit 409/422 rejection and accepted-request/lost-response behavior. UI regressions first reproduced stale excerpts after detail failures, then verified removal for forbidden, not-found and network errors. Existing S2 tests and CI additionally exercise transport/TLS, credentials, context cancellation and diagnostic child-process cleanup. + +## Delivery status + +S1 package evidence is historical. This source checkpoint does not replace installer hashes or qualify a signed release. Clean standard-user Windows, absent WebView2, signing, notification cold activation, a named Agent capture/recall host, maintenance ownership and performance budgets remain explicit gates. See the READMEs for local startup and operational limitations. + +The expanded real Server probe also passes 0/1/10-result searches and repeated exact reads, and records native API p50/p95 observations in [S4](S4.md). Complete phase qualification remains open. + +## Completed development-window workflow + +The Chinese native development window completed explicit anonymous loopback activation, exact Scope selection, save, FTS search and exact reading against the isolated real Server. The input contained Chinese, a decomposed accent, a newline and literal script markup. The returned normalized body rendered as plaintext. Both copy actions were verified by pasting into the unsaved note field: body text retained its newline and citation JSON contained the complete memory reference, entry ID and entry-version ID. Pasted verification content was cleared without submitting it. + +Normal window close removed the running Desktop window. An independent HTTP client then retrieved that exact citation, verified the full text, and successfully wrote another synthetic note. [Machine-readable record](native-workflow.json) includes the native executable digest and fixture identity. This is development-window evidence, not installed-release, actual-IME, screen-reader or small-window qualification. + +## Completed remote checkpoint + +[Windows run 35447513910](https://github.com/knqiufan/powercontext/actions/runs/35447513910) completed successfully for `088ca7b9445e98f837561b8183703b863b8a98ed`. It passed all workflow steps, including the native/real Server/CLI suites and unsigned NSIS install/uninstall. The downloaded installer SHA-256 independently matches `58282974C5DCB674DE9A2ED47DAF498107B2798B4C7E0BA381C6FE83CC5E7011`; size is 2,581,044 bytes. Signature is `NotSigned`. Installation and uninstallation exited 0 and preserved the external synthetic sentinel. This hosted-runner result does not qualify standard-user, absent-WebView2 or UI behavior. + +Exact CI reports: [installer](ci-35447513910-windows-smoke.json), [real Server](ci-35447513910-real-server.json), [real CLI](ci-35447513910-real-cli.json). These qualify that checkpoint, not later source changes. + +## Completed installed-window workflow + +The release NSIS package was installed successfully into a dedicated Chinese-named directory on the existing Windows 11 administrator developer machine with WebView2 present. [Candidate identity and observations](installed-workflow.json) record package and installed-executable SHA-256 digests; both were independently rechecked against the files. Application sources match `dc7f2384` and are unchanged through `0018e8a2`. This is a local release build, distinct from the CI installer above. + +With the Vite development server stopped, the installed window served its packaged `http://tauri.localhost/` resources. Startup retained the saved profile but required explicit connection activation and Scope selection. The anonymous isolated real Server accepted `desktopinstalled20260919 安装包验证` followed by a newline and `纯文本与换行 café`. FTS found the entry, exact reading returned its body, and both body and full citation were copied and verified by pasting into an unsaved form. Pasted content was cleared without submission. + +After normal window close, an independent HTTP client retrieved the exact citation and matching body and successfully wrote another synthetic note. This proves the observed normal-exit case only. The subsequent abnormal-exit trial was not executed because the user stopped computer operation. + +Five native Ctrl+plus increments enlarged the reader; its controls remained accessible by scrolling. [Observed reader](installed-reader-zoom.jpg) contains synthetic data only. The precise zoom factor was not queried, and the attempted window resize did not establish 800×600. Neither required accessibility scenario is marked passed from this observation. + +## Remote current application checkpoint + +[Windows run 35448482560](https://github.com/knqiufan/powercontext/actions/runs/35448482560) passed every step for `0018e8a2edd9d0033d021ab5b9dac2f60246c84a`. The downloaded installer digest was independently verified as `2586DAE019241FFAC9680EDC6DD1AA35B9CA61C0CC315C5632B901A7739B276F`, 2,580,814 bytes, NotSigned. [Installation report](ci-35448482560-windows-smoke.json) records successful install/uninstall, installed executable identity and retained external sentinel. [Real Server report](ci-35448482560-real-server.json) records the three transport modes and repeated API timings. Later provider/revocation harness changes require their own CI run. Hosted Windows Server 2025 does not qualify the remaining Windows desktop environment/UI gates. diff --git a/desktop/evidence/S4.md b/desktop/evidence/S4.md new file mode 100644 index 000000000..fb3140723 --- /dev/null +++ b/desktop/evidence/S4.md @@ -0,0 +1,131 @@ +# S4 qualification matrix + +This matrix tracks the complete T-01–T-29 acceptance scope. **Overall qualification is incomplete.** “Partial” means only the specifically listed evidence exists; it never means that the full scenario passed. Local automated checks, real Server fixtures, installed-package checks and human/native interaction evidence are distinct. + +## Candidate and evidence identity + +The S3 implementation checkpoint is `088ca7b9445e98f837561b8183703b863b8a98ed`, version 0.1.0. Its Windows CI is [35447513910](https://github.com/knqiufan/powercontext/actions/runs/35447513910); the run completed successfully and its exact reports and verified installer digest are linked in [S3](S3.md). The previous S2 run 35446851988 was cancelled by the newer push during installer compilation and is not a completed passing run. + +The tested local Server wheel/contract and Windows developer environment are recorded in [S2](S2.md) and [S1](S1.md). Local observations use an existing administrator development machine with WebView2 installed. They do not prove standard-user, absent-runtime or no-Python installation. Execution is by Codex; no human release reviewer or organizational owner has been assigned. + +## S4 work-item audit + +| Work item | Result and matching evidence | +| --- | --- | +| S4-01 Candidate identity | Local installed and CI application checkpoints have distinct commit/package/executable/wheel/contract identities in [S3](S3.md). New test-only commits do not alter application sources; they still require their own CI evidence. | +| S4-02 Isolated installation | Incomplete. Hosted install/uninstall and local Chinese-path installed UI passed; neither proves standard-user Windows 11 with WebView2 absent and no Python/Server. | +| S4-03 T-01–T-29 | Incomplete. The table below retains each scenario and its remaining scope. | +| S4-04 Official gates | Incomplete. Publisher signing, generic notification cold activation, actual named Agent capture/recall, ownership and complete performance/accessibility evidence remain open. | +| S4-05 Regression and scope | Desktop checks and real fixtures passed at the recorded checkpoints; API/JS generation checks and 48 contract tests passed locally. The seven recorded Windows baseline type errors remain unresolved; current CI is evaluated per commit. No backend contract, service-management or Agent-configuration changes are included. | +| S4-06 Usage documentation | [English](../README.md) and [Chinese](../README.zh.md) cover development/installed startup, exact connection/Scope selection, compatibility, ownership, unknown-write handling and uninstall boundaries. | +| S4-07 PR and delivery | [Draft PR #1663](https://github.com/oceanbase/powercontext/pull/1663) includes scope, validation, limitations and AI usage. No final qualification or issue closure is claimed. | + +## Itemized results + +| ID | Status | Evidence now | Evidence still required | +| --- | --- | --- | --- | +| T-01 | Blocked for full scenario | S1 restricted-PATH UI launch; native real Server adapter | Clean no-Python/no-Server machine and external Server workflow | +| T-02 | Partial / release gates blocked | S1 unsigned NSIS/Chinese-path results; CI installer/uninstaller script preserves synthetic external file | Exact current package, standard user, WebView2 absent/present, authentic publisher signature; sentinel is not a real Server database | +| T-03 | Partial | Native `session` and `profiles` tests; UI `connections` draft-cancel/removal tests | Installed anonymous A/B activation and inactive removal pass in run 35455969549; edit/check and authenticated variants remain | +| T-04 | Partial | Native `transport`, `tls`, `api` tests and three real Server modes | Complete installed-window recovery presentation | +| T-05 | Partial | Windows vault cross-process probe; profile secret-free persistence/owned removal tests | Installed form, app restart and authenticated reuse together | +| T-06 | Partial | Native unavailable-vault, explicit session and restart-expiry tests | Installed-window cancel/session-only flow | +| T-07 | Partial | Native retargeting invalidates credentials; UI retargeting invalidates verification | Observe actual new endpoint receives no old credential in full workflow | +| T-08 | Partial | Independent facts, static Bearer and explicit anonymous fixtures; capability denial test | Injected-provider success and changed-identity rejection passed in the real fixture; degraded-readiness combinations and rendered statuses remain | +| T-09 | Partial | Explicit compatibility profile and operation allowlist; no inferred version on 404 | All supported/unknown handshake combinations and complete UI matrix | +| T-10 | Partial | Safe API errors, no anonymous fallback, identity invalidation | Full recovery/authorization combinations in native window | +| T-11 | Partial | Explicit paging limit, opaque cursor, exact lookup, default-scope real fixture, query cancellation; UI expiry and directory-denial recovery | Real 51-item pagination passed; real directory-denial/cursor410 combinations remain; corresponding UI recovery tests pass | +| T-12 | Partial | Native exact Scope and original write-context tests; real manager fixture | 51 same-title Scopes and exact-ID lookup pass in real native fixture; installed exact selection now passes in the remote workflow below; complete same-title installed selection and host-binding observation remain | +| T-13 | Partial | Real no-model SQLite save; Chinese/NFC/newline fixture; UI byte/IME simulation; native nullable entry | Installed anonymous save passed; actual IME and full byte-boundary combinations remain | +| T-14 | Partial | Real FTS/limit10; expanded probe tests 0/1/10 and repeated queries | Installed anonymous result presentation passed; full boundary evidence remains | +| T-15 | Partial | Real exact-citation read; UI explicit full-text/citation copy tests | Installed and development WebView body/reference copy verified by paste and independent exact read; broader authorization cases remain | +| T-16 | Partial | UI regression reproduces and fixes stale excerpts after forbidden/not-found/network detail failures | Real same-identity Scope binding revocation and historical citation now pass in the native adapter; installed failure presentation remains | +| T-17 | Partial | Native duplicate protection, invalid response, dropped future, 409/422 rejection and accepted request/lost response tests | Four real fixture modes and installed anonymous committed-response-loss recovery pass; other rejection/recovery combinations remain | +| T-18 | Partial | Native slow read cancellation, identity change and immutable dispatched write; UI late-query suppression | Complete token/Principal/Scope switching matrix in installed product | +| T-19 | Partial | Generated ten-operation allowlist; explicit nonempty FTS, limit10 notice | Native navigation/request trace across all entry points | +| T-20 | Partial | No persistent body/queue; context invalidation; detail failures clear excerpts/body | Installed disconnect/reconnect clears the observed reader, hits, query and empty draft; a nonempty unsaved draft and app-restart workflow remain | +| T-21 | Partial | Safe diagnostic projection, real isolated CLI, timeout/cancel/tree ownership and invalid registration probes | Installed UI and real configured external service/host | +| T-22 | Partial | Independent Server healthy after manager/probe exit; HTTP200 after development process termination | Hosted installed forced exit now passes readiness/original-read/new-write verification in run 35456351561; local installed/development normal exit passed; standard-user Windows 11 and configured external deployment remain | +| T-23 | Partial | Bilingual/theme UI tests; plaintext wrapping/focus handling; native Chinese form observation | 800×600, 200%, high contrast, screen reader, actual IME and keyboard core workflow | +| T-24 | Partial | Secret-free serialization, bounded safe errors/diagnostics; in-memory-only body implementation | Exact synthetic-marker scan of both app data directories found no matches; transformed storage, complete logs and final package audit remain | +| T-25 | Partial | Hostile text renders literally; IPC window/origin denial; endpoint/path validation; stale generation tests | Complete installed IPC/navigation attack matrix | +| T-26 | Blocked | S1 records the unmet gate | Installed native generic notification and cold activation trial; no business notification claim | +| T-27 | Blocked | Isolated CLI diagnostics are not Agent loading evidence | Named/versioned host with actual capture/recall, independent service/login evidence, accepted maintenance/release owners | +| T-28 | Partial / baseline check failed | Exact source and wheel identities; native API timing probe; current Windows check reproduces seven #1658 type errors | Local current installer hashes/size/unsigned fact recorded in S3; cold launch, process-tree CPU/memory, package size and complete timing report; fix/resolve Windows baseline; approved numerical budget remains unset | +| T-29 | Partial | Independent locks/build, generated OpenAPI/IPC drift checks, lint/typecheck/Rust suites; no backend API modifications | 48 API/JS contract tests and generation checks passed locally; latest harness CI and final repository regression audit remain | + +## Official gates and limitations + +Signing proves a publisher and package integrity; an unsigned internal artifact does not meet the signed-reference-package gate. Standard-user and absent-WebView2 qualification require an appropriate isolated Windows environment. Notification cold activation remains a native feasibility test, not a reason to introduce Review polling or business notification features. + +Host and ownership gates require actual capture/recall on a named host version and real human responsibility for Desktop, Server, installation and release. The test executor and issue author are not automatically these owners. No missing gate is relabeled as out of scope. + +Performance measurements report environment, dataset, repetitions and p50/p95. Native API round trips include identity verification but exclude rendering, clipboard and application startup. They do not establish a numerical product budget or whole-component resource use. + +The PR stays a draft and does not close #1654 or #1428. See [English startup instructions](../README.md) and [中文启动与使用说明](../README.zh.md). Full qualification requires filling the remaining cells with matching evidence, not only obtaining green unit tests. + +## Local native API timing observation + +[Raw samples and environment](api-performance.json) record the development working tree based on `088ca7b9` (dirty), not an installed release build. Each isolated Scope contains 13 synthetic notes. Saves have 11 samples while populating; search/exact read each have 20 samples after population. Percentiles use nearest rank. The native manager rechecks identity for each operation. The same run verified empty, unique and capped-ten searches in all three modes. + +| Local mode | Save p50 / p95 ms | Search p50 / p95 ms | Exact read p50 / p95 ms | +| --- | --- | --- | --- | +| Anonymous HTTP | 96.11 / 183.36 | 62.47 / 67.12 | 30.24 / 33.59 | +| Bearer HTTP | 123.55 / 133.28 | 122.34 / 132.76 | 57.07 / 59.48 | +| Bearer HTTPS with CA/prefix | 125.21 / 128.05 | 121.51 / 134.23 | 58.40 / 67.54 | + +These are small local fixtures on an active developer machine, without a cold-cache guarantee. They do not predict remote-network or large-dataset performance. Startup and complete process-tree CPU/memory remain unmeasured here. The local installed package is 2,580,161 bytes; its exact digest and installed workflow are recorded in [S3](S3.md) and [the machine-readable record](installed-workflow.json). + +## Real injected-provider identity change + +[Four-mode fixture report](provider-workflow.json) records a local run based on `68436a46` with test changes present. The fourth case injects a credential-validating Authentication Provider and real built-in enforced authorization into the isolated wheel Server. The native manager completes save/search/exact reads. A test-only file then changes the subject returned for the same credential; the next exact read returns `StaleContext`, advances the generation and clears the active connection. A direct native HTTP exact read under the changed identity returns `Forbidden`. The Server remains healthy after client exit. This verifies changed identity and enforced denial, not all degraded-readiness combinations or installed UI presentation. The existing CI harness runs this fourth case automatically on subsequent source pushes. + +The same provider fixture grants a separate reader `scope.viewer`, verifies an older exact citation after additional writes, revokes that binding through the real policy API, and confirms the unchanged Principal receives `Forbidden` for the same citation. This exercises real policy persistence and native HTTP decoding; UI excerpt clearing remains covered separately by frontend regression tests. + +## Real committed write with lost response + +[Four-mode report](response-loss-workflow.json) records the test working tree based on `a076db49`. In each isolated Server mode, the test proxy waits until the real remember operation completes, then sends only a partial success body and closes the response. The native manager reports `Unknown` with no success body. A subsequent FTS search finds exactly one matching entry and an exact read verifies its full text; the proxy records exactly one remember request while this fault is enabled. There is no automatic write replay. This exercises a committed transaction and transport failure together, beyond a mock socket fixture. The timing sample dataset still contains 13 notes; the fault case adds a fourteenth note after those measurements. Installed anonymous presentation and search recovery are verified in run 35455969549 below; other recovery combinations remain open. + +## Bounded local storage observation + +After the development and installed windows had closed normally, an exact binary/text scan of both application data directories found neither synthetic note marker. [Scan scope and limitations](local-marker-audit.json) record the 336 enumerated files and `rg` no-match exit codes. This is evidence only for those two byte markers in those directories; it does not prove the absence of all transformed content, operating-system clipboard history or crash logs. + +## Real Scope pagination + +[Fixture report](scope-pagination-workflow.json) records 51 distinct Scope IDs with the same synthetic title. The native ConnectionManager retrieves 50 results then one result using the returned opaque cursor. Their combined IDs exactly match the created set, with no duplicates or omissions and no next cursor after the second page. Exact native lookups distinguish the first and last IDs despite equal titles, and merely browsing does not change the active memory Scope. Installed selection, cursor expiry and directory denial remain distinct scenarios. + +## Remote installed WebView workflow + +`tests/installed_ui.py` launches the exact installed executable with a loopback debugging endpoint enabled by a temporary machine policy restricted to its executable on the disposable runner; the previous value is restored afterward, then attaches a matching Microsoft-signed Edge WebDriver. Execution is restricted to disposable GitHub Windows runners; the product configuration is unchanged. The workflow checks packaged-resource loading and uses real element clicks and keystrokes to activate an anonymous loopback connection, choose an exact Scope, save a multiline Chinese note, search and read its exact version. It independently verifies the returned citation/body with the real SQLite Server and pastes copied body/reference back into an unsaved form for comparison, then clears that form. No mocked IPC or injected application state supplies these results. + +Local static checks pass, and the isolated Server fixture has passed real save/exact-read verification and cleanup. Remote run [35451893591](https://github.com/knqiufan/powercontext/actions/runs/35451893591), commit `3aea0371`, failed to establish the initial driver-launched session within 60 seconds. [UI report](ci-35451893591-installed-ui.json) records failure with no browser session or workflow evidence. [Package report](ci-35451893591-windows-smoke.json) verifies successful installation/uninstallation and sentinel preservation; the downloaded installer digest matches. All earlier build, native and real Server/CLI steps passed. That failed run supplies no UI pass; passing later runs are identified below. `installed-ui.json`, `installed-ui.png`, driver logs and `windows-smoke.json` identify the actual run/package. The Server is independent of the app and is stopped by the harness. Driver cleanup and forced cleanup of a remaining task-owned app process are not evidence of normal user exit. This gate does not establish no-Python installation, a clean Windows 11 standard-user environment, an external deployment or accessibility/Agent-host qualification. + + +Run [35453323102](https://github.com/knqiufan/powercontext/actions/runs/35453323102), commit `a12d36db`, passed all steps before installed UI acceptance. The [UI report](ci-35453323102-installed-ui.json) identifies `application_start`: the app process remained alive but its loopback debugging endpoint did not become ready before timeout. No WebDriver session or business interaction is claimed. The [package report](ci-35453323102-windows-smoke.json) records successful install/uninstall. Linux quality now passes after guarding the Windows-only process flag. + +The CI dispatch input `installer_run` reuses one installer from this repository's Desktop preview workflow, after matching its recorded commit, SHA-256 and byte length. These diagnostic runs record both the current harness commit and the source installer commit; they do not replace full-build checks. Application-start failures capture only the task-owned application's process tree and, when available, its window. Diagnostics remain limited to disposable runners. + + +Diagnostic run [35454684629](https://github.com/knqiufan/powercontext/actions/runs/35454684629) reused the exact `a12d36db` executable (`f8565ad6bca0e37a8edc9f956be4d0259f44f2a55b0dd662d8b8c597c6d15e70`). The [native window capture](ci-installed-home.png) shows the actual packaged home page. Its process snapshot shows a responding application and WebView2 renderer in runner session 2, with the requested debugging switches absent from the browser command line. Thus the endpoint timeout does not establish that the application failed to render. No business workflow pass is implied by this screenshot. + +[Microsoft's elevated-host guidance](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/security#for-an-elevated-host-app-use-appropriate-override-flags) explains that elevated hosts ignore WebView2 environment overrides while honoring machine policy. The harness now temporarily sets only the `powercontext-desktop.exe` value under the machine AdditionalBrowserArguments policy, enables loopback debugging, and restores that exact previous value after the owned processes exit. It never changes the wildcard policy or product configuration. This remains a CI-only diagnostic configuration, not standard-user qualification; the passing remote evidence is identified below. + + +## Verified remote installed workflow + +[Diagnostic run 35455040338](https://github.com/knqiufan/powercontext/actions/runs/35455040338) passed every applicable step. [UI report](ci-35455040338-installed-ui.json) identifies harness commit `e98ff6b9` and the reused installer commit `a12d36db`; [installation report](ci-35455040338-windows-smoke.json) records its SHA-256 `B48557AB2C65A85DA14DD138DC2D749E397BCDECF96FAE2262B5886E2E4685A6` and 2,579,502-byte size, independently checked against the downloaded original. The installed executable digest matches both reports. The package remains unsigned. + +The actual installed WebView2 152.0.4191.66 loaded `http://tauri.localhost/` and completed explicit connection/compatibility activation, exact Scope selection, Chinese multiline plaintext note save, FTS search and exact-version reading. A separate HTTP read verified its full citation and text. Both body and citation clipboard actions were verified by pasting into the unsaved note field, comparing the result, and clearing it. The [final screenshot](ci-installed-memory.png) shows the saved result and literal script-like text. Installation and uninstallation returned 0, and the external synthetic sentinel survived. The temporary executable-specific debug policy was restored before the script reported success. + +This proves the listed anonymous loopback workflow on a hosted Windows Server runner. It does not prove standard-user Windows 11, no-Python/no-Server installation, real IME or screen-reader behavior, authenticated restart/isolation, normal/abnormal user exit, a real Server database surviving uninstall, or Agent-host capture/recall. Full build run 35455039808 for the harness commit is evaluated independently; the reused-package run does not imply that later builds passed. + + +## Verified complete build and extended installed scenarios + +[Full run 35455039808](https://github.com/knqiufan/powercontext/actions/runs/35455039808) passed all applicable steps for `e98ff6b9`. Its [UI report](ci-35455039808-installed-ui.json) verifies the core installed workflow; its [package report](ci-35455039808-windows-smoke.json) records the unsigned installer at 2,580,890 bytes with SHA-256 `F85CDADF914542EC4AE4135FD06238A09F9CA90F856D7CA019B426C0751E5E1C`, independently checked against the downloaded package. Install and uninstall returned zero; the external synthetic sentinel survived. The manual-only installed-package job is intentionally skipped in a full build. + +[Run 35455969549](https://github.com/knqiufan/powercontext/actions/runs/35455969549) passed using harness `94c45bb6` and that exact `e98ff6b9` installer. The [UI report](ci-35455969549-installed-ui.json) verifies all ten workflow assertions; the [package report](ci-35455969549-windows-smoke.json) matches the original package and executable digests. Two independent Servers return their own exact citations after activation. Disconnect/reconnect clears the observed old reader, hits, query and empty draft. Removing the inactive profile preserves both Servers' independently readable data. A multiline keystroke does not submit before the save button. After one committed write loses its response, the installed UI shows an unknown outcome, retains the draft, recovers the exact entry through search, and the fixture counter remains one. These anonymous scenarios do not establish authenticated isolation, a nonempty draft across disconnect or restart behavior. + +A further CI-only lifecycle scenario saves and reads through the installed app, forcibly terminates only that harness-owned application process, then independently checks Server readiness, the original exact citation, and a new write/exact read. [Run 35456351561](https://github.com/knqiufan/powercontext/actions/runs/35456351561) passed with harness `fd10d9d6` and the exact `e98ff6b9` installer. Its [UI/lifecycle report](ci-35456351561-installed-ui.json) records application exit code 1, readiness, original exact read, and independent new write/read all passing. It also confirms the explicit new-write confirmation after the previous unknown outcome. The [package report](ci-35456351561-windows-smoke.json) matches the verified package identity and successful uninstall. This is deliberate process termination on the hosted runner; it does not establish standard-user Windows 11, every crash mode or preservation of an external production database. + +Pending installed boundary coverage adds exact 8192-byte Unicode save/read-back, over-budget disabled submission, zero/capped-ten search presentation, and cancellation/confirmation of disconnect with a nonempty unsaved draft. These cases are not credited before a matching remote result. diff --git a/desktop/evidence/api-performance.json b/desktop/evidence/api-performance.json new file mode 100644 index 000000000..b4083acde --- /dev/null +++ b/desktop/evidence/api-performance.json @@ -0,0 +1,264 @@ +{ + "serverWheel": "powercontext-1.0.1.dev61+g63f918b7e.d20260919-py3-none-any.whl", + "serverWheelSha256": "de90495cf9cf66a00a0b063825b458557b63b805faf8ff64f33d2dcd8bb73ec9", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "desktopCommit": "088ca7b9445e98f837561b8183703b863b8a98ed", + "desktopWorkingTreeDirty": true, + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": [ + { + "mode": "loopback-anonymous", + "result": "passed", + "serverAliveAfterClientExit": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 30.236099999999997, + "p95Ms": 33.589800000000004, + "samplesMs": [ + 27.248, + 27.962500000000002, + 28.4415, + 28.512800000000002, + 28.715, + 28.8007, + 28.8074, + 29.6306, + 29.7654, + 30.236099999999997, + 30.568, + 30.895, + 30.9106, + 31.0103, + 31.1758, + 32.2218, + 32.597899999999996, + 33.1938, + 33.589800000000004, + 34.2943 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 96.1135, + "p95Ms": 183.3563, + "samplesMs": [ + 77.6782, + 78.07690000000001, + 87.53, + 88.9176, + 94.9752, + 96.1135, + 96.9184, + 106.301, + 106.7726, + 107.64219999999999, + 183.3563 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 62.465199999999996, + "p95Ms": 67.1151, + "samplesMs": [ + 56.5614, + 59.257600000000004, + 60.9638, + 61.3091, + 61.7295, + 61.762, + 61.9525, + 62.2624, + 62.4618, + 62.465199999999996, + 63.24720000000001, + 63.2901, + 63.295100000000005, + 64.0587, + 64.8142, + 64.93599999999999, + 65.6898, + 65.8195, + 67.1151, + 69.47449999999999 + ] + } + } + }, + { + "mode": "loopback-bearer", + "result": "passed", + "serverAliveAfterClientExit": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 57.0691, + "p95Ms": 59.4827, + "samplesMs": [ + 54.348, + 55.3064, + 55.3813, + 55.5148, + 55.944, + 56.0004, + 56.555, + 56.745200000000004, + 56.9204, + 57.0691, + 57.3684, + 57.780499999999996, + 58.1468, + 58.4104, + 58.4197, + 58.493, + 58.7927, + 59.1378, + 59.4827, + 60.0786 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 123.55040000000001, + "p95Ms": 133.2773, + "samplesMs": [ + 116.59469999999999, + 118.82239999999999, + 119.1632, + 120.1482, + 122.85889999999999, + 123.55040000000001, + 125.91820000000001, + 125.97569999999999, + 127.08900000000001, + 129.30829999999997, + 133.2773 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 122.3404, + "p95Ms": 132.7628, + "samplesMs": [ + 115.9966, + 116.8864, + 118.3108, + 121.0864, + 121.2452, + 121.32600000000001, + 121.60759999999999, + 121.70649999999999, + 121.9281, + 122.3404, + 122.42269999999999, + 122.5752, + 122.6548, + 122.70150000000001, + 123.69149999999999, + 125.9198, + 129.77730000000003, + 131.55339999999998, + 132.7628, + 135.1571 + ] + } + } + }, + { + "mode": "https-bearer-base-path", + "result": "passed", + "serverAliveAfterClientExit": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 58.4003, + "p95Ms": 67.5426, + "samplesMs": [ + 54.2547, + 55.1286, + 55.741600000000005, + 56.315400000000004, + 56.960499999999996, + 57.0707, + 57.1038, + 57.9316, + 58.2725, + 58.4003, + 58.563900000000004, + 59.0556, + 59.3583, + 59.6827, + 59.7365, + 59.79750000000001, + 62.57840000000001, + 63.9697, + 67.5426, + 68.6664 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 125.21370000000002, + "p95Ms": 128.05069999999998, + "samplesMs": [ + 117.8108, + 120.2207, + 121.756, + 124.2043, + 124.93849999999999, + 125.21370000000002, + 125.38659999999999, + 125.61810000000001, + 125.92970000000001, + 126.9378, + 128.05069999999998 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 121.5113, + "p95Ms": 134.2323, + "samplesMs": [ + 113.67399999999999, + 116.7222, + 117.4458, + 119.73540000000001, + 119.73870000000001, + 120.6332, + 121.11489999999999, + 121.22189999999999, + 121.28490000000001, + 121.5113, + 121.56060000000001, + 122.0095, + 122.2243, + 123.16829999999999, + 123.4621, + 126.6931, + 131.43089999999998, + 131.71089999999998, + 134.2323, + 139.0478 + ] + } + } + } + ], + "measurementDate": "2026-09-19", + "environment": { + "os": "Windows 11 Pro 10.0.26200 x64", + "account": "existing administrator development account", + "cpu": "AMD Ryzen 9 8945HX, 16 cores / 32 logical processors", + "physicalMemoryBytes": 33605316608, + "clientBuildProfile": "debug", + "data": "13 synthetic notes per isolated SQLite Scope; saves measured while populating, reads after population", + "percentileMethod": "nearest rank", + "budget": "not defined; no pass/fail budget claim" + } +} diff --git a/desktop/evidence/ci-35447513910-real-cli.json b/desktop/evidence/ci-35447513910-real-cli.json new file mode 100644 index 000000000..02b8414ec --- /dev/null +++ b/desktop/evidence/ci-35447513910-real-cli.json @@ -0,0 +1,175 @@ +{ + "version": "1.0.1.dev64+g088ca7b94", + "wheelSha256": "ae9d529231cfebbf61b3defbc76342836dc0208ea45eed7bbfdc44ae3c0e3570", + "launcherSha256": "a06b463425fa148b0086b953d3fe3676e66be9f1ebeab0bc8760b42bd208d372", + "environment": "Isolated home and system-only PATH; wheel extracted on explicit test PYTHONPATH", + "result": { + "integrations": { + "checkedAt": 1789827033, + "exitCode": 1, + "hosts": [ + { + "checks": [ + { + "field": "claude_code", + "status": "failed" + }, + { + "field": "plugin", + "status": "skipped" + } + ], + "host": "claude-code", + "presence": "missing" + }, + { + "checks": [ + { + "field": "codex", + "status": "failed" + }, + { + "field": "plugin", + "status": "skipped" + } + ], + "host": "codex", + "presence": "missing" + }, + { + "checks": [ + { + "field": "dsh", + "status": "failed" + }, + { + "field": "plugin", + "status": "skipped" + } + ], + "host": "dsh", + "presence": "missing" + }, + { + "checks": [ + { + "field": "hermes", + "status": "failed" + }, + { + "field": "plugin", + "status": "skipped" + } + ], + "host": "hermes", + "presence": "missing" + }, + { + "checks": [ + { + "field": "openclaw", + "status": "failed" + }, + { + "field": "plugin", + "status": "skipped" + } + ], + "host": "openclaw", + "presence": "missing" + }, + { + "checks": [ + { + "field": "opencode", + "status": "failed" + }, + { + "field": "plugin", + "status": "skipped" + }, + { + "field": "skill", + "status": "skipped" + } + ], + "host": "opencode", + "presence": "missing" + }, + { + "checks": [ + { + "field": "package", + "status": "skipped" + }, + { + "field": "pi", + "status": "failed" + } + ], + "host": "pi", + "presence": "missing" + }, + { + "checks": [ + { + "field": "hooks", + "status": "failed" + }, + { + "field": "mcp", + "status": "failed" + }, + { + "field": "settings", + "status": "failed" + }, + { + "field": "skill", + "status": "failed" + } + ], + "host": "workbuddy", + "presence": "present" + } + ], + "items": [ + { + "field": "integrations", + "status": "failed" + } + ] + }, + "service": { + "checkedAt": 1789827026, + "exitCode": 1, + "hosts": [], + "items": [ + { + "field": "support", + "status": "supported" + }, + { + "field": "registration", + "status": "not_installed" + }, + { + "field": "definition", + "status": "unknown" + }, + { + "field": "manager_ownership", + "status": "unknown" + }, + { + "field": "manager", + "status": "unknown" + }, + { + "field": "server_liveness", + "status": "unknown" + } + ] + } + } +} diff --git a/desktop/evidence/ci-35447513910-real-server.json b/desktop/evidence/ci-35447513910-real-server.json new file mode 100644 index 000000000..345c207c2 --- /dev/null +++ b/desktop/evidence/ci-35447513910-real-server.json @@ -0,0 +1,25 @@ +{ + "serverWheel": "powercontext-1.0.1.dev64+g088ca7b94-py3-none-any.whl", + "serverWheelSha256": "ae9d529231cfebbf61b3defbc76342836dc0208ea45eed7bbfdc44ae3c0e3570", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "desktopCommit": "088ca7b9445e98f837561b8183703b863b8a98ed", + "desktopWorkingTreeDirty": false, + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": [ + { + "mode": "loopback-anonymous", + "result": "passed", + "serverAliveAfterClientExit": true + }, + { + "mode": "loopback-bearer", + "result": "passed", + "serverAliveAfterClientExit": true + }, + { + "mode": "https-bearer-base-path", + "result": "passed", + "serverAliveAfterClientExit": true + } + ] +} diff --git a/desktop/evidence/ci-35447513910-windows-smoke.json b/desktop/evidence/ci-35447513910-windows-smoke.json new file mode 100644 index 000000000..171aa15dd --- /dev/null +++ b/desktop/evidence/ci-35447513910-windows-smoke.json @@ -0,0 +1,10 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "088ca7b9445e98f837561b8183703b863b8a98ed", + "installerSha256": "58282974C5DCB674DE9A2ED47DAF498107B2798B4C7E0BA381C6FE83CC5E7011", + "signature": "NotSigned", + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35448482560-real-server.json b/desktop/evidence/ci-35448482560-real-server.json new file mode 100644 index 000000000..3523c1feb --- /dev/null +++ b/desktop/evidence/ci-35448482560-real-server.json @@ -0,0 +1,261 @@ +{ + "environment": { + "os": "Windows-2025Server-10.0.26100-SP0", + "architecture": "AMD64", + "python": "3.12.10", + "clientBuildProfile": "debug", + "percentileMethod": "nearest rank", + "budget": "No approved numerical budget; observation only" + }, + "serverWheel": "powercontext-1.0.1.dev68+g0018e8a2e-py3-none-any.whl", + "serverWheelSha256": "ae76f112f925a09fdd4cc471594c3ad5ba48da3f73811fd7ef6a0a255c711353", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "desktopCommit": "0018e8a2edd9d0033d021ab5b9dac2f60246c84a", + "desktopWorkingTreeDirty": false, + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": [ + { + "mode": "loopback-anonymous", + "result": "passed", + "serverAliveAfterClientExit": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 22.2554, + "p95Ms": 24.9969, + "samplesMs": [ + 21.416600000000003, + 21.5547, + 21.835, + 21.9454, + 21.9511, + 21.9704, + 21.979100000000003, + 22.0524, + 22.1086, + 22.2554, + 22.403200000000002, + 22.4067, + 22.4163, + 22.4347, + 22.5585, + 22.7778, + 23.0865, + 23.8192, + 24.9969, + 29.6953 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 63.5241, + "p95Ms": 74.3859, + "samplesMs": [ + 59.916900000000005, + 60.305, + 61.1552, + 61.918, + 62.417300000000004, + 63.5241, + 65.11590000000001, + 65.84819999999999, + 66.3672, + 67.2203, + 74.3859 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 43.3516, + "p95Ms": 45.945600000000006, + "samplesMs": [ + 42.2258, + 42.3404, + 42.6442, + 42.6655, + 43.072399999999995, + 43.107, + 43.1297, + 43.2164, + 43.3125, + 43.3516, + 43.4175, + 43.51349999999999, + 43.5276, + 43.5432, + 43.5523, + 43.8048, + 44.1816, + 44.3068, + 45.945600000000006, + 54.159800000000004 + ] + } + } + }, + { + "mode": "loopback-bearer", + "result": "passed", + "serverAliveAfterClientExit": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 56.4552, + "p95Ms": 128.4581, + "samplesMs": [ + 44.5997, + 44.9974, + 45.360200000000006, + 54.6304, + 54.9392, + 55.473, + 55.9286, + 56.1234, + 56.2316, + 56.4552, + 57.1444, + 57.5794, + 60.9724, + 61.3493, + 68.3561, + 83.55659999999999, + 93.5037, + 104.3713, + 128.4581, + 167.0991 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 227.50029999999998, + "p95Ms": 513.8306, + "samplesMs": [ + 120.4114, + 126.3408, + 139.7801, + 189.32080000000002, + 206.0003, + 227.50029999999998, + 243.8714, + 264.3188, + 309.9078, + 326.7732, + 513.8306 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 115.7401, + "p95Ms": 181.2545, + "samplesMs": [ + 88.3609, + 88.9984, + 90.59259999999999, + 96.3857, + 106.55420000000001, + 106.667, + 110.68459999999999, + 113.8626, + 114.3614, + 115.7401, + 115.7492, + 116.0702, + 131.1135, + 142.519, + 153.74360000000001, + 171.6357, + 176.3393, + 176.5715, + 181.2545, + 187.7293 + ] + } + } + }, + { + "mode": "https-bearer-base-path", + "result": "passed", + "serverAliveAfterClientExit": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 43.8418, + "p95Ms": 49.698699999999995, + "samplesMs": [ + 41.2162, + 41.651, + 41.7473, + 41.8746, + 42.0499, + 42.141999999999996, + 43.356199999999994, + 43.4906, + 43.6082, + 43.8418, + 43.8712, + 43.918899999999994, + 46.057300000000005, + 46.3298, + 46.7072, + 47.6093, + 48.9811, + 49.274699999999996, + 49.698699999999995, + 50.882799999999996 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 118.1832, + "p95Ms": 145.47209999999998, + "samplesMs": [ + 108.5028, + 108.5601, + 109.5988, + 116.0195, + 118.1807, + 118.1832, + 122.0233, + 129.8454, + 133.9758, + 142.121, + 145.47209999999998 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 89.6927, + "p95Ms": 101.5462, + "samplesMs": [ + 82.9861, + 84.0739, + 84.1268, + 84.4961, + 84.6357, + 84.63719999999999, + 85.4838, + 87.0894, + 87.7747, + 89.6927, + 90.5743, + 90.75, + 91.098, + 91.47470000000001, + 91.6004, + 91.6882, + 96.7508, + 98.84370000000001, + 101.5462, + 219.4417 + ] + } + } + } + ] +} diff --git a/desktop/evidence/ci-35448482560-windows-smoke.json b/desktop/evidence/ci-35448482560-windows-smoke.json new file mode 100644 index 000000000..dd1847674 --- /dev/null +++ b/desktop/evidence/ci-35448482560-windows-smoke.json @@ -0,0 +1,17 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "0018e8a2edd9d0033d021ab5b9dac2f60246c84a", + "measuredAtUtc": "2026-09-19T14:38:29.5092853Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2580814, + "installerSha256": "2586DAE019241FFAC9680EDC6DD1AA35B9CA61C0CC315C5632B901A7739B276F", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "F040C60F3A7597ACFBF9D2ED441B20F889902DB29E2D74AF7EEC42EDC3DB6CB9", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35451893591-installed-ui.json b/desktop/evidence/ci-35451893591-installed-ui.json new file mode 100644 index 000000000..4ef97d950 --- /dev/null +++ b/desktop/evidence/ci-35451893591-installed-ui.json @@ -0,0 +1,8 @@ +{ + "commit": "3aea0371f54573a077e3c28ad174d7245a0912d0", + "installedExecutableSha256": "05b555adc731d6382a77ab9e6eec38bc46c6f4eed5199d9bc1e6e39313929775", + "driverSha256": "9e9b1f048d2cc781deee084e6cb6e9f2f3417a33ed45d96cf7c34be4eb23077b", + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "failed", + "browserVersion": null +} diff --git a/desktop/evidence/ci-35451893591-windows-smoke.json b/desktop/evidence/ci-35451893591-windows-smoke.json new file mode 100644 index 000000000..e68fb613d --- /dev/null +++ b/desktop/evidence/ci-35451893591-windows-smoke.json @@ -0,0 +1,17 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "3aea0371f54573a077e3c28ad174d7245a0912d0", + "measuredAtUtc": "2026-09-19T15:43:59.9310984Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2580883, + "installerSha256": "9791F5684B3E27CA449D0D205B6DDC7211C5B09D38B55BE2FFC2CDB516718E25", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "05B555ADC731D6382A77AB9E6EEC38BC46C6F4EED5199D9BC1E6E39313929775", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35453323102-installed-ui.json b/desktop/evidence/ci-35453323102-installed-ui.json new file mode 100644 index 000000000..f119008eb --- /dev/null +++ b/desktop/evidence/ci-35453323102-installed-ui.json @@ -0,0 +1,8 @@ +{ + "commit": "a12d36db49c176445bea3319a077b93ad4fad1e6", + "installedExecutableSha256": "f8565ad6bca0e37a8edc9f956be4d0259f44f2a55b0dd662d8b8c597c6d15e70", + "driverSha256": "9e9b1f048d2cc781deee084e6cb6e9f2f3417a33ed45d96cf7c34be4eb23077b", + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "failed", + "stage": "application_start" +} diff --git a/desktop/evidence/ci-35453323102-windows-smoke.json b/desktop/evidence/ci-35453323102-windows-smoke.json new file mode 100644 index 000000000..3a51929f9 --- /dev/null +++ b/desktop/evidence/ci-35453323102-windows-smoke.json @@ -0,0 +1,17 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "a12d36db49c176445bea3319a077b93ad4fad1e6", + "measuredAtUtc": "2026-09-19T16:09:08.0565212Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2579502, + "installerSha256": "B48557AB2C65A85DA14DD138DC2D749E397BCDECF96FAE2262B5886E2E4685A6", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "F8565AD6BCA0E37A8EDC9F956BE4D0259F44F2A55B0DD662D8B8C597C6D15E70", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35455039808-installed-ui.json b/desktop/evidence/ci-35455039808-installed-ui.json new file mode 100644 index 000000000..3f05e13b4 --- /dev/null +++ b/desktop/evidence/ci-35455039808-installed-ui.json @@ -0,0 +1,22 @@ +{ + "commit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "sourceInstallerCommit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "debugConfiguration": "Temporary machine WebView2 policy for powercontext-desktop.exe on disposable runner; restored on exit", + "installedExecutableSha256": "182484b9557ebc699fb1e7bd3a16b79d2f07180e8ddea52e596a18c8ef31e74c", + "driverSha256": "9e9b1f048d2cc781deee084e6cb6e9f2f3417a33ed45d96cf7c34be4eb23077b", + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "passed", + "stage": "complete", + "webviewDebugEndpointReady": true, + "browserVersion": "152.0.4191.66", + "packagedUrl": "http://tauri.localhost/", + "workflow": { + "serverWheelSha256": "913077e11ca3270e6d357966b412d441a3e70c160321fb984cb5710a2504f89b", + "mode": "anonymous loopback SQLite, no model", + "explicitConnectionAndScope": true, + "saveSearchExactRead": true, + "independentServerExactRead": true, + "bodyAndCitationClipboardPaste": true + }, + "screenshotCaptured": true +} diff --git a/desktop/evidence/ci-35455039808-windows-smoke.json b/desktop/evidence/ci-35455039808-windows-smoke.json new file mode 100644 index 000000000..d68fb540b --- /dev/null +++ b/desktop/evidence/ci-35455039808-windows-smoke.json @@ -0,0 +1,18 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "sourceInstallerCommit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "measuredAtUtc": "2026-09-19T16:43:56.2685673Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2580890, + "installerSha256": "F85CDADF914542EC4AE4135FD06238A09F9CA90F856D7CA019B426C0751E5E1C", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "182484B9557EBC699FB1E7BD3A16B79D2F07180E8DDEA52E596A18C8EF31E74C", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35455040338-installed-ui.json b/desktop/evidence/ci-35455040338-installed-ui.json new file mode 100644 index 000000000..aa0356e91 --- /dev/null +++ b/desktop/evidence/ci-35455040338-installed-ui.json @@ -0,0 +1,22 @@ +{ + "commit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "sourceInstallerCommit": "a12d36db49c176445bea3319a077b93ad4fad1e6", + "debugConfiguration": "Temporary machine WebView2 policy for powercontext-desktop.exe on disposable runner; restored on exit", + "installedExecutableSha256": "f8565ad6bca0e37a8edc9f956be4d0259f44f2a55b0dd662d8b8c597c6d15e70", + "driverSha256": "9e9b1f048d2cc781deee084e6cb6e9f2f3417a33ed45d96cf7c34be4eb23077b", + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "passed", + "stage": "complete", + "webviewDebugEndpointReady": true, + "browserVersion": "152.0.4191.66", + "packagedUrl": "http://tauri.localhost/", + "workflow": { + "serverWheelSha256": "913077e11ca3270e6d357966b412d441a3e70c160321fb984cb5710a2504f89b", + "mode": "anonymous loopback SQLite, no model", + "explicitConnectionAndScope": true, + "saveSearchExactRead": true, + "independentServerExactRead": true, + "bodyAndCitationClipboardPaste": true + }, + "screenshotCaptured": true +} diff --git a/desktop/evidence/ci-35455040338-windows-smoke.json b/desktop/evidence/ci-35455040338-windows-smoke.json new file mode 100644 index 000000000..9dc2b692f --- /dev/null +++ b/desktop/evidence/ci-35455040338-windows-smoke.json @@ -0,0 +1,18 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "sourceInstallerCommit": "a12d36db49c176445bea3319a077b93ad4fad1e6", + "measuredAtUtc": "2026-09-19T16:28:42.3195444Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2579502, + "installerSha256": "B48557AB2C65A85DA14DD138DC2D749E397BCDECF96FAE2262B5886E2E4685A6", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "F8565AD6BCA0E37A8EDC9F956BE4D0259F44F2A55B0DD662D8B8C597C6D15E70", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35455969549-installed-ui.json b/desktop/evidence/ci-35455969549-installed-ui.json new file mode 100644 index 000000000..3d849e5d7 --- /dev/null +++ b/desktop/evidence/ci-35455969549-installed-ui.json @@ -0,0 +1,27 @@ +{ + "commit": "94c45bb62b92b9a6c4e675bc5e2ae265ec0f7c2a", + "sourceInstallerCommit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "debugConfiguration": "Temporary machine WebView2 policy for powercontext-desktop.exe on disposable runner; restored on exit", + "installedExecutableSha256": "182484b9557ebc699fb1e7bd3a16b79d2f07180e8ddea52e596a18c8ef31e74c", + "driverSha256": "9e9b1f048d2cc781deee084e6cb6e9f2f3417a33ed45d96cf7c34be4eb23077b", + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "passed", + "stage": "complete", + "webviewDebugEndpointReady": true, + "browserVersion": "152.0.4191.66", + "packagedUrl": "http://tauri.localhost/", + "workflow": { + "serverWheelSha256": "a769d5a2c8fe7fc61bdc929c021057a2269e63626cb12053c71cb4a0063813fd", + "mode": "anonymous loopback SQLite, no model", + "explicitConnectionAndScope": true, + "saveSearchExactRead": true, + "independentServerExactRead": true, + "bodyAndCitationClipboardPaste": true, + "twoServerConnectionIsolation": true, + "disconnectReconnectClearsContent": true, + "inactiveProfileRemovalPreservesServerData": true, + "enterDoesNotSubmit": true, + "committedLostResponseUnknownWithoutReplay": true + }, + "screenshotCaptured": true +} diff --git a/desktop/evidence/ci-35455969549-windows-smoke.json b/desktop/evidence/ci-35455969549-windows-smoke.json new file mode 100644 index 000000000..f69b05d0e --- /dev/null +++ b/desktop/evidence/ci-35455969549-windows-smoke.json @@ -0,0 +1,18 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "94c45bb62b92b9a6c4e675bc5e2ae265ec0f7c2a", + "sourceInstallerCommit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "measuredAtUtc": "2026-09-19T16:46:13.4618005Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2580890, + "installerSha256": "F85CDADF914542EC4AE4135FD06238A09F9CA90F856D7CA019B426C0751E5E1C", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "182484B9557EBC699FB1E7BD3A16B79D2F07180E8DDEA52E596A18C8EF31E74C", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-35456351561-installed-ui.json b/desktop/evidence/ci-35456351561-installed-ui.json new file mode 100644 index 000000000..b0b68c2f4 --- /dev/null +++ b/desktop/evidence/ci-35456351561-installed-ui.json @@ -0,0 +1,36 @@ +{ + "commit": "fd10d9d670ef83c14351b68ccff847ecfe9ec866", + "sourceInstallerCommit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "debugConfiguration": "Temporary machine WebView2 policy for powercontext-desktop.exe on disposable runner; restored on exit", + "installedExecutableSha256": "182484b9557ebc699fb1e7bd3a16b79d2f07180e8ddea52e596a18c8ef31e74c", + "driverSha256": "9e9b1f048d2cc781deee084e6cb6e9f2f3417a33ed45d96cf7c34be4eb23077b", + "scope": "Hosted Windows runner, actual installed WebView2 with automation enabled; not clean Windows 11 qualification", + "result": "passed", + "stage": "complete", + "webviewDebugEndpointReady": true, + "browserVersion": "152.0.4191.66", + "packagedUrl": "http://tauri.localhost/", + "workflow": { + "serverWheelSha256": "4be8ed7d600753e6cf4f91f44f5f1f2e09cb6a5c8f53b2d97f96eef96721c41e", + "mode": "anonymous loopback SQLite, no model", + "explicitConnectionAndScope": true, + "saveSearchExactRead": true, + "independentServerExactRead": true, + "bodyAndCitationClipboardPaste": true, + "twoServerConnectionIsolation": true, + "disconnectReconnectClearsContent": true, + "inactiveProfileRemovalPreservesServerData": true, + "enterDoesNotSubmit": true, + "committedLostResponseUnknownWithoutReplay": true + }, + "screenshotCaptured": true, + "lifecycle": { + "serverWheelSha256": "4be8ed7d600753e6cf4f91f44f5f1f2e09cb6a5c8f53b2d97f96eef96721c41e", + "applicationExitCode": 1, + "explicitSaveAfterUnknownConfirmed": true, + "forcedOwnedApplicationExit": true, + "serverReadyAfterExit": true, + "originalExactReadAfterExit": true, + "independentWriteAndExactReadAfterExit": true + } +} diff --git a/desktop/evidence/ci-35456351561-windows-smoke.json b/desktop/evidence/ci-35456351561-windows-smoke.json new file mode 100644 index 000000000..d95219ddb --- /dev/null +++ b/desktop/evidence/ci-35456351561-windows-smoke.json @@ -0,0 +1,18 @@ +{ + "scope": "Hosted runner; not standard-user, absent-WebView2 or visual UI qualification", + "runnerImage": "20260907.229.1", + "commit": "fd10d9d670ef83c14351b68ccff847ecfe9ec866", + "sourceInstallerCommit": "e98ff6b9890611593e1a57bc7153f8d581852d86", + "measuredAtUtc": "2026-09-19T16:53:44.5940780Z", + "os": "Microsoft Windows Server 2025 Datacenter 10.0.26100 64-bit", + "buildProfile": "release", + "installerBytes": 2580890, + "installerSha256": "F85CDADF914542EC4AE4135FD06238A09F9CA90F856D7CA019B426C0751E5E1C", + "signature": "NotSigned", + "signerSubject": null, + "installedExecutableSha256": "182484B9557EBC699FB1E7BD3A16B79D2F07180E8DDEA52E596A18C8EF31E74C", + "installedExecutableBytes": 11344384, + "installExit": 0, + "uninstallExit": 0, + "externalSentinelPreserved": true +} diff --git a/desktop/evidence/ci-installed-home.png b/desktop/evidence/ci-installed-home.png new file mode 100644 index 000000000..cecdd4d85 Binary files /dev/null and b/desktop/evidence/ci-installed-home.png differ diff --git a/desktop/evidence/ci-installed-memory.png b/desktop/evidence/ci-installed-memory.png new file mode 100644 index 000000000..5f4e11d94 Binary files /dev/null and b/desktop/evidence/ci-installed-memory.png differ diff --git a/desktop/evidence/installed-reader-zoom.jpg b/desktop/evidence/installed-reader-zoom.jpg new file mode 100644 index 000000000..fe39c3e15 Binary files /dev/null and b/desktop/evidence/installed-reader-zoom.jpg differ diff --git a/desktop/evidence/installed-workflow.json b/desktop/evidence/installed-workflow.json new file mode 100644 index 000000000..1af91a0a1 --- /dev/null +++ b/desktop/evidence/installed-workflow.json @@ -0,0 +1,30 @@ +{ + "source": "dc7f2384 (application sources unchanged through 0018e8a2)", + "buildProfile": "release", + "installerBytes": 2580161, + "installerSha256": "B751CD1EE37803D9909562EFF3422037A0C8B59FFF686714DEDC8764320408A1", + "signature": "NotSigned", + "installedExecutableBytes": 11344896, + "installedExecutableSha256": "E658774A054184F1AF1DDC93FE79B9B0AC1D037C59B292202F24EC24D1248368", + "installExit": 0, + "environment": "Existing Windows 11 developer administrator account; WebView2 already installed", + "nativeUi": "Passed installed anonymous loopback save, FTS, exact read, body/reference clipboard paste verification", + "clipboardCitation": { + "memory_ref": { + "family": "memory", + "artifact_id": "memory", + "revision": 3 + }, + "entry_id": "mem_ent_1b65540cb21e4c1e8dd11272baa722ae", + "entry_version_id": "mem_ver_b6c5bed67afd48479d2eda1931b0dc47" + }, + "normalExitIndependentReadWrite": true, + "zoomObservation": "Five native Ctrl+plus increments; reader and controls accessible by scrolling; exact zoom factor was not queried", + "limitations": [ + "Existing administrator developer environment, not clean standard-user qualification", + "No actual Chinese IME or screen reader trial", + "Exact 200% zoom and 800x600 not established", + "Abnormal-exit trial not executed; computer operation stopped by user", + "Unsigned local release package, not signed distribution qualification" + ] +} diff --git a/desktop/evidence/local-marker-audit.json b/desktop/evidence/local-marker-audit.json new file mode 100644 index 000000000..e3a0992a6 --- /dev/null +++ b/desktop/evidence/local-marker-audit.json @@ -0,0 +1,29 @@ +{ + "observedAt": "2026-09-19", + "scope": "Existing Windows developer account after normal development and installed Desktop exits", + "directories": [ + { + "location": "APPDATA/com.powercontext.desktop.preview", + "fileCount": 1 + }, + { + "location": "LOCALAPPDATA/com.powercontext.desktop.preview", + "fileCount": 335 + } + ], + "markers": [ + "desktopinstalled20260919", + "desktopnative20260919" + ], + "method": "rg -l -a --hidden --no-ignore -F for both exact synthetic markers in each application directory", + "exitCodes": [ + 1, + 1 + ], + "matches": [], + "limitations": [ + "Exact byte marker scan only; transformed or encoded content was not exhaustively inspected", + "Does not cover clipboard history, crash dumps, OS indexing or system logs outside the application directories", + "No GUI interaction or storage mutation performed" + ] +} diff --git a/desktop/evidence/native-workflow.json b/desktop/evidence/native-workflow.json new file mode 100644 index 000000000..2036aa1ab --- /dev/null +++ b/desktop/evidence/native-workflow.json @@ -0,0 +1,27 @@ +{ + "date": "2026-09-19", + "mode": "development WebView2/native, not installed package", + "serverWheel": "powercontext-1.0.1.dev61+g63f918b7e.d20260919-py3-none-any.whl", + "scopeId": "scp_1vr45bjnnvf768v9dv9mact73t", + "syntheticBody": "desktopnative20260919 中文 café\n第二行:纯文本 ", + "clipboardCitation": { + "memory_ref": { + "family": "memory", + "artifact_id": "memory", + "revision": 1 + }, + "entry_id": "mem_ent_71dcb0a7c56a4a00acfdac073b9763d6", + "entry_version_id": "mem_ver_a1d033f0133e44b4b30b50f986775737" + }, + "bodyClipboardVerifiedByPaste": true, + "citationClipboardVerifiedByPaste": true, + "independentReadAndWriteAfterNormalExit": true, + "frontendSource": "dc7f2384", + "nativeExecutableSha256": "5d63f4153d6a07c653bf7257c92212caf6200e439ff034764578041a9df8eaf1", + "limits": [ + "Development window, not installed package", + "Existing administrator account with WebView2", + "Chinese entered through accessibility value setting, not actual IME composition", + "No screen-reader or 800x600/200-percent qualification in this observation" + ] +} diff --git a/desktop/evidence/provider-workflow.json b/desktop/evidence/provider-workflow.json new file mode 100644 index 000000000..a97db8ccf --- /dev/null +++ b/desktop/evidence/provider-workflow.json @@ -0,0 +1,350 @@ +{ + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "architecture": "AMD64", + "python": "3.12.13", + "clientBuildProfile": "debug", + "percentileMethod": "nearest rank", + "budget": "No approved numerical budget; observation only" + }, + "serverWheel": "powercontext-1.0.1.dev61+g63f918b7e.d20260919-py3-none-any.whl", + "serverWheelSha256": "de90495cf9cf66a00a0b063825b458557b63b805faf8ff64f33d2dcd8bb73ec9", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "desktopCommit": "68436a4654d26dbff202b007a680179b6145c9dc", + "desktopWorkingTreeDirty": true, + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": [ + { + "mode": "loopback-anonymous", + "result": "passed", + "serverAliveAfterClientExit": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 30.2695, + "p95Ms": 32.379, + "samplesMs": [ + 28.269, + 28.7192, + 28.8193, + 29.0029, + 29.6855, + 29.7545, + 29.906499999999998, + 29.9391, + 30.1935, + 30.2695, + 30.3727, + 30.4302, + 30.7747, + 30.9317, + 31.1605, + 31.242800000000003, + 31.602999999999998, + 32.0685, + 32.379, + 32.537299999999995 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 79.19239999999999, + "p95Ms": 85.53059999999999, + "samplesMs": [ + 75.2433, + 76.16760000000001, + 76.3025, + 77.473, + 78.68799999999999, + 79.19239999999999, + 80.61659999999999, + 81.96159999999999, + 82.4308, + 85.0175, + 85.53059999999999 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 60.8341, + "p95Ms": 65.2313, + "samplesMs": [ + 57.3011, + 59.1113, + 59.6893, + 59.6905, + 60.0042, + 60.0889, + 60.4541, + 60.5756, + 60.760099999999994, + 60.8341, + 60.8418, + 61.153, + 61.808299999999996, + 62.6105, + 62.6719, + 62.91629999999999, + 62.9174, + 63.30629999999999, + 65.2313, + 65.7423 + ] + } + } + }, + { + "mode": "loopback-bearer", + "result": "passed", + "serverAliveAfterClientExit": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 53.3501, + "p95Ms": 55.233900000000006, + "samplesMs": [ + 51.3061, + 51.9358, + 52.2523, + 52.3349, + 52.3668, + 52.7939, + 53.05929999999999, + 53.1958, + 53.3251, + 53.3501, + 53.4747, + 53.5475, + 53.8163, + 54.044000000000004, + 54.1094, + 54.1454, + 54.3296, + 55.048300000000005, + 55.233900000000006, + 55.4015 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 117.8473, + "p95Ms": 126.94720000000001, + "samplesMs": [ + 103.9538, + 104.8578, + 110.27080000000001, + 115.99289999999999, + 116.1269, + 117.8473, + 120.0056, + 121.3749, + 121.8554, + 125.5154, + 126.94720000000001 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 113.1708, + "p95Ms": 115.7753, + "samplesMs": [ + 109.295, + 109.7586, + 110.01310000000001, + 110.0478, + 111.4325, + 111.446, + 111.8713, + 112.1298, + 112.50640000000001, + 113.1708, + 113.3151, + 113.75699999999999, + 113.7778, + 113.7864, + 114.1905, + 114.2927, + 115.2616, + 115.49629999999999, + 115.7753, + 117.8365 + ] + } + } + }, + { + "mode": "https-bearer-base-path", + "result": "passed", + "serverAliveAfterClientExit": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 54.1608, + "p95Ms": 57.8187, + "samplesMs": [ + 49.3559, + 51.306200000000004, + 51.5103, + 51.8362, + 51.8751, + 53.043, + 53.7028, + 53.744800000000005, + 54.1189, + 54.1608, + 54.2122, + 54.2689, + 55.2536, + 55.744299999999996, + 56.1102, + 56.804300000000005, + 56.8754, + 57.101800000000004, + 57.8187, + 60.5086 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 114.8705, + "p95Ms": 126.1707, + "samplesMs": [ + 111.85889999999999, + 112.6129, + 112.9396, + 114.1066, + 114.118, + 114.8705, + 114.9785, + 119.89099999999999, + 121.036, + 122.0662, + 126.1707 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 113.98859999999999, + "p95Ms": 123.13159999999999, + "samplesMs": [ + 110.1799, + 110.4536, + 110.8698, + 111.9226, + 112.2622, + 112.2762, + 112.3218, + 112.3904, + 112.6963, + 113.98859999999999, + 114.2398, + 114.88969999999999, + 115.02130000000001, + 115.2676, + 117.26159999999999, + 118.3453, + 119.12920000000001, + 123.0976, + 123.13159999999999, + 130.87380000000002 + ] + } + } + }, + { + "mode": "loopback-provider", + "result": "passed", + "serverAliveAfterClientExit": true, + "providerIdentityChangeInvalidatesContext": true, + "sameIdentityRevocationDeniesHistoricalCitation": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 54.2451, + "p95Ms": 56.9688, + "samplesMs": [ + 52.5259, + 53.0028, + 53.330799999999996, + 53.8071, + 53.815799999999996, + 53.8243, + 53.982, + 54.080799999999996, + 54.2214, + 54.2451, + 54.8422, + 54.864200000000004, + 55.0943, + 55.1307, + 55.7299, + 55.8115, + 56.5067, + 56.5986, + 56.9688, + 57.0085 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 115.876, + "p95Ms": 121.8325, + "samplesMs": [ + 110.5975, + 113.703, + 114.1868, + 114.92660000000001, + 115.1749, + 115.876, + 118.9081, + 118.9803, + 121.0571, + 121.22189999999999, + 121.8325 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 112.6643, + "p95Ms": 114.9313, + "samplesMs": [ + 108.29719999999999, + 110.08840000000001, + 111.3542, + 111.79039999999999, + 111.9251, + 112.29, + 112.3811, + 112.53299999999999, + 112.5393, + 112.6643, + 113.1046, + 113.92009999999999, + 114.3522, + 114.378, + 114.4457, + 114.65599999999999, + 114.6627, + 114.8631, + 114.9313, + 116.62060000000001 + ] + } + } + } + ] +} diff --git a/desktop/evidence/response-loss-workflow.json b/desktop/evidence/response-loss-workflow.json new file mode 100644 index 000000000..74478eee7 --- /dev/null +++ b/desktop/evidence/response-loss-workflow.json @@ -0,0 +1,354 @@ +{ + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "architecture": "AMD64", + "python": "3.12.13", + "clientBuildProfile": "debug", + "percentileMethod": "nearest rank", + "budget": "No approved numerical budget; observation only" + }, + "serverWheel": "powercontext-1.0.1.dev61+g63f918b7e.d20260919-py3-none-any.whl", + "serverWheelSha256": "de90495cf9cf66a00a0b063825b458557b63b805faf8ff64f33d2dcd8bb73ec9", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "desktopCommit": "a076db494406f97146266a0b638eb20a0b32e880", + "desktopWorkingTreeDirty": true, + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": [ + { + "mode": "loopback-anonymous", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 29.4789, + "p95Ms": 31.128, + "samplesMs": [ + 28.4794, + 28.7338, + 28.932699999999997, + 29.1001, + 29.297900000000002, + 29.3585, + 29.3631, + 29.401400000000002, + 29.4076, + 29.4789, + 29.7032, + 29.8492, + 29.8764, + 30.0833, + 30.1275, + 30.1626, + 30.304299999999998, + 30.461100000000002, + 31.128, + 31.2451 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 81.6545, + "p95Ms": 86.81790000000001, + "samplesMs": [ + 75.4132, + 77.05, + 77.2263, + 78.0856, + 81.05510000000001, + 81.6545, + 82.03190000000001, + 82.98039999999999, + 83.5127, + 86.3718, + 86.81790000000001 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 59.643, + "p95Ms": 61.7901, + "samplesMs": [ + 57.708299999999994, + 58.6502, + 58.776, + 58.8505, + 58.8927, + 58.897600000000004, + 58.945299999999996, + 59.3317, + 59.431900000000006, + 59.643, + 59.7027, + 60.5826, + 60.6184, + 60.6724, + 60.879599999999996, + 60.9154, + 61.3767, + 61.5705, + 61.7901, + 62.002700000000004 + ] + } + } + }, + { + "mode": "loopback-bearer", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 53.4789, + "p95Ms": 55.2299, + "samplesMs": [ + 50.148199999999996, + 51.0566, + 52.2693, + 52.3593, + 52.741099999999996, + 52.7697, + 53.018, + 53.3823, + 53.4325, + 53.4789, + 53.5124, + 53.748999999999995, + 54.021300000000004, + 54.0339, + 54.0566, + 54.113, + 54.1849, + 54.6182, + 55.2299, + 55.628 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 116.872, + "p95Ms": 119.9421, + "samplesMs": [ + 113.96679999999999, + 114.2354, + 114.7821, + 115.1465, + 115.9893, + 116.872, + 117.862, + 118.1309, + 119.1059, + 119.8263, + 119.9421 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 112.4813, + "p95Ms": 114.76509999999999, + "samplesMs": [ + 107.79329999999999, + 108.0247, + 110.20389999999999, + 110.4506, + 110.999, + 111.16460000000001, + 111.3259, + 111.4483, + 112.4542, + 112.4813, + 112.6781, + 112.8422, + 113.08980000000001, + 113.37010000000001, + 113.8893, + 114.0663, + 114.2929, + 114.4273, + 114.76509999999999, + 114.7957 + ] + } + } + }, + { + "mode": "https-bearer-base-path", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 54.1177, + "p95Ms": 55.2219, + "samplesMs": [ + 51.3259, + 52.8532, + 52.9935, + 53.1028, + 53.402100000000004, + 53.6512, + 53.790699999999994, + 53.8271, + 53.841899999999995, + 54.1177, + 54.3084, + 54.439800000000005, + 54.481300000000005, + 54.4959, + 54.866699999999994, + 54.9314, + 55.0258, + 55.1079, + 55.2219, + 55.894600000000004 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 118.10130000000001, + "p95Ms": 127.08959999999999, + "samplesMs": [ + 112.2261, + 113.9711, + 114.8244, + 117.8189, + 117.9435, + 118.10130000000001, + 118.9041, + 118.9436, + 123.9936, + 126.14529999999999, + 127.08959999999999 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 113.07209999999999, + "p95Ms": 117.37209999999999, + "samplesMs": [ + 110.5259, + 111.3192, + 111.8053, + 112.0904, + 112.3092, + 112.42439999999999, + 112.5299, + 112.7843, + 113.025, + 113.07209999999999, + 113.78729999999999, + 114.34519999999999, + 114.52130000000001, + 114.5681, + 114.84419999999999, + 114.9788, + 115.16940000000001, + 115.2437, + 117.37209999999999, + 119.9197 + ] + } + } + }, + { + "mode": "loopback-provider", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": true, + "sameIdentityRevocationDeniesHistoricalCitation": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 55.4683, + "p95Ms": 58.7827, + "samplesMs": [ + 52.3785, + 52.9123, + 53.7119, + 53.9629, + 54.7917, + 54.8381, + 54.904, + 54.950199999999995, + 55.4282, + 55.4683, + 55.5831, + 55.7898, + 56.0741, + 56.163900000000005, + 56.641200000000005, + 58.3321, + 58.392199999999995, + 58.6375, + 58.7827, + 60.2858 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 116.8417, + "p95Ms": 122.9537, + "samplesMs": [ + 109.7378, + 112.9399, + 112.9962, + 115.2126, + 115.8925, + 116.8417, + 116.9732, + 117.9649, + 120.27980000000001, + 122.8906, + 122.9537 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 114.89439999999999, + "p95Ms": 125.633, + "samplesMs": [ + 109.5987, + 110.26769999999999, + 111.7705, + 112.05189999999999, + 112.0772, + 112.1439, + 113.5249, + 113.7592, + 114.11630000000001, + 114.89439999999999, + 115.0019, + 115.288, + 116.35379999999999, + 117.2598, + 120.06609999999999, + 120.3123, + 120.75909999999999, + 121.6819, + 125.633, + 126.2878 + ] + } + } + } + ] +} diff --git a/desktop/evidence/scope-pagination-workflow.json b/desktop/evidence/scope-pagination-workflow.json new file mode 100644 index 000000000..fe6f5aadb --- /dev/null +++ b/desktop/evidence/scope-pagination-workflow.json @@ -0,0 +1,358 @@ +{ + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "architecture": "AMD64", + "python": "3.12.13", + "clientBuildProfile": "debug", + "percentileMethod": "nearest rank", + "budget": "No approved numerical budget; observation only" + }, + "serverWheel": "powercontext-1.0.1.dev61+g63f918b7e.d20260919-py3-none-any.whl", + "serverWheelSha256": "de90495cf9cf66a00a0b063825b458557b63b805faf8ff64f33d2dcd8bb73ec9", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "desktopCommit": "28d992652eea0fded84d9da2c70399de05cb01b2", + "desktopWorkingTreeDirty": true, + "networkScope": "Independent loopback HTTP/TLS fixture processes; not an external production deployment", + "cases": [ + { + "mode": "loopback-anonymous", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "sameTitleScopePagination51": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 30.323, + "p95Ms": 32.7919, + "samplesMs": [ + 27.7346, + 28.278399999999998, + 28.4371, + 28.984099999999998, + 29.4956, + 29.5182, + 29.7836, + 29.8616, + 30.0775, + 30.323, + 30.5553, + 30.5565, + 30.684, + 30.6952, + 30.9423, + 31.1461, + 31.1982, + 32.59780000000001, + 32.7919, + 33.305099999999996 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 82.9445, + "p95Ms": 88.8189, + "samplesMs": [ + 75.6319, + 75.8455, + 78.74449999999999, + 81.525, + 82.74579999999999, + 82.9445, + 83.82000000000001, + 83.9511, + 87.2227, + 87.24419999999999, + 88.8189 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 60.543600000000005, + "p95Ms": 67.4299, + "samplesMs": [ + 55.5101, + 56.4845, + 56.6663, + 57.1742, + 57.495200000000004, + 58.0458, + 59.716699999999996, + 60.1391, + 60.271699999999996, + 60.543600000000005, + 60.957699999999996, + 61.025999999999996, + 61.2198, + 61.37370000000001, + 62.2301, + 62.3284, + 62.676899999999996, + 62.9808, + 67.4299, + 72.15950000000001 + ] + } + } + }, + { + "mode": "loopback-bearer", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "sameTitleScopePagination51": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 53.7421, + "p95Ms": 56.1734, + "samplesMs": [ + 49.6623, + 52.134899999999995, + 52.5739, + 52.5829, + 52.7896, + 52.8181, + 53.0464, + 53.157500000000006, + 53.4105, + 53.7421, + 53.8108, + 54.1305, + 54.1684, + 54.6642, + 54.8651, + 54.9981, + 55.0456, + 55.4695, + 56.1734, + 56.1838 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 116.9487, + "p95Ms": 123.3223, + "samplesMs": [ + 112.6466, + 112.8772, + 115.9299, + 116.1064, + 116.8536, + 116.9487, + 119.97829999999999, + 120.0778, + 121.067, + 122.86500000000001, + 123.3223 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 112.53739999999999, + "p95Ms": 116.7614, + "samplesMs": [ + 100.8131, + 109.6514, + 110.14970000000001, + 110.3231, + 110.7435, + 111.4512, + 111.66850000000001, + 111.8999, + 112.21340000000001, + 112.53739999999999, + 113.1675, + 113.2288, + 113.35130000000001, + 113.9744, + 114.2902, + 114.77619999999999, + 115.42869999999999, + 116.1892, + 116.7614, + 119.2602 + ] + } + } + }, + { + "mode": "https-bearer-base-path", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": false, + "sameIdentityRevocationDeniesHistoricalCitation": false, + "sameTitleScopePagination51": false, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 54.8429, + "p95Ms": 57.8988, + "samplesMs": [ + 49.8754, + 50.113, + 53.036, + 53.3384, + 53.411699999999996, + 53.494, + 53.5546, + 53.601000000000006, + 54.1813, + 54.8429, + 54.8721, + 54.9225, + 55.0791, + 55.1497, + 55.3564, + 55.3882, + 56.0028, + 56.7957, + 57.8988, + 58.8346 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 118.982, + "p95Ms": 122.9194, + "samplesMs": [ + 108.0489, + 112.84230000000001, + 113.13810000000001, + 116.017, + 117.866, + 118.982, + 119.04, + 119.0513, + 121.0624, + 121.9427, + 122.9194 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 113.91080000000001, + "p95Ms": 118.1906, + "samplesMs": [ + 105.7501, + 107.86800000000001, + 110.2442, + 111.46209999999999, + 111.5999, + 112.5094, + 112.51469999999999, + 112.8079, + 112.93599999999999, + 113.91080000000001, + 114.2573, + 114.53009999999999, + 114.7787, + 114.83279999999999, + 114.90350000000001, + 115.8439, + 117.3873, + 118.16, + 118.1906, + 122.7113 + ] + } + } + }, + { + "mode": "loopback-provider", + "result": "passed", + "serverAliveAfterClientExit": true, + "committedWriteWithLostResponseUnknownWithoutReplay": true, + "providerIdentityChangeInvalidatesContext": true, + "sameIdentityRevocationDeniesHistoricalCitation": true, + "sameTitleScopePagination51": true, + "performance": { + "exactRead": { + "count": 20, + "p50Ms": 55.65260000000001, + "p95Ms": 62.231500000000004, + "samplesMs": [ + 51.8671, + 52.4547, + 53.9941, + 54.400400000000005, + 54.7057, + 54.7872, + 54.888, + 55.455400000000004, + 55.6036, + 55.65260000000001, + 55.9532, + 56.137, + 56.162499999999994, + 56.7101, + 56.7465, + 57.9137, + 58.4644, + 62.1083, + 62.231500000000004, + 62.87429999999999 + ] + }, + "noteCount": 13, + "save": { + "count": 11, + "p50Ms": 118.0147, + "p95Ms": 128.2481, + "samplesMs": [ + 113.0279, + 113.1726, + 115.985, + 116.0062, + 116.8746, + 118.0147, + 120.257, + 121.0209, + 122.7998, + 123.9937, + 128.2481 + ] + }, + "scope": "Native ConnectionManager round trips, including identity recheck; local fixture; no UI latency or approved budget", + "search": { + "count": 20, + "p50Ms": 115.70909999999999, + "p95Ms": 122.3334, + "samplesMs": [ + 107.14460000000001, + 112.5375, + 113.50250000000001, + 113.60810000000001, + 113.78960000000001, + 113.8295, + 114.6253, + 115.00779999999999, + 115.48089999999999, + 115.70909999999999, + 115.873, + 116.1477, + 116.16980000000001, + 116.3285, + 116.53429999999999, + 116.9108, + 117.0007, + 117.52029999999999, + 122.3334, + 122.53699999999999 + ] + } + } + } + ] +} diff --git a/desktop/evidence/source-manifest.json b/desktop/evidence/source-manifest.json new file mode 100644 index 000000000..3ef8bb221 --- /dev/null +++ b/desktop/evidence/source-manifest.json @@ -0,0 +1,52 @@ +{ + "algorithm": "SHA-256", + "baseCommit": "aa697c5315204249e090acdf1a00d0582e851c5f", + "files": { + "desktop/.gitattributes": "e861d974e31c847b35f5a17ba0c442c00b5e6204a9fd22475aee367db57802ca", + "desktop/.gitignore": "d9dc197ff5a68fa87d4b803a2548de630f0d2f5162f9bc228782a998a8fe91bd", + "desktop/.mise.toml": "dbf0eb56655bde88f4f7c1aa3781fe26de6e49e06be50fcf97db4bda5c1e26db", + "desktop/package.json": "4735d9fc9d1f77c7dd18ab9c67bf2a2fc73dbda8dcd19a6270926894efa49a3e", + "desktop/pnpm-lock.yaml": "d1ecf3e76c569cd19d2e8121e4dc9f0420fa30f48faaa791b66396d5efed730e", + "desktop/pnpm-workspace.yaml": "5946bc4237b6ff484037870fbbe508cf053a9beccd3f2d0567858adf23a9c8be", + "desktop/rust-toolchain.toml": "050bd535fc1ff64b1c2e01651f00f4937bcd559fc787679f1157aafe178af05e", + "desktop/scripts/generate.mjs": "918893d09900d147f45a6e2d8785299adfac7cd8f11cc837ebd8b28eef9cfbf7", + "desktop/scripts/icons.mjs": "596d53761a804425ccf285a9e897442434745f2438baae86d702fc7e38e988b3", + "desktop/src-tauri/build.rs": "676f0ad813c595bdc9fae6de27df65e0779a2853c3ce87c1fdfea40e001039ff", + "desktop/src-tauri/capabilities/main.json": "6eb2b89504d8c58bc127abfca44ddaa5d46b7864032c440a34f961cff44147aa", + "desktop/src-tauri/Cargo.lock": "6045e2fcc843792f7dc678cef4e2fa07a2ed97e80191b4ca0755e048e74b0c7d", + "desktop/src-tauri/Cargo.toml": "7513f5f4c78f6ca5c9792e96d6e47eba14f8495214f587b4a4e11825ba8f74da", + "desktop/src-tauri/examples/credential_probe.rs": "d57ee47bb15b53d969d9543370587bc010f626ea6da793b86c6c018c33552737", + "desktop/src-tauri/examples/export_ipc.rs": "91726d6cf36d52ee048b4535ebc28ad9dd47285c476b22621bb31bbf06987f54", + "desktop/src-tauri/icons/brand.png": "6290e31c3cc920f827d52bded3db56a012ed6aba239bb423771c8c769e24b794", + "desktop/src-tauri/icons/icon.ico": "2739d373ed7ce348f7c00b2dc6dc66331e0ebd151d4deb92de81c0bc8435cae2", + "desktop/src-tauri/icons/icon.png": "d9621cfb49788516d6113f2e822bc03de739d486865ce9680becfca76568902b", + "desktop/src-tauri/src/credentials.rs": "63d13ac95fafac2e8b25320165319a5bf8c9a2ceb8521ee5f63d759583047fe9", + "desktop/src-tauri/src/error.rs": "a3bd62f84df513da6bbedead573a831bf5ac91619043169670d230c91eec2859", + "desktop/src-tauri/src/ipc.rs": "db330929b2303672f4f12cdc0a6ba4bd3a8de0adc45f321600823043fe373e87", + "desktop/src-tauri/src/lib.rs": "f9d3ab770dea3cbf2bc0db4bdd2194641c832b49852df624b77c0583b25581d1", + "desktop/src-tauri/src/main.rs": "3e78ad1e12cf02524caefeaca0a8e1d355ccb167025ccd4b90a66eac0f9974f7", + "desktop/src-tauri/src/transport/mod.rs": "77c1e4973ded396f1fefc3dd26f0fa960d49a6091e19d646ea08e544fa58ff1f", + "desktop/src-tauri/src/transport/operations.json": "e336d58bae6319b9c90e11e0b11c22c47f60650483f5b9d8d396ceb627333973", + "desktop/src-tauri/tauri.conf.json": "a9c94ac973c658cfa16f1fa8613dd21f9267934357367332ed6fa98ff223fdd1", + "desktop/src-tauri/tests/ipc.rs": "a9221a8ea2e35069a1fad96a74a6e158e9ffcb5cad0f16071971ff0a2c6c98c5", + "desktop/src-tauri/tests/tls.rs": "c1d33986368b732d772759a91393d68821200f423f7db3a5e738e0bae5f90fd2", + "desktop/src-tauri/tests/transport.rs": "5f5965543062516397dc63419abe7073669e98c9c4cb6648428629fed904922b", + "desktop/tsconfig.json": "d675aa0fb23f549589c5f61209d29bc08451f6e222c56b7e25d1755252ccf2ff", + "desktop/ui/index.html": "821dda39212fb3f18c07829a65ec6da20f559e969eb4dc6276cacce4ccbff511", + "desktop/ui/src/app/App.tsx": "c1bb096637e4f2a139b7a90336b1bbb3056e142422e9e07f214f02f4bb0970d3", + "desktop/ui/src/app/messages.ts": "883ef94575f36d5ef6d71b16c7461256657ceb474511f5914d2261cf8a31e82f", + "desktop/ui/src/app/style.css": "3c3a4e7a44cc12ab8b9a1abf904c00b8ae43a186d3e43a91ce6d1f964ffd3223", + "desktop/ui/src/assets/connections.svg": "e15016461cfcfcbf173f6dd36b4a2dd528c4ca3e1a0a9e7f35f3bfb8462ad898", + "desktop/ui/src/assets/memory.svg": "10d6038ffdd02bb0ec97725ec8fe0a0eaa5535c1a7a0baa03af5bc8663f44f01", + "desktop/ui/src/assets/overview.svg": "532e26726d8223f301a5fb996f75fd38e80a4e143642300615254e86023240ad", + "desktop/ui/src/generated/api.d.ts": "0ab11a453eaf86ab63c7c35f624cfe5d5e6b60a2cce8aa0890e4c263cbdc2ee2", + "desktop/ui/src/generated/ipc.ts": "25192168a9caa50666ae9ae743a4f3d1fb872711c12ba10f9a20d44a90864233", + "desktop/ui/src/generated/operations.ts": "5683b2f3b6329e3e184cc06b6b4eca52dc4d275342ce4d9c50cc3c76e5a7f0b5", + "desktop/ui/src/main.tsx": "ef2cde0a9a5c7a9bea5c37f05d4a7e111fadeba6519c4813b55b5059f1d77d66", + "desktop/ui/src/shared/ipc.ts": "35585044901f502b0aa649ab34aed4437c3c82620eb2425925bf348f0c1f88b0", + "desktop/ui/tests/app.test.tsx": "5da31c5bef77f4e59771d84cd7e31f56b5846da38d483ba7098df1d33f93fdee", + "desktop/vite.config.ts": "a90de8f19b1b2f01fe801f0797630ec5b44874d1cd05da7360244ac136f12624", + ".github/workflows/desktop.yml": "adbe6433d6a2ffbe442abd55cdeeb1b32ab5ea2f3d399cf42d26439bfe41b675", + "desktop/scripts/windows-smoke.ps1": "b36588f165a53b6ab5fefab340da5e089dc125015e874427d4fe61653b08cd2d" + } +} 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..b81f7eea8 --- /dev/null +++ b/desktop/src-tauri/src/connections/compatibility.json @@ -0,0 +1,21 @@ +[ + { + "id": "sqlite-63f918b7-v1", + "serverCommit": "63f918b7ee8076965d488a3565020b70eee6a472", + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "artifactSha256": "de90495cf9cf66a00a0b063825b458557b63b805faf8ff64f33d2dcd8bb73ec9", + "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/evidence/S2.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..fd7d8651f --- /dev/null +++ b/desktop/src-tauri/src/diagnostics.rs @@ -0,0 +1,342 @@ +/* + * 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.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..e63cc4d2e --- /dev/null +++ b/desktop/src-tauri/src/transport/operations.json @@ -0,0 +1,45 @@ +{ + "contractSha256": "de9750808e12e7a6c7a88b736b5368ba3b7c5fcf3c986f1c8cb762b66afce03c", + "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..2d7a5aa37 --- /dev/null +++ b/desktop/tests/installed_workflow.py @@ -0,0 +1,303 @@ +# 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(self, xpath: str) -> None: + self.post(f"/element/{self.element(xpath)}/click", {}) + + 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-63f918b7-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.post(f"/element/{field}/click", {}) + 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} +