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
//! The Streamable HTTP server adapter: everything an axum server is told,
//! shown, or asked before a single byte of protocol is dispatched.
//!
//! # What is here and what is not
//!
//! This module is the *configuration and refusal* half of T14. It owns three
//! things, and each is a value rather than a function because each of them has
//! to be the same value in three places — the router, the session store, and
//! the SSE responder — and three separately assembled answers is three ways for
//! them to disagree:
//!
//! - [`ServerConfig`]: the endpoint path, the body ceiling, the replay window,
//! the streaming policy, the timeouts, the security headers, and the scopes
//! a caller must hold.
//! - [`SecurityHeaders`]: the three response headers the project's security
//! rules require, with strict defaults.
//! - [`Refusal`]: the status code and JSON-RPC error body for every request the
//! adapter declines to answer, in one exhaustive `match`.
//!
//! The [`Authenticator`] port and the [`AuthPolicy`] that consumes it sit
//! beside them: the port is declared by the consumer (which is why it lives
//! here, in the adapter that will call it, rather than in a `ports` module that
//! does not exist in this layout), and T16 supplies the OAuth implementation.
//!
//! **What is deliberately absent is the router itself.** There is no `Router`,
//! no route table, no session store binding and no SSE responder in this module,
//! and that is the next dispatch rather than an oversight: a router assembled
//! before the session store has a shape to bind to would be a router that gets
//! rewritten. What lands here instead is the vocabulary that router will need
//! — [`Endpoint`] to mount on, [`ServerConfig`] to read, [`Refusal`] to answer
//! with, and [`AuthPolicy`] to decide with — so that when it arrives it is an
//! assembly job rather than a design job.
//!
//! # The fail-secure shape
//!
//! Two independent things make an unconfigured deployment fail-secure rather
//! than fail-open, and they are worth separating because they fail differently:
//!
//! - **Structure.** Every stateful path resolves an [`AuthContext`](crate::domain::registry::auth::AuthContext) before it
//! looks at a session, a body, or a stream. There is no route that skips it,
//! because a session is reachable only through the context it was handed.
//! - **Policy.** [`AuthContext::anonymous`](crate::domain::registry::auth::AuthContext::anonymous) holds no scopes, and
//! [`AuthPolicy::refuse`] compares the caller's scopes against
//! [`ServerConfig::required_scopes`] *before* anything else happens. Name a
//! scope and every anonymous caller is refused `401`; a caller with a
//! principal that lacks a named scope is refused `403`.
//!
//! The default configuration names no scopes, and that default is a decision
//! with a stated cost rather than an absence: it serves a read-only endpoint to
//! anyone who can reach it. It is the right default for a loopback transport
//! and a conformance runner, and the wrong one for a routable deployment — which
//! is precisely why [`AuthPolicy::Require`] and
//! [`ServerConfig::with_required_scopes`] exist and why an empty
//! [`AuthPolicy::Public`] is a thing a deployment writes on purpose.
//!
//! # Example
//!
//! ```
//! use protocol_droid::adapters::primary::transport::http::{ServerConfig, SecurityHeaders};
//!
//! // The specification's own endpoint, the domain's frame ceiling, and the
//! // strict security headers — the defaults a public deployment runs.
//! let config = ServerConfig::new()?;
//! assert_eq!(config.endpoint().as_str(), "/mcp");
//! assert!(config.security().content_security_policy().contains("default-src 'none'"));
//!
//! // A deployment that decides this endpoint needs a credential says so.
//! use protocol_droid::domain::registry::{Scope, ScopeSet};
//! let locked = config.with_required_scopes(ScopeSet::empty().with(Scope::Read));
//! assert!(locked.required_scopes().contains(Scope::Read));
//!
//! // And one that also relaxes framing, for a deployment that has measured why.
//! let relaxed = ServerConfig::new()?
//! .with_security(SecurityHeaders::default().with_x_frame_options("SAMEORIGIN"));
//! assert_eq!(relaxed.security().x_frame_options(), "SAMEORIGIN");
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
pub
/// The media type a JSON answer carries, and the only one besides a stream
/// that this transport speaks.
///
/// One name, shared by the two places that write it — the refusal path and the
/// router's stamping of a handler's answer — so a client cannot be told one
/// thing about how this header is spelled and another about how it is used.
/// `text/event-stream` is the other shape, and the
/// two are what `HttpReply` tells apart on the way back.
pub const JSON_MEDIA_TYPE: &str = "application/json";
pub use ;
pub use ;
pub use ;
pub use ;
pub use FramePayload;
pub use Refusal;
pub use ;
pub use ;
// Re-exported for the `2024-11-05` transport rather than duplicated there: a
// second implementation of "authenticate, then refuse" is a second place to get
// the ordering wrong, and the ordering is a security property, not a preference.
pub use ;
pub use ;
pub use ;
pub use ;
/// The default replay window is the domain's, and the security defaults are
/// [`SecurityHeaders::default`]. This module re-exports nothing else, so a
/// change to a re-export list is the only way a caller reaches a private item.