# AGENTS.md This file provides guidance to coding agents (Claude Code, Codex, Cursor, Aider, …) working with this repository. Claude Code reads it via `@AGENTS.md` in `CLAUDE.md`. # Architecture This project is split into two parts: - `/src` — OxiCloud Backend server in **Rust** - `/frontend` — OxiCloud Frontend: a **SvelteKit (Svelte 5) + TypeScript** single-page app built with Vite > The original vanilla-JS/CSS frontend still lives in `/static` and is retained > during the migration, but new frontend work goes in `/frontend`. Vite builds > the SvelteKit app to `static-dist/`, which the Rust web layer serves in > release. # Backend part ## Backend Build & Dev Commands ```bash cargo build # Dev build cargo build --release # Optimized release build cargo run # Run server (port 8086) cargo test --workspace # Run all tests (~208) cargo test # Run a single test by name cargo test --features test_utils # Run tests that use mockall mocks cargo clippy -- -D warnings # Lint (zero warnings policy) cargo fmt --all --check # Format check cargo fmt --all # Auto-format RUST_LOG=debug cargo run # Run with debug logging cargo run --bin generate-openapi # Regenerate resources/gen/openapi.json ``` A `justfile` is available for common tasks (`just --list` to see all). Key recipes: `just check` (fmt + clippy), `just test`, `just openapi`. Requires **Rust 1.93+** (edition 2024) and **PostgreSQL 13+** (with `pg_trgm` and `ltree` extensions). Database setup: `docker compose up -d postgres` — schema is applied automatically via sqlx migrations on app startup. Migration files live in `migrations/`. For local dev, set `DATABASE_URL` in `.env` (see `example.env`). ## Backend Pre-commit checks Always run these before committing, in this order: ```bash cargo fmt --all # Auto-format cargo clippy --all-features --all-targets -- -D warnings # Lint (must pass with zero warnings) ``` CI enforces both — commits that fail either check will not merge. ## Backend Pre-push checks When the change touches server code (anything under `src/`, `migrations/`, `Cargo.toml`, or `tests/`), run the full suite locally before pushing — CI is slower and a red CI run after a public push wastes maintainer attention: ```bash just check # cargo fmt --check + cargo clippy -D warnings just test # cargo test --workspace just test-integration # cargo test --tests with integration cfg just api-test # Hurl API + WebDAV scenarios ``` Run them in that order — `just check` is fastest and catches the most common issues first. Don't push if any step fails; investigate locally. ## Backend Architecture Hexagonal / Clean Architecture with four layers. Dependencies point inward only. ### Layer structure (`src/`) - **`domain/`** — Core business entities (`entities/`) and repository trait definitions (`repositories/`). Pure Rust, no framework dependencies. Entity types: `File`, `Folder`, `User`, `Calendar`, `CalendarEvent`, `Contact`, `Share`, `TrashedItem`, `Session`, `DeviceCode`, `AppPassword`. - **`application/`** — Use cases and orchestration. - `ports/` — Trait definitions (inbound/outbound) for storage, auth, caching, compression, dedup, thumbnails, chunked uploads, CalDAV/CardDAV, etc. This is the hexagonal "ports" layer. - `services/` — Use case implementations (`FileManagementService`, `FolderService`, `ShareService`, `TrashService`, `CalendarService`, `ContactService`, `SearchService`, `BatchOperations`, etc.). - `adapters/` — CalDAV/CardDAV protocol adapters (iCalendar/vCard parsing). - `dtos/` — Data transfer objects for API boundaries. - **`infrastructure/`** — Concrete implementations of ports. - `repositories/pg/` — All PostgreSQL repository implementations (via `sqlx`). Uses `auth` schema for users/sessions, `storage` schema for files/folders/blobs (content-addressable dedup with ltree paths). - `services/` — JWT, password hashing (Argon2), OIDC, compression, thumbnails, chunked uploads, WOPI discovery, WebDAV locking, file content caching (moka). - `adapters/` — CalDAV/CardDAV storage adapters bridging domain traits to PG. - `db.rs` — Dual connection pool setup (user pool + maintenance pool). - **`interfaces/`** — HTTP layer (Axum). - `api/handlers/` — REST API handlers for files, folders, auth, admin, search, shares, WebDAV, CalDAV, CardDAV, WOPI, chunked uploads, batch operations. - `api/routes.rs` — Route registration, splits protected vs public routes. - `nextcloud/` — NextCloud-compatible API (WebDAV, OCS, login flow v2, trashbin) with Basic Auth middleware. - `middleware/` — Auth (JWT validation), CSRF, rate limiting. - `web/` — Static file serving. - **`common/`** — Cross-cutting concerns. - `di.rs` — `AppServiceFactory` builds all services and produces `AppState` (the central DI container passed to Axum). This is the composition root. - `config.rs` — `AppConfig::from_env()` loads all `OXICLOUD_*` env vars. ### Key patterns - **DI via `AppState`**: All services are `Arc`-wrapped and assembled in `common/di.rs`. `AppState` is wrapped in `Arc` and passed as Axum state. Many services are `Option>` because they depend on features being enabled (auth, WOPI, trash, etc.). - **Content-addressable storage**: Files use BLAKE3 blob dedup. `storage.file_blobs` stores content; `storage.file_metadata` references blobs with ref-counting. See `file_blob_write_repository.rs` and `file_blob_read_repository.rs`. - **ltree paths**: Folder hierarchy uses PostgreSQL `ltree` for efficient subtree queries (recursive copies, moves, searches). - **Dual DB pools**: `DbPools` in `infrastructure/db.rs` separates user-facing queries from maintenance/background tasks to prevent starvation. - **Feature flags**: Major features (auth, trash, search, sharing, quotas) are toggled via `OXICLOUD_ENABLE_*` env vars in `FeaturesConfig`. - **UUID columns**: All ID columns use native PostgreSQL `UUID` type. SQL queries must use `::uuid` casts when passing string parameters to UUID columns. ### Database schemas - `auth` schema: `users`, `sessions`, `app_passwords`, `device_codes`, `admin_settings` - `storage` schema: `folders`, `file_metadata`, `file_blobs`, `trash`, `shares`, `favorites`, `recent_items`, `nextcloud_object_ids` - `caldav` schema: `calendars`, `calendar_events` - `carddav` schema: `address_books`, `contacts`, `contact_groups`, `contact_group_members` Schema definition: `migrations/` (sqlx migrations, applied on startup) ### Protocol support The server exposes multiple protocol interfaces simultaneously: - REST API under `/api/` - WebDAV at `/webdav/` (RFC 4918) - CalDAV at `/caldav/` - CardDAV at `/carddav/` - NextCloud-compatible API at `/remote.php/`, `/ocs/`, `/status.php` - WOPI at `/wopi/` (when enabled) - Well-known discovery at `/.well-known/caldav` and `/.well-known/carddav` ### Test organization Tests are primarily `#[cfg(test)]` modules within source files (~36 files have inline tests). Dedicated test files exist at `*_test.rs` alongside their source. The `test_utils` feature flag enables `mockall` mock generation for trait-heavy testing. No separate `tests/` directory. ### Code duplication Never duplicate logic across handlers or services. If the same behaviour is needed in more than one place, extract it into a shared function, method, or service before writing the second callsite. Preferred homes by layer: - Cross-handler request logic → method on `CoreServices` or `AppState` (`common/di.rs`) - Reusable infrastructure behaviour → method on the relevant service struct - Shared port behaviour → default method on the trait ### Authorization (AuthZ) **AuthZ is enforced exclusively in the application service layer, never in handlers.** All permission checks go through `AuthorizationEngine` (port: `application/ports/authorization_ports.rs`) via service methods named with the `_with_perms` suffix. HTTP handlers (REST, WebDAV, NextCloud, CalDAV, CardDAV) authenticate the caller and pass `caller_id` into the service — they MUST NOT perform their own ownership/permission checks. The authentication middleware extracts the caller; the service decides if the action is allowed. This rule prevents drift between layers and ensures every code path goes through the same policy. New service methods that touch a user-scoped resource must take `caller_id: Uuid` and call `authz.require(...)` before any read or mutation. ### Audit logging for denials and rejections **Every permission denial or auth rejection MUST emit a structured audit log line before returning the error.** Without one, security-relevant outcomes are invisible to operators and incident response loses its primary signal. The convention: ```rust tracing::info!( target: "audit", event = ".", // e.g. "authz.denied", "auth.login_rejected", // "magic_link.redemption_rejected", // "user_profile.rejected" reason = "", // stable machine-readable key for filtering // (e.g. "bad_password", "expired", "no_visibility_path") // …structured fields naming the actors / targets… caller_id = %caller_id, // or subject_id, user_id, granted_by, etc. target_id = %target_id, // or resource_id, subject_id, etc. "👮🏻‍♂️ human-readable message: …", // helpful for live tailing, do not parse ); ``` Rules: - **`target: "audit"`** routes the line to the audit channel (separable from operational `oxicloud::*` debug noise). - **`event`** uses the dotted form `.` and stays stable — log aggregators key off it. - **`reason`** is a machine-readable enum-style key. Don't reword across releases. New denial cause → new `reason` value, never repurpose an existing one. - **Structured fields** carry every actor/target involved (`caller_id`, `target_id`, `resource_id`, `subject_id`, role, is_external flag, etc.). Request id and client IP come from the request-scope span automatically — don't duplicate them. - **Anti-enumeration is preserved.** Returning `NotFound` to the caller while logging the real reason internally is the canonical pattern (e.g. `user_profile.rejected` with `reason = "external_caller_no_relationship"` returns 404, never 403). Operators see the truth; the attacker sees the same response shape regardless of whether the user exists. - **Success paths stay quiet** by default — every authorized request would otherwise flood the log. Use `tracing::debug!` with `target: "oxicloud::authz"` (or similar) when a low-volume granted-trace helps debugging. Reserve `tracing::info!(target: "audit", …)` for outcomes worth surfacing in security reviews. Canonical examples to mirror: `authz.denied` in `application/ports/authorization_ports.rs::require`, `auth.login_rejected` and `magic_link.redemption_rejected` and `user_profile.rejected` in `application/services/auth_application_service.rs`. # Frontend part The frontend is a **SvelteKit** single-page app (Svelte 5 + TypeScript, Vite, `adapter-static`) under `frontend/`. Vite builds it to `static-dist/`, which the Rust web layer serves in release (unmatched client routes fall back to the SPA shell); `PROFILE=dev` serves the unbuilt source. The legacy vanilla frontend in `static/` is retained for now but is **not** where new work goes. ## Frontend Build & Dev Commands Run from `frontend/` (or via the `fe-*` justfile recipes from the repo root): ```bash npm ci # install deps (just fe-install) npm run dev # Vite dev server + HMR (just fe-dev) — backend must run on :8086 npm run build # build the SPA → static-dist/ (just fe-build) npm run check # svelte-check + ESLint + Stylelint + Prettier (just fe-check) npm run test:unit # Vitest (just fe-test) npm run format # prettier --write . ``` `just dev` runs the backend and the Vite dev server together. CI uses **Node 26**; Node 24+ works locally. ## Frontend Architecture (`frontend/src/`) - `routes/` — SvelteKit pages (`+page.svelte`, `+layout.svelte`), one folder per route (`files/[...path]`, `photos`, `shared`, `trash`, `admin`, `s/[token]`, …). - `lib/components/` — reusable Svelte components (`AppShell`, `PhotoLightbox`, `ShareDialog`, `Modal`, …). - `lib/api/` — HTTP layer: `client.ts` (`apiFetch`/`apiJson`), `csrf.ts` (`getCsrfHeaders`), `types.ts` (API DTO types — map the backend here), and `endpoints/*.ts` (one module per area: files, folders, photos, people, grants, …). - `lib/stores/` — global reactive state as `*.svelte.ts` rune stores (`session`, `ui`, `theme`, `dialogs`). - `lib/composables/` — reusable rune logic (`useSelection`, `useOwnerCache`). - `lib/i18n/` — bespoke reactive i18n; `t(key, [params], fallback)` reads `frontend/static/locales/*.json` (16 locales) with `{{param}}` interpolation and an English fallback. - `lib/icons/` — `Icon.svelte` + a generated Font Awesome `registry.ts`. - `lib/utils/`, `lib/vendor/` — shared helpers and minimal typings/loaders for vendored libs. - `lib/styles/` — global CSS (`app.css`, `base/`, `ported/`). - `static/` — served at the web root: `locales/`, `vendors/` (maplibre-gl, pmtiles, hash-wasm), `workers/` (deltaWorker), optional `basemaps/`. ## Code conventions ### Svelte / TypeScript - **Svelte 5 runes** — `$state`, `$derived`, `$props`, `$effect`, `$bindable`. No legacy `export let` for new components. - **TypeScript everywhere** (`lang="ts"` in components). **No `any`** — `typescript-eslint` recommended is enforced; prefer precise types, `unknown` + narrowing, or a minimal declared interface for an untyped global (see `lib/vendor/maplibre.ts`). - ES Modules; `camelCase` for variables/functions, `PascalCase` for components/classes; `const`/`let`, never `var`. - API DTO shapes live in `lib/api/types.ts`; call the backend through `lib/api/endpoints/*` — don't bare-`fetch` `/api` from components. ### Code duplication Never duplicate logic across modules/components. Extract shared behaviour: - DOM/UI helpers → `lib/utils/` - API wrappers → the relevant `lib/api/endpoints/*` module - Cross-component state/logic → a `lib/stores/*.svelte.ts` store or a `lib/composables/*` - Shared markup → a component (e.g. `PhotoLightbox` is shared by the photos grid, People and Places) ### CSS - BEM methodology for class names (`.block__element--modifier`). - Component styles live in the component's scoped `