diff --git a/.github/assets/logo.png b/.github/assets/logo.png new file mode 100644 index 00000000..55caf05c Binary files /dev/null and b/.github/assets/logo.png differ diff --git a/README.md b/README.md index 3a766bcc..4a264484 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,98 @@ +
+ +Nvisy Server + # Nvisy Server -[![Build](https://img.shields.io/github/actions/workflow/status/nvisycom/server/build.yml?branch=main&label=build%20%26%20test&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/build.yml) +**Detect and redact sensitive data across documents, images, and audio.** + +The open-source multimodal redaction API: an LLM-powered engine and HTTP service +that finds PII and applies your redaction policies, wrapped in a multi-tenant, +self-hostable Rust server. + +[![Build](https://img.shields.io/github/actions/workflow/status/nvisycom/server/build.yml?branch=main&label=build&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/build.yml) +[![Release](https://img.shields.io/github/actions/workflow/status/nvisycom/server/release.yml?branch=main&label=release&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/release.yml) +[![Security](https://img.shields.io/github/actions/workflow/status/nvisycom/server/security.yml?branch=main&label=security&style=flat-square)](https://github.com/nvisycom/server/actions/workflows/security.yml) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE.txt) -Open-source multimodal redaction API. Detect and redact PII and sensitive data -across documents, images, and audio. +[**nvisy.com**](https://nvisy.com) · [**docs.nvisy.com**](https://docs.nvisy.com) · [**app.nvisy.com**](https://app.nvisy.com) + +
+ +A document flows through two phases: **detection** analyzes it for sensitive +entities and stores a reviewable report; **redaction** applies the pipeline's +policies (with optional reviewer edits) to produce a redacted file. Detection +runs asynchronously off a transactional work queue; redaction is synchronous and +repeatable. Everything is scoped to isolated workspaces with per-workspace +credential encryption. > [!WARNING] -> **Active development: API not stable.** This project is under active -> development. Public APIs, configuration shapes, on-disk formats, and -> wire protocols may change without notice between releases. Pin a -> specific commit if you depend on this in production. +> **Active development. API not stable.** Public APIs, configuration shapes, +> on-disk formats, and wire protocols may change without notice between releases. +> Pin a specific commit if you depend on this in production. ## Features -- **Multimodal Redaction:** Detect and remove sensitive data across PDFs, images, and audio -- **AI-Powered Detection:** LLM-driven PII and entity recognition with configurable redaction policies -- **Workspace Isolation:** Multi-tenant workspaces with HKDF-derived credential encryption -- **Real-Time Collaboration:** WebSocket and NATS pub/sub for live document editing -- **Interactive Docs:** Auto-generated OpenAPI with Scalar UI +- **Multimodal redaction** — detect and remove sensitive data across PDFs, office documents, images, and audio. +- **AI-powered detection** — LLM- and pattern-driven PII/entity recognition, governed by configurable redaction policies. +- **Reviewer edits** — suppress a false positive, retag a detection, or add one the analysis missed, then re-redact — as many times as needed. +- **Workspace isolation** — multi-tenant workspaces with HKDF-derived, per-workspace credential encryption. +- **Real-time collaboration** — WebSocket and NATS pub/sub for live status and document editing. +- **Interactive docs** — auto-generated OpenAPI served through a Scalar UI. + +## Requirements -## Quick Start +- **Rust + Cargo** — 1.95+, Edition 2024 +- **PostgreSQL** 18+ and **NATS** 2.10+ (JetStream) — the dev compose file provides both -The fastest way to get started is with [Nvisy Cloud](https://nvisy.com). +## Quick start -For self-hosted deployments, refer to [`docker/`](docker/) for compose files and -infrastructure requirements, and [`.env.example`](.env.example) for configuration. +The fastest way to get started is with [Nvisy Cloud](https://nvisy.com). To run a +server locally: + +```bash +make install-all # Install tools and make scripts executable +make generate-all # Generate .env, auth keys, and apply migrations + +docker compose -f docker/docker-compose.dev.yml up -d # Start Postgres + NATS +make run # Run the server +``` + +The API then serves interactive OpenAPI docs (Scalar UI) at the running server's +docs path. For self-hosted deployments, see [`docker/`](docker/) for compose +files and infrastructure requirements, and [`.env.example`](.env.example) for +configuration. + +## Commands + +| Command | What it does | +| --- | --- | +| `make run` | Run the server (starts Postgres and NATS first) | +| `make ci` | Run all CI checks locally (check, fmt, clippy, test, docs) | +| `make fmt` | Fix code formatting (nightly rustfmt) | +| `make security` | Run security checks (`cargo deny`) | +| `make generate-migrations` | Apply migrations and regenerate `schema.rs` | +| `make reset-docker` | Reset the dev containers (`down -v`, then `up -d`) | ## Documentation -See [`docs/`](docs/) for architecture, intelligence capabilities, provider -design, and security documentation. +See [`docs/`](docs/) for the details: + +- [Architecture](docs/ARCHITECTURE.md) — the crates, the detect/redact pipeline, and how they fit together. +- [Intelligence](docs/INTELLIGENCE.md) — detection capabilities and the redaction engine. +- [Providers](docs/PROVIDERS.md) — inference and object-store provider design. +- [Security](docs/SECURITY.md) — the encryption, authentication, and isolation model. -## Changelog +## Contributing -See [CHANGELOG.md](CHANGELOG.md) for release notes and version history. +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines, and +[CHANGELOG.md](CHANGELOG.md) for release notes. ## License -Apache 2.0 License, see [LICENSE.txt](LICENSE.txt) +Apache 2.0 License, see [LICENSE.txt](LICENSE.txt). ## Support -- **Documentation:** [docs.nvisy.com](https://docs.nvisy.com) -- **Issues:** [GitHub Issues](https://github.com/nvisycom/server/issues) -- **Email:** [support@nvisy.com](mailto:support@nvisy.com) -- **API Status:** [nvisy.openstatus.dev](https://nvisy.openstatus.dev) +- **Documentation**: [docs.nvisy.com](https://docs.nvisy.com) +- **Email**: [support@nvisy.com](mailto:support@nvisy.com) diff --git a/crates/nvisy-server/src/extract/auth/auth_state.rs b/crates/nvisy-server/src/extract/auth/auth_state.rs index d660b892..45a81f9c 100644 --- a/crates/nvisy-server/src/extract/auth/auth_state.rs +++ b/crates/nvisy-server/src/extract/auth/auth_state.rs @@ -397,7 +397,8 @@ where T: Clone + Send + Sync + for<'de> Deserialize<'de> + 'static, { fn operation_input(_ctx: &mut GenContext, operation: &mut Operation) { - // Add security requirement for Bearer token + // The Bearer token is required: the only way to satisfy the operation is + // to present it. operation.security = vec![[("BearerAuth".to_string(), vec![])].into()]; } } diff --git a/crates/nvisy-server/src/extract/auth/mod.rs b/crates/nvisy-server/src/extract/auth/mod.rs index d4590925..a56f9e1f 100644 --- a/crates/nvisy-server/src/extract/auth/mod.rs +++ b/crates/nvisy-server/src/extract/auth/mod.rs @@ -8,6 +8,7 @@ mod auth_provider; mod auth_state; mod jwt_claims; mod jwt_header; +mod optional_auth; mod permission; use uuid::Uuid; @@ -16,6 +17,7 @@ pub use self::auth_provider::AuthProvider; pub use self::auth_state::AuthState; pub use self::jwt_claims::AuthClaims; pub use self::jwt_header::AuthHeader; +pub use self::optional_auth::OptionalAuth; pub use self::permission::{AuthResult, Permission}; impl AuthProvider for AuthClaims { diff --git a/crates/nvisy-server/src/extract/auth/optional_auth.rs b/crates/nvisy-server/src/extract/auth/optional_auth.rs new file mode 100644 index 00000000..0177d4d1 --- /dev/null +++ b/crates/nvisy-server/src/extract/auth/optional_auth.rs @@ -0,0 +1,91 @@ +//! Optional authentication extractor for endpoints that vary by whether a caller +//! authenticated, without marking themselves auth-required in the OpenAPI spec. + +use aide::OperationInput; +use aide::generate::GenContext; +use aide::openapi::{Operation, SecurityRequirement}; +use axum::extract::{FromRef, FromRequestParts, OptionalFromRequestParts}; +use axum::http::request::Parts; +use derive_more::{Deref, DerefMut}; +use nvisy_postgres::PgClient; +use serde::Deserialize; + +use super::AuthState; +use crate::handler::{Error, Result}; +use crate::service::SessionKeys; + +/// Optional [`AuthState`] for an endpoint that runs with or without a token. +/// +/// Extracting a bare `Option` authenticates the same way, but its +/// generated OpenAPI security comes from the blanket `Option` `OperationInput`, +/// which delegates to [`AuthState`] and so wrongly marks the operation +/// auth-required. This wrapper carries the same optional value while declaring the +/// token as *optional* in the spec (an empty requirement alongside the Bearer one, +/// so a public probe is not shown as needing a token). Use it for endpoints that +/// vary their behavior by whether a caller authenticated — e.g. the health check. +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct OptionalAuth(pub Option>); + +impl FromRequestParts for OptionalAuth +where + T: Clone + Send + Sync + for<'de> Deserialize<'de> + 'static, + S: Sync + Send + 'static, + PgClient: FromRef, + SessionKeys: FromRef, +{ + type Rejection = Error<'static>; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // Reuses the optional extraction: a valid token authenticates, an absent or + // invalid one yields `None` rather than rejecting. + as OptionalFromRequestParts>::from_request_parts(parts, state) + .await + .map(OptionalAuth) + } +} + +impl OperationInput for OptionalAuth +where + T: Clone + Send + Sync + for<'de> Deserialize<'de> + 'static, +{ + fn operation_input(_ctx: &mut GenContext, operation: &mut Operation) { + // Two alternatives: an empty requirement (no auth) and the Bearer one, so + // the operation is documented as accessible with or without a token. + operation.security = vec![ + SecurityRequirement::new(), + [("BearerAuth".to_string(), vec![])].into(), + ]; + } +} + +#[cfg(test)] +mod tests { + use aide::OperationInput; + use aide::openapi::Operation; + + use super::OptionalAuth; + + /// The OpenAPI security for an optional-auth operation must offer an + /// unauthenticated alternative (an empty requirement) alongside the Bearer + /// one, so the endpoint is not documented as requiring a token — a public + /// probe hitting the health check must not appear to need credentials. + #[test] + fn documents_auth_as_optional_not_required() { + let mut operation = Operation::default(); + aide::generate::in_context(|ctx| { + OptionalAuth::<()>::operation_input(ctx, &mut operation); + }); + + assert!( + operation.security.iter().any(|req| req.is_empty()), + "an empty requirement must be present so no auth also satisfies the operation", + ); + assert!( + operation + .security + .iter() + .any(|req| req.contains_key("BearerAuth")), + "the Bearer alternative must still be offered for authenticated callers", + ); + } +} diff --git a/crates/nvisy-server/src/extract/mod.rs b/crates/nvisy-server/src/extract/mod.rs index 686600ac..9cb99d89 100644 --- a/crates/nvisy-server/src/extract/mod.rs +++ b/crates/nvisy-server/src/extract/mod.rs @@ -17,7 +17,7 @@ mod version; mod workspace_context; pub use crate::extract::auth::{ - AuthClaims, AuthHeader, AuthProvider, AuthResult, AuthState, Permission, + AuthClaims, AuthHeader, AuthProvider, AuthResult, AuthState, OptionalAuth, Permission, }; pub use crate::extract::avatar::Avatar; pub use crate::extract::connection_info::{AppConnectInfo, ClientIp}; diff --git a/crates/nvisy-server/src/handler/monitors.rs b/crates/nvisy-server/src/handler/monitors.rs index 48544a2b..4350d852 100644 --- a/crates/nvisy-server/src/handler/monitors.rs +++ b/crates/nvisy-server/src/handler/monitors.rs @@ -11,7 +11,7 @@ use axum::http::StatusCode; use nvisy_core::health::HealthStatus; use super::response::Health; -use crate::extract::{AuthState, Json, Version}; +use crate::extract::{Json, OptionalAuth, Version}; use crate::handler::Result; use crate::service::{HealthCache, ServiceState}; @@ -43,7 +43,7 @@ const TRACING_TARGET: &str = "nvisy_server::handler::monitors"; )] async fn health_status( State(health_service): State, - auth_state: Option, + OptionalAuth(auth_state): OptionalAuth, version: Version, ) -> Result<(StatusCode, Json)> { let is_authenticated = auth_state.is_some();