Find a file
Tony Frazier 47d52a822b
Some checks are pending
CI / build-desktop (push) Waiting to run
CI / build-android (push) Waiting to run
CI / build-ios (push) Waiting to run
Rust Lint / Format (push) Waiting to run
Rust Lint / Clippy (push) Waiting to run
Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622)
## Summary

Remote HTTP goes through Tauri's HTTP plugin, and the plugin's defaults
fall short of what the app's callers need. A response body that stalled
after the headers hung forever. Local servers (stock Ollama, ComfyUI)
rejected the webview `Origin` the plugin attaches. Responses arrived
uncompressed, TLS ignored the OS trust store, and the plugin kept a
cookie jar nobody used. Vault portrait downloads bypassed the plugin
altogether. This branch fixes each of these and routes every plugin call
through one wrapper, `httpFetch`.

## Changes

### Timeouts and aborts

- **Request guard** (`utils/idleTimeout.ts`): `createRequestGuard`
combines the header timer, the caller's abort signal and a per-read idle
timer on the body. `withIdleTimeout` re-wraps the body so a stalled read
errors with `RequestTimeoutError`. The timer runs only while a read
waits on upstream, so a slow consumer never trips it.
`createTimeoutFetch`, `imageFetch` and `imageGetFetch` all use the guard
in place of their own timer/abort code.
- **Timeout errors**: `RequestTimeoutError` (`utils/error.ts`) is
deliberately not named `TimeoutError`, which the AI SDK treats as an
abort. `mapError` also turns the plugin's bare-string and plain-`Error`
abort rejections into an `AbortError` when the caller aborted.
`imageFetch` no longer reports a caller-caused `AbortError` as a
timeout.
- **LLM fetch**: error responses and non-JSON (streaming) responses get
the idle timer. Buffered JSON responses don't; the header timer bounds
them, since `dispose()` runs after `response.text()`.
- **Fake provider**: new `stream-stall` model (some text, then silence
with the connection open), documented in `docs/development/testing.md`.

### One entry point into the plugin

- **`httpFetch`** (`utils/http.ts`) wraps the plugin's `fetch` and sets
`Origin: ''` unless the caller sets one. The plugin drops an empty
`Origin` under `unsafe-headers`, so local servers no longer see the
webview origin. A caller's own `Origin` and `Referer` are sent as set.
- All plugin callers now use it: `createTimeoutFetch`, the image fetch
helpers, ComfyUI's `fetchApi`, `pingModel`, `corsFetch` and the updater.
An ESLint `no-restricted-imports` rule rejects any other import of the
plugin's `fetch` outside `http.ts` and tests.

### Plugin features (`src-tauri/Cargo.toml`)

- `tauri-plugin-http` default features are off and its list is spelled
out, so the `cookies` jar stays out: `rustls-tls`, `http2`, `charset`,
`system-proxy`, plus the three below. The plugin's cookie jar is gone,
and startup deletes the `.cookies` file an older build left in the app
cache dir (`lib.rs`).
- **`gzip`, `brotli`, `zstd`**: requests send `Accept-Encoding: gzip,
br, zstd` and responses are decoded in Rust before the WebView sees
them.
- **`unsafe-headers`**: needed to drop `Origin`. It lifts the plugin's
whole forbidden-header filter (`Host`, `Cookie`, `Content-Length`,
`Sec-*`, …), so `httpFetch` is where to filter if untrusted input ever
feeds request headers. Every caller today is app code.
`docs/architecture/overview.md` records this.
- **`rustls-tls-native-roots`** on Windows and Linux only: the OS trust
store is added to the bundled roots. Android and iOS have no store
`rustls-native-certs` can read, and on macOS every request would walk
the keychain, so those keep the bundled roots. The target-specific entry
also sets `default-features = false`, or Cargo would merge the defaults
(cookies included) back in.

### Vault portraits

- Importing a card with a `sourceUrl` downloads the portrait with
`fetchPublicImageBase64` (public URLs and redirects only, 15s timeout)
instead of a bare WebView `fetch`. The download runs alongside
`sanitize`. The result is accepted only if `sniffImageMime` recognises
the bytes; otherwise the import keeps the URL as the portrait, as
before.
- **`sniffImageMime`** (`utils/image.ts`) identifies PNG, APNG, JPEG,
GIF, WebP, AVIF, HEIC and HEIF from the leading bytes. Unlike
`base64ImageMime`, it returns `null` rather than defaulting to PNG.
- `fetchPublicImageBase64` accepts `timeoutMs`.

### iOS

- `NSLocalNetworkUsageDescription` now also names local AI servers,
since the app connects to them over the LAN.

## Changes outside the main purpose

- `@tauri-apps/plugin-http` 2.8.2 → 2.8.3, with the matching
`tauri-plugin-http` crate. The lockfile update also moved
`tauri-plugin-fs` 2.7.0 → 2.7.1.
- In `fetch.ts` and `fetchAdapter.ts`, the debug-log bodies moved into
`logErrorResponse`, `logStreamResponse` and `logImageResponse`, and
`normalizeHeaders` / the FormData key listing were simplified. Behaviour
is unchanged.

## Testing

New unit tests cover `idleTimeout`, `httpFetch`, the `fetch.ts` and
`fetchAdapter.ts` guard integration, and `sniffImageMime`. The Rust and
platform changes (Cargo features, native roots, `Origin`, decompression,
the iOS string) have no automated coverage and need a release build to
check:

- Run the fake provider with the `stream-stall` model and confirm the
generation errors after the timeout instead of hanging.
- On Windows and Android, point the app at stock Ollama on loopback and
confirm requests succeed without `OLLAMA_ORIGINS`.
- On Windows or Linux, reach an HTTPS server whose CA is only in the OS
store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019V1AUE2StsGUjVV6KzUx2G


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added safeguards for stalled AI requests, with clearer timeout errors
and support for cancelling requests.
* Character imports can now fetch and validate portrait images, with a
fallback to the original image URL.
* Improved image format detection for PNG, JPEG, GIF, WebP, AVIF, HEIC,
and HEIF files.
* Updated the local network permission message to include connections to
local AI servers.

* **Bug Fixes**
* Improved handling of request headers and response timeouts across AI,
discovery, and update requests.
  * App startup now attempts to clear cached cookies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 13:27:54 -04:00
.cargo Add Incognito Keyboard option for Android (#467) 2026-08-26 20:45:30 -04:00
.github Keep a generation running in the background on iOS (#626) 2026-10-18 15:01:14 +05:30
docs Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
scripts Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
src Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
src-tauri Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
static switched font for logo 2026-01-19 14:48:22 -06:00
third-party-licenses Added harper 2026-01-02 05:08:39 -07:00
.gitattributes Fix CRLF Migrations (#162) 2026-02-22 15:46:59 -05:00
.gitignore Cache Gradle again and share one npm cache entry on Linux (#613) 2026-10-12 00:49:33 -04:00
.nvmrc Overhaul CI/CD: Node 24, shared release workflows, faster cached builds (#518) 2026-09-16 22:38:32 -04:00
.prettierrc fix: prettier, eslint, pipeline (#71) 2026-02-06 00:49:36 +01:00
CLAUDE.md WIP, Part 1 of 2 - State Tracking: record every world-state write, and create checkpoints at past entries (#618) 2026-10-16 21:14:03 -04:00
compileApk.sh Android build hygiene: minSdk 26, lint in CI, resource cleanup (#468) 2026-08-29 21:52:01 -04:00
eslint.config.js Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
lefthook.yml Run Clippy on push and in CI, and split lint into lint:ts and lint:rust (#608) 2026-10-11 14:50:50 -04:00
LICENSE.md Db rework (#140) (#141) 2026-02-13 17:37:27 +05:30
logo.png switched font for logo 2026-01-19 14:48:22 -06:00
package-lock.json Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
package.json Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622) 2026-10-18 13:27:54 -04:00
README.md Keep a generation running in the background on iOS (#626) 2026-10-18 15:01:14 +05:30
sun4.jpg Use custom icon for android 2026-01-03 07:08:45 -07:00
sun17.jpg Use custom icon for android 2026-01-03 07:08:45 -07:00
sun2022.png Use custom icon for android 2026-01-03 07:08:45 -07:00
tsconfig.json Type-check vitest.config.ts and note TypeScript 6's strict default (#616) 2026-10-12 13:27:22 -04:00
vite.config.js Upgrade to SvelteKit 3, TypeScript 6 and vitest 5, and bump dependencies (#615) 2026-10-12 12:29:54 -04:00
vitest.config.ts Upgrade to SvelteKit 3, TypeScript 6 and vitest 5, and bump dependencies (#615) 2026-10-12 12:29:54 -04:00

Aventuras

Interactive fiction with an AI that remembers. Play a text adventure or write collaboratively, on desktop and Android, with any LLM provider you like — cloud or running on your own machine.

Aventuras keeps the story straight: chapters are summarized as you play, a lorebook feeds the right entries into the right scene, an agent tidies that lorebook behind you, and the world state — who is present, where you are, what you carry — is tracked turn by turn.

Highlights

  • Two modes — Adventure, with multiple-choice actions and world tracking; Creative Writing, freeform with AI suggestions. First/second/third person, past or present tense.
  • Any provider — OpenRouter, OpenAI, Anthropic, Google, xAI, Groq, DeepSeek, Mistral, Z.AI, NanoGPT, Chutes, Pollinations, NVIDIA NIM, plus local llama.cpp, LM Studio and Ollama. Streaming, extended thinking, and per-subsystem Agent Profiles so retrieval, classification and narration can each use a different model.
  • Memory that scales — automatic chapter summarization, and a retrieval step that goes and finds what the current scene needs from everything that came before.
  • Lorebook — characters, locations, items, factions, concepts and events, with aliases, secrets, keyword and relevance-based injection, SillyTavern import, and an autonomous agent that consolidates duplicates and fills gaps.
  • The Vault — a cross-story library of reusable characters, lorebooks and scenarios, with a tool-calling assistant that edits them and shows you a diff before anything lands.
  • Every prompt is editable — they are Liquid templates, all of them, in an in-app editor.
  • Images — inline <pic> illustrations, scene backgrounds and character portraits, across nine backends including local ComfyUI and A1111.
  • Writing tools — local grammar checking (Harper/WASM), style analysis for repetition, suggestions that match how you write.
  • Yours to keep — SQLite on your machine, full backup/restore, .avt story export, LAN sync between devices over QR pairing, and an optional translation layer over the whole app.
  • 26 themes, adjustable text size, dialogue highlighting.

Install

Pre-built binaries are on the Releases page:

Platform Download Minimum
Windows Aventuras_vX.Y.Z_x64-setup.exe Windows 10
macOS Aventuras_vX.Y.Z_x64.dmg / _aarch64.dmg macOS 13.3
Linux Aventuras_vX.Y.Z_amd64.deb / .AppImage, or _x86_64.rpm WebKitGTK 2.40 for the .deb / .rpm; the AppImage carries its own, needs glibc 2.35
Android Aventuras_vX.Y.Z.apk Android 8, with System WebView 111 or later
iOS Aventuras_vX.Y.Z_ios-arm64-unsigned.ipa (sideload; unsigned) iOS 26

The minimums come from each platform's web engine, OS and runtime requirements. They are derived from documentation and have not been tested on devices at those versions; see docs/development/platform-support.md.

No API keys in config files — providers are set up in the app, under Settings → API Settings.

Build from source

Requires the Node.js version in .nvmrc, the latest stable Rust, (for Android) the Android SDK, NDK r27d and JDK 21–24, and (for iOS) a Mac with Xcode.

git clone https://github.com/AventurasTeam/Aventuras.git
cd Aventuras
npm install
npx tauri dev
Script Does
npm run dev Vite dev server (frontend only)
npx tauri dev full app, with hot reload
npm run build production build
npm run check type checking (svelte-check)
npm test test suite (Vitest)
npm run lint lint:ts (ESLint) and lint:rust (rustfmt, Clippy); :fix fixes
npm run format Prettier
npm run release version bump, tag and push

Desktop builds are npx tauri build; Android and iOS are documented in docs/development/release.md. iOS produces an unsigned device .ipa (no Apple certificates or signing secrets — see docs/development/ios-build-target.md for the full account of how the target was added and how the unsigned CI pipeline works).

Tech stack

TypeScript (strict) · SvelteKit 3 · Svelte 5 runes · Tauri 2 (Rust) · Tailwind + bits-ui · SQLite (tauri-plugin-sql / sqlx) · Vercel AI SDK · LiquidJS · CodeMirror 6 · Zod · Harper.js (WASM) · Vitest

Contributing

Start with docs/ — the architecture is documented per area, and CLAUDE.md has the conventions. CI runs lint, type-check, tests and a build on every PR against master, develop or dev.

Acknowledgments

Tauri · SvelteKit · OpenRouter · Harper · Lucide

License

AGPL-3.0