stackure 1.20261028.91

Stackure for Rust: passwordless auth for B2B apps as tower middleware for axum, tonic and hyper.
Documentation
//! Stackure is the Rust SDK for the Stackure authentication API.
//!
//! Stackure provides passwordless B2B authentication. This SDK wraps the
//! public API behind 10 free functions and a tower middleware.
//!
//! # Quickstart
//!
//! Protect an axum app:
//!
//! ```no_run
//! # use axum::{Router, routing::get};
//! # let app: Router = Router::new().route("/admin", get(|| async {}));
//! let app = app.layer(stackure::auth());
//! ```
//!
//! The layer works in any tower stack: axum, tonic or hyper.
//!
//! Access the authenticated user inside a handler:
//!
//! ```no_run
//! # fn example(parts: &http::request::Parts) {
//! let user = stackure::user_from_request(parts);
//! # }
//! ```
//!
//! Manual verification without middleware:
//!
//! ```no_run
//! # async fn example(parts: &http::request::Parts) {
//! let result = stackure::verify(parts).await;
//! if result.authenticated {
//!     // use result.user
//! }
//! # }
//! ```
//!
//! Send a magic-link email:
//!
//! ```no_run
//! # async fn example() {
//! let response = stackure::send_magic_link("user@example.com").await;
//! # }
//! ```
//!
//! Sign the user out, ending their access everywhere. Mount it for every method
//! on the sign-out path. Trigger it with a form or button that POSTs from the
//! app's own page; a link or any other request is sent to Stackure's sign-out
//! page, where the user confirms. Awaiting it yields the redirect response,
//! never an error:
//!
//! ```no_run
//! # use axum::{Router, routing::any};
//! async fn logout(parts: http::request::Parts) -> axum::response::Response {
//!     stackure::logout(&parts).await
//! }
//!
//! let app: Router = Router::new().route("/logout", any(logout));
//! ```
//!
//! # Sign-in handoff
//!
//! Stackure's session cookie is scoped to the Stackure host and is never
//! visible to your app. After a successful magic-link sign-in, Stackure hands
//! the browser back to the app's registered URL with a `session_token` POST
//! form field: an app-scoped session token valid only for this app, accepted
//! by the validate endpoint for your app ID and never by Stackure itself.
//!
//! The [`auth`] layer consumes it automatically: it validates the token, stores
//! it in a cookie on your own domain and redirects to the same URL as a GET.
//! Invalid tokens are ignored, and handoff bodies over 4 KB are ignored (treated
//! as no token).
//!
//! # Session binding
//!
//! Sessions are not bound to the browser's user agent or IP address. The SDK
//! still forwards the original `User-Agent` and `X-Forwarded-For` on every
//! validation call, but they are informational only; validation does not
//! depend on them.
//!
//! Every request with a session token is validated against Stackure, so revoking
//! a session takes effect immediately. Requests without a well-formed token get
//! the sign-in URL without a Stackure call, except under `auth()` on localhost,
//! where the sign-in URL comes from Stackure.
//!
//! # Content negotiation
//!
//! The [`auth`] layer inspects the `Accept` header. Browser requests (`Accept:
//! text/html`) redirect to the sign-in URL on 401. API requests (`Accept:
//! application/json`) receive a JSON error body.
//!
//! # MCP
//!
//! Protect an MCP route:
//!
//! ```no_run
//! # use axum::{Router, routing::any};
//! # let app: Router = Router::new().route("/mcp", any(|| async {}));
//! let app = app.layer(stackure::mcp());
//! ```
//!
//! AI clients such as Claude, Claude Code, VS Code and Cursor sign users in
//! through Stackure. This one line checks every MCP request in real time with
//! the same app secret; there is no extra setup. The MCP endpoint must be
//! served from the same site as the app's registered URL unless an MCP URL is
//! set for the app in Stackure.
//!
//! The [`mcp`] layer reads only `Authorization: Bearer` and ignores cookies.
//! It attaches the user exactly as [`auth`] does, and otherwise answers in
//! JSON, never with a redirect or a cookie: 401 with a `WWW-Authenticate`
//! header when not signed in, 503 when the check itself fails.
//!
//! # Identity facts
//!
//! Every authenticated [`User`], from [`auth`] or [`mcp`], also carries 2
//! facts from the user's Stackure organization: `user_is_app_admin` is true
//! when the user is an app admin or owner there, in charge of its apps, and
//! `user_teams` lists the Stackure teams they belong to (empty when none).
//! Stackure defines no in-app permissions; the app decides what these mean.
//!
//! List the users in the caller's organization who can open the app, and the
//! teams that have it, for pickers and sharing. Each call returns one page,
//! sorted by name:
//!
//! ```no_run
//! # async fn example(parts: &http::request::Parts) -> Result<(), stackure::StackureError> {
//! let opts = stackure::DirectoryOptions {
//!     query: Some("ada".into()),
//!     ..Default::default()
//! };
//! let page = stackure::directory_users(parts, &opts).await?;
//! // page.items, page.next_cursor, page.total
//! let teams = stackure::directory_teams(parts, &Default::default()).await?;
//! # Ok(())
//! # }
//! ```
//!
//! `query` matches a user's name, email and job title, or a team's name.
//! `limit` is the page size: 100 by default, 200 at most. Pass `next_cursor`
//! as `cursor` to get the next page; it's `None` on the last one. `total` is
//! set on the first page when there are 10,000 matches or fewer. `ids` returns
//! only those IDs that can open the app, up to 200, to check or name people
//! and teams you already store.
//!
//! [`directory_users`] and [`directory_teams`] are authenticated by the
//! request's session cookie, so call them from a route behind [`auth`]; MCP
//! bearer tokens are not accepted. A request without a valid session gets
//! [`StackureError::Auth`].
//!
//! # Configuration
//!
//! `STACKURE_APP_ID` must be set to the app's UUID, shown on the app page in
//! Stackure, and `STACKURE_APP_SECRET` to the app secret shown when the app
//! was registered (or last rotated) in Stackure. The secret is sent as the
//! `X-App-Secret` header on every call except the sign-out made by
//! [`logout`], which carries the user's session token instead. The first call
//! that needs either fails with [`StackureError::Validation`] when it is
//! unset, or when `STACKURE_APP_ID` is not a UUID. `STACKURE_BASE_URL`
//! overrides the API host (default `https://stackure.com`).
//!
//! People can open a newly registered app once an app admin gives it to
//! everyone in the organization or to one of their teams in Stackure. App
//! admins and the owner can always open it, so they can test sign-in first.
//!
//! Every SDK call has one 2-second wall-clock deadline covering connect,
//! headers, body and the single retry. The SDK retries once, after 500 ms, on
//! a 5xx response or a connection failure, and only if more than 500 ms of the
//! deadline remain. A timeout, including while reading the body, surfaces as
//! [`StackureError::Timeout`] and is never retried. The localhost sign-in code
//! swap is never retried, because each code works only once.
//!
//! # Errors
//!
//! Every function except [`verify`] and [`logout`] returns [`StackureError`].
//! [`logout`] never returns an error: a failed sign-out call still ends in a
//! redirect. Match on the variant, or call [`StackureError::code`] for the same
//! lowercase category string the other Stackure SDKs expose as `.code`.

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod client;

pub mod errors;
pub mod middleware;
pub mod types;
pub mod validation;

pub use client::{
    SESSION_COOKIE, TOKEN_PARAM, base_url, directory_teams, directory_users, send_magic_link,
    validate_session,
};
pub use errors::StackureError;
pub use middleware::{Auth, AuthLayer, auth, logout, mcp, user_from_request, verify};
pub use types::{
    DirectoryOptions, DirectoryPage, DirectoryUser, MagicLinkResponse, Session, Team, User,
    VerifyError, VerifyResult,
};