1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Stackure is the Rust SDK for the Stackure authentication API.
//!
//! Stackure provides passwordless B2B authentication. This SDK wraps the
//! public API behind eight 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(&["can_approve_invoice"]));
//! ```
//!
//! 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;
//! # }
//! ```
//!
//! Log the user out, ending their access everywhere. Mount it for every method
//! on the logout 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.
//!
//! # 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, 403 when a required permission is missing, 503
//! when the check itself fails.
//!
//! # 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`).
//!
//! A newly registered app is not usable by anyone, even its creator, until it
//! is shared with the organization or assigned to a team in Stackure. Do that
//! before testing sign-in.
//!
//! 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.
//!
//! # 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`.
pub use ;
pub use StackureError;
pub use ;
pub use ;