Commit Graph
5 Commits
Author SHA1 Message Date
Jason bee47fdf48 feat(deeplink): let provider links bring their own icon via iconUrl
A provider link can now carry `iconUrl`, an https address of the site's
own logo. The confirmation dialog downloads it as soon as it opens and
shows it at the top; importing stores it as the card's custom icon. No
extra confirmation step. Any failure is silent: the card falls back to
the built-in `icon` name, or to the name's initials.

The image goes through the same path as a local upload: the backend
returns a data URI, the frontend redraws it on a canvas into a 96x96 PNG,
and the provider save validation checks it again. The address itself is
never stored or rendered, and the import command never downloads.

The download (`deeplink/icon_fetch.rs`):
- Parsing drops addresses that are not https, use an IP or a non-default
  port, carry credentials or exceed 2048 characters; the rest of the link
  still imports.
- Every resolved address must be public, and the connection is pinned to
  them so a second DNS answer cannot point it elsewhere. Redirects are
  followed by hand, up to 3, and each hop is checked again.
- No system proxy, no cookies, no Referer, 5 s overall timeout, body read
  as a stream and capped at 256 KB.
- Content-Type must be PNG, JPEG, WebP or SVG and match the file's magic
  bytes; bitmap dimensions are read from the header and capped at 4096.
- 198.18.0.0/15 is allowed on purpose: Clash and Surge fake-ip mode
  resolve every domain into it. The trade-off is written next to
  `is_public_ipv4`.
- Errors are logged without the URL, which may carry query tokens.

Closes #8117
2026-10-12 23:26:43 +08:00
Jason 16e7ff1c9d feat(deeplink): let links declare the API format, import Pi providers, and leave activation to the user
- Add a single `apiFormat` parameter (anthropic / openai_chat /
  openai_responses / gemini_native) that maps to each app's own field:
  meta.apiFormat for Claude Code, Codex and Grok Build (non-native formats
  need local routing), `npm` for OpenCode, `api` for OpenClaw and Pi,
  `api_mode` for Hermes. Unsupported combinations are rejected while
  parsing; links without it keep the previous defaults byte for byte.
- Support Pi provider links. They write a models.json node and must carry
  a model.
- The confirmation dialog now has a switch for "switch after import"
  (providers), "add to the app config" (additive apps, on by default) and
  "enable after import" (prompts). The link's `enabled` only sets its
  initial state.
- Turning the switch off really only saves the row: a new
  `ProviderService::add_without_switching` skips the first-provider path
  in `add` that would otherwise write the client config. The deep-link
  golden rows now have is_current false accordingly.
- Reject link values that additive apps would expand: Pi keys starting
  with `!` and `$NAME` / `${NAME}`, OpenCode `{env:}` / `{file:}`, and
  `${...}` for OpenClaw and Hermes. These would run a command or send
  local secrets and files to the link's endpoint. Checked when parsing,
  when merging `config`, and when building the provider.
- Update the deep-link manual in zh/en/ja.
2026-10-12 23:26:43 +08:00
Jason 450bc039f0 feat(deeplink): enable imported usage scripts by default
A provider link that carries a usage script now enables usage query
unless it says `usageEnabled=false`. Site owners attach the script so
users see their balance; making them switch it on again after import
was an extra step with little safety benefit: the script runs in the
QuickJS sandbox, sends a single request, and deep-link scripts have no
template type, so that request must be same-origin HTTPS with the base
URL.

Links without a script still import usage fields disabled. The
confirmation badge follows the same rule as the backend, and the
script body is still shown in full before the user imports.
2026-10-12 23:26:43 +08:00
Jason 98222d19e3 fix(deeplink): stop losing links on cold start and tighten import checks
Deep links that arrived before the frontend was listening were dropped:
on Windows/Linux the plugin emits the startup URL before our handler is
registered, and Tauri events fired before the JS listener exists (cold
start, window rebuilt in lightweight mode) go nowhere.

- Keep the parsed link (or parse error) in a backend pending slot with a
  monotonic id and emit `deeplink-pending`. The dialog peeks on mount and
  on every notification, and acks by id only after the link is shown, so
  an interrupted read never loses it and a stale ack never clears a newer
  link.
- Read `get_current()` after registering the URL handler so the startup
  URL is handled on Windows/Linux.
- Drop our own `RunEvent::Opened` handling on macOS; the deep-link plugin
  already emits it, which made links open twice.
- Process pending links serially in the dialog (rerun on notifications
  that arrive mid-processing) so a late read of an older link can't
  replace a newer one, including during config merge. The effect no
  longer depends on `t`, so the startup language sync can't interrupt it.
- Reject `configUrl` up front instead of failing after the user confirms.
- Accept `mcode` for prompts and `pi`/`mcode` for MCP apps.
- Warn on the confirm dialog when the provider will be switched to right
  away (`enabled=true`), and refresh the tray menu after a provider import.
2026-10-12 23:26:43 +08:00
zayokaandClaude Fable 5 6b8f36431b fix(security): cap scripts, file reads, and proxy bodies; surface deeplink usage fields (#5919)
* fix(security): harden usage script execution and Grok session log collection

Three hardening fixes in v3.19.0 code:

1. **usage_script: cap JS runtime resources and execution time**
   `execute_usage_script` used a vanilla `Runtime::new()` with no limits,
   so a malicious or buggy script delivered via deeplink or synced DB could
   hang the backend thread forever with `while(true)`. Add
   `create_script_runtime()` enforcing a 5-second interrupt handler, 16 MiB
   memory limit, and 256 KiB stack limit. Regression test confirms an
   infinite-loop script is rejected within seconds instead of blocking.

2. **Grok session log: skip oversized files**
   `sync_single_grok_file` loaded the entire `updates.jsonl` into memory
   with no size cap. Add a 50 MiB limit and skip files that exceed it.

3. **Grok session log: bound directory traversal**
   `collect_files_named` recursed without depth limits and followed
   symlinks, so a symlink cycle under `~/.grok/sessions` caused a stack
   overflow. Add a 16-level depth limit and skip symlinks entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): show hidden usage access token and user id in deeplink provider import dialog

The provider import confirmation dialog parsed usageAccessToken and
usageUserId from ccswitch:// URLs and persisted them in
ProviderMeta.usage_script, but never rendered them. Users could not see
these credentials before approving the import.

- Add i18n keys for usageAccessToken/usageUserId in all locales.
- Render both fields in the usage script section; mask the access token
  like usageApiKey.
- Add a regression test verifying the dialog displays the masked token
  and the user id.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(security): bound Codex model catalog and proxy response bodies

Two issues were found while auditing security boundaries:

1. **Codex model_catalog_json arbitrary file read**
    only checked the filename and
   accepted any absolute path. A malicious  could
   point CC Switch at an external file (e.g. ) or a huge
   file to exhaust memory.

   - Require the resolved path to stay inside
     using a new  helper (lexical, no filesystem calls).
   - Treat Unix-style absolute paths as absolute on Windows.
   - Cap catalog reads at 32 MiB.

2. **Unbounded proxy response bodies / compression bombs**
   Non-streaming responses and error responses were read with
    and fully decompressed without any byte
   limit, allowing a malicious upstream to OOM the backend.

   - Add  with a 128 MiB ceiling.
   - Reject oversized bodies and oversized decompressed payloads with
     .
   - Map the new error variant into the Codex proxy error response.

Regression tests cover: absolute-path containment, traversal rejection,
oversized catalog file, oversized buffered/streamed proxy responses.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ci): set up pnpm via Corepack instead of pnpm/action-setup v6

pnpm/action-setup v6 installs pnpm through a two-step self-installer:
it bootstraps the latest pnpm (11.7.0) via npm, then downloads the
pinned version (10.12.3) from the npm registry to switch. When that
second tarball fetch failed (error 23), the self-installer still
reported "done" and crashed with ENOENT on @pnpm/linux-x64's
package.json, failing the CI setup step.

Replace the action with Node 20's bundled Corepack and pin the version
once via package.json's packageManager field, so setup performs a
single exact-version download with no floating bootstrap. This also
lets release.yml drop its Windows ARM64 special case, since Corepack
has no arch restrictions, and pins the same version for local dev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): close review gaps in proxy body caps, deeplink dialog, catalog path

Address maintainer review on the security PR:

- Proxy: bound the four remaining unbounded forwarder read paths
  (non-2xx error body, failover pre-read, both 2xx error-envelope
  validators) with bytes_with_limit + bounded decompression.
- Proxy: bytes_with_limit now accumulates Hyper/Reqwest/Streamed bodies
  chunk-by-chunk and aborts mid-stream instead of collecting the full
  body before comparing; Buffered keeps the post-hoc check.
- Proxy: decompression is budgeted at the decoder (Read::take on
  gzip/deflate/zstd, bounded brotli reader) via decompress_body_with_limit,
  so bombs are cut off at the byte budget instead of after full expansion,
  including stacked-encoding intermediates.
- Deeplink: show the usage section whenever any usage field is present,
  matching build_provider_meta persistence — a deeplink carrying only
  usageAccessToken/usageUserId no longer imports credentials invisibly.
- Codex catalog: after the lexical containment check, canonicalize the
  existing file and re-verify containment so symlinks inside ~/.codex
  cannot escape the config dir; the path_is_within doc comment no longer
  oversells the lexical check as symlink-safe.
- Grok sync: comment now matches the skip-all-symlinks behavior and each
  skip is logged, so sessions behind a symlink are diagnosable.
- Docs: CONTRIBUTING notes the packageManager pnpm pin and Corepack flow.

Regression tests follow the same methodology as round 1: reverting a
guard fails the corresponding test (server write-count assertions prove
the mid-stream abort; a truncated gzip stream distinguishes TooLarge
from read-to-end errors; symlink escape is rejected only after the
canonicalize re-check).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:44:08 +08:00