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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//! 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.
//!
//! # 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.
//!
//! # 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 ;