Skip to main content

gproxy_protocol/capability/
mod.rs

1//! Runtime-neutral capability contracts.
2//!
3//! Capabilities are deliberately smaller than a client implementation. They do
4//! not know about conversion, routing, credentials, persistence engines, or a
5//! common content representation. A host supplies the opaque target/scope
6//! types and enforces the limits returned by each capability instance.
7
8use std::{
9    error::Error,
10    fmt,
11    future::Future,
12    pin::Pin,
13    time::{Duration, SystemTime},
14};
15
16use bytes::Bytes;
17
18use crate::{WebSocket, WireRequest, WireResponse, connection::HttpBody};
19
20/// A boxed capability operation future.
21///
22/// Native futures are `Send`; wasm futures follow the connection stream
23/// contracts and do not require `Send`.
24#[cfg(not(target_arch = "wasm32"))]
25pub type CapabilityFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
26
27#[cfg(target_arch = "wasm32")]
28pub type CapabilityFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
29
30/// The limits a capability instance is required to enforce.
31///
32/// There is no implicit unlimited configuration. Hosts must return the limits
33/// bound to this particular instance and continue enforcing stream idle and
34/// byte limits after an operation has returned a streaming body.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct CapabilityLimits {
37    /// Total wall-clock time allowed for one operation, including body transfer.
38    pub operation_total: Duration,
39    /// Maximum idle interval between stream progress events.
40    pub stream_idle: Duration,
41    /// Maximum bytes accepted by a read or an upstream response body.
42    pub read_bytes: u64,
43    /// Maximum bytes accepted by a write or an upstream request body.
44    pub write_bytes: u64,
45    /// Maximum payload bytes in one WebSocket frame.
46    pub ws_frame_bytes: u64,
47}
48
49/// A stable category for a capability failure.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
52pub enum CapabilityErrorKind {
53    Unsupported,
54    NotFound,
55    Expired,
56    Conflict,
57    Limit,
58    Transport,
59    Storage,
60    Invalid,
61}
62
63/// Whether an error occurred before or during transfer.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
66pub enum CapabilityErrorStage {
67    Start,
68    BodyTransfer,
69    Stream,
70}
71
72/// A structured capability error that retains the originating error when one
73/// exists. HTTP status responses are not represented by this type: an upstream
74/// HTTP non-2xx response is still a successful `send` result and retains its
75/// complete response body.
76#[derive(Debug)]
77pub struct CapabilityError {
78    kind: CapabilityErrorKind,
79    stage: CapabilityErrorStage,
80    message: String,
81    source: Option<Box<dyn Error + Send + Sync + 'static>>,
82}
83
84impl CapabilityError {
85    /// Creates an error without a lower-level source.
86    pub fn new(
87        kind: CapabilityErrorKind,
88        stage: CapabilityErrorStage,
89        message: impl Into<String>,
90    ) -> Self {
91        Self {
92            kind,
93            stage,
94            message: message.into(),
95            source: None,
96        }
97    }
98
99    /// Creates an error while retaining the lower-level source.
100    pub fn with_source(
101        kind: CapabilityErrorKind,
102        stage: CapabilityErrorStage,
103        message: impl Into<String>,
104        source: impl Into<Box<dyn Error + Send + Sync + 'static>>,
105    ) -> Self {
106        Self {
107            kind,
108            stage,
109            message: message.into(),
110            source: Some(source.into()),
111        }
112    }
113
114    pub fn kind(&self) -> CapabilityErrorKind {
115        self.kind
116    }
117
118    pub fn stage(&self) -> CapabilityErrorStage {
119        self.stage
120    }
121
122    pub fn source_error(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
123        self.source.as_deref()
124    }
125}
126
127impl fmt::Display for CapabilityError {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "{} ({:?}/{:?})", self.message, self.kind, self.stage)
130    }
131}
132
133impl Error for CapabilityError {
134    fn source(&self) -> Option<&(dyn Error + 'static)> {
135        self.source
136            .as_deref()
137            .map(|source| source as &(dyn Error + 'static))
138    }
139}
140
141/// The result of a WebSocket-capable upstream connect attempt.
142pub enum UpstreamConnection {
143    /// The handshake succeeded and the socket is ready for frames.
144    Connected {
145        handshake: WireResponse<()>,
146        socket: WebSocket,
147    },
148    /// The upstream rejected the handshake. The complete response body is
149    /// retained so the host can decode or relay the vendor error.
150    Rejected(WireResponse<HttpBody>),
151}
152
153impl fmt::Debug for UpstreamConnection {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self {
156            Self::Connected { handshake, .. } => f
157                .debug_struct("Connected")
158                .field("handshake", handshake)
159                .field("socket", &"WebSocket { .. }")
160                .finish(),
161            Self::Rejected(response) => f.debug_tuple("Rejected").field(response).finish(),
162        }
163    }
164}
165
166/// The upstream transport family.
167pub trait Upstream {
168    /// Opaque host-owned destination identity, origin, and authentication.
169    type Target;
170
171    /// Sends one already-encoded request. A non-2xx HTTP response is returned
172    /// as `Ok(WireResponse)` and is never converted into a capability error.
173    /// Hosts must validate that the request path is origin-relative and
174    /// compatible with `target`, and must replace or remove request auth in
175    /// both headers and query parameters (such as `key` or `access_token`)
176    /// rather than inheriting source credentials. Implementations never route or
177    /// recursively invoke conversion.
178    fn send<'a>(
179        &'a self,
180        target: &'a Self::Target,
181        request: WireRequest<HttpBody>,
182    ) -> CapabilityFuture<'a, Result<WireResponse<HttpBody>, CapabilityError>>;
183
184    /// Performs an HTTP upgrade. A rejected handshake is a response result,
185    /// including its full body, while transport failures are errors. The same
186    /// origin-relative path, target compatibility, and source-auth replacement
187    /// rules as [`Self::send`] apply.
188    fn connect<'a>(
189        &'a self,
190        target: &'a Self::Target,
191        request: WireRequest<()>,
192    ) -> CapabilityFuture<'a, Result<UpstreamConnection, CapabilityError>>;
193
194    /// Limits bound to this capability instance.
195    fn limits(&self) -> CapabilityLimits;
196}
197
198/// File-only metadata exposed by resource access. A filename is retained across
199/// upload and read so adapters can preserve the source file's identity.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct ResourceMetadata {
202    pub mime: Option<String>,
203    pub length: Option<u64>,
204    pub filename: Option<String>,
205    pub expires_at: Option<SystemTime>,
206}
207
208/// A resource locator understood by the host boundary. An `Id` is interpreted
209/// using the associated scope's upstream and identity; a `Url` remains subject
210/// to host authorization. Neither form grants deletion authority.
211#[derive(Debug, Clone, PartialEq, Eq, Hash)]
212#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
213pub enum ResourceReference {
214    Id(String),
215    Url(String),
216}
217
218/// The representation requested for a newly published resource. If the host
219/// cannot provide the requested form, it must reject before consuming the body
220/// or causing any remote side effect.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
223pub enum PublicationKind {
224    Id,
225    Url,
226}
227
228/// A resource body and its metadata.
229pub struct ResourceRead {
230    pub metadata: ResourceMetadata,
231    pub body: HttpBody,
232}
233
234impl fmt::Debug for ResourceRead {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        f.debug_struct("ResourceRead")
237            .field("metadata", &self.metadata)
238            .field("body", &self.body)
239            .finish()
240    }
241}
242
243/// The handle and reference returned by a successful publication.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct PublishedResource<H> {
246    pub handle: H,
247    pub reference: ResourceReference,
248    pub metadata: ResourceMetadata,
249}
250
251/// The durable status of an idempotent publication operation.
252#[derive(Debug, Clone, PartialEq, Eq)]
253#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
254pub enum PublicationStatus<H> {
255    Missing,
256    Pending,
257    Published(PublishedResource<H>),
258    Expired,
259}
260
261/// Scoped resource publication and access.
262pub trait ResourceAccess {
263    type Scope;
264    type PublishedHandle;
265
266    /// Returns file metadata or `NotFound`/`Expired` as appropriate.
267    fn resolve<'a>(
268        &'a self,
269        scope: &'a Self::Scope,
270        reference: &'a ResourceReference,
271    ) -> CapabilityFuture<'a, Result<ResourceMetadata, CapabilityError>>;
272
273    /// Reads a resource as a streamable body with file-only metadata.
274    fn read<'a>(
275        &'a self,
276        scope: &'a Self::Scope,
277        reference: &'a ResourceReference,
278    ) -> CapabilityFuture<'a, Result<ResourceRead, CapabilityError>>;
279
280    /// Publishes one body under a host-assigned reference and handle.
281    ///
282    /// `operation_id` is idempotency-scoped by `scope`. Hosts check and update
283    /// its stored state atomically, including stored expiry, before validating
284    /// new metadata. No preceding status query is required.
285    ///
286    /// A duplicate completed operation with the same `kind` returns the original
287    /// publication and metadata without consuming the new body. A different
288    /// kind or a pending operation returns `Conflict`; a released or expired
289    /// operation returns `Expired`, irrespective of newly supplied metadata.
290    /// An existing publication's metadata is never overwritten by a retry.
291    /// Callers can inspect the durable state with
292    /// [`Self::publication_status`]. If the caller is dropped after remote
293    /// publication but before receiving the handle, it can recover the result
294    /// with [`Self::publication_status`] without replaying the body. The
295    /// metadata expiry for a new operation must be present and in the future;
296    /// otherwise the host returns `Invalid` before consuming the body. If `kind`
297    /// is unsupported, the host returns `Unsupported` before any side effect.
298    /// Rejection and duplicate paths do not consume the supplied body.
299    fn publish<'a>(
300        &'a self,
301        scope: &'a Self::Scope,
302        operation_id: &'a str,
303        kind: PublicationKind,
304        metadata: ResourceMetadata,
305        body: HttpBody,
306    ) -> CapabilityFuture<'a, Result<PublishedResource<Self::PublishedHandle>, CapabilityError>>;
307
308    /// Recovers publication state after cancellation or a lost result.
309    fn publication_status<'a>(
310        &'a self,
311        scope: &'a Self::Scope,
312        operation_id: &'a str,
313    ) -> CapabilityFuture<'a, Result<PublicationStatus<Self::PublishedHandle>, CapabilityError>>;
314
315    /// Releases a published handle after the host verifies signed ownership and
316    /// scope. This is intentionally separate from publication and does not add
317    /// a delete-existing-resource operation for arbitrary references.
318    /// On success the operation becomes `Expired`, retaining its idempotency
319    /// record: status queries and duplicate publishes must not return a stale
320    /// publication or recreate the resource. Expiry has the same terminal effect.
321    fn release<'a>(
322        &'a self,
323        scope: &'a Self::Scope,
324        handle: &'a Self::PublishedHandle,
325    ) -> CapabilityFuture<'a, Result<(), CapabilityError>>;
326
327    fn limits(&self) -> CapabilityLimits;
328}
329
330/// An opaque, host-generated state version. Versions must never be reused,
331/// including after delete and recreate.
332#[derive(Debug, Clone, PartialEq, Eq, Hash)]
333pub struct Version(Vec<u8>);
334
335impl Version {
336    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
337        Self(bytes.into())
338    }
339
340    pub fn as_bytes(&self) -> &[u8] {
341        &self.0
342    }
343}
344
345/// A state entry returned by a store read.
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct StateEntry {
348    pub payload: Bytes,
349    pub version: Version,
350    pub expires_at: Option<SystemTime>,
351}
352
353/// Replacement data for a state CAS. `None` replacement means delete.
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub struct StateWrite {
356    pub payload: Bytes,
357    pub expires_at: Option<SystemTime>,
358}
359
360/// The outcome of a scoped compare-and-exchange.
361#[derive(Debug, Clone, PartialEq, Eq)]
362#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
363pub enum CasResult {
364    /// The write was applied. A replacement has a fresh version; deletion has
365    /// no resulting entry and therefore returns `None`.
366    Applied(Option<Version>),
367    /// The expected version did not match. No current entry is disclosed.
368    Conflict,
369}
370
371/// An atomically scoped state store.
372pub trait StateStore {
373    type Scope;
374
375    /// Expired entries are treated as absent, consistently for reads and CAS.
376    fn get<'a>(
377        &'a self,
378        scope: &'a Self::Scope,
379        key: &'a str,
380    ) -> CapabilityFuture<'a, Result<Option<StateEntry>, CapabilityError>>;
381
382    /// `expected = None` means absent. The operation is atomic within `scope`;
383    /// a conflict never returns the current entry. Hosts generate versions and
384    /// never reuse them across delete/recreate cycles.
385    fn compare_exchange<'a>(
386        &'a self,
387        scope: &'a Self::Scope,
388        key: &'a str,
389        expected: Option<Version>,
390        replacement: Option<StateWrite>,
391    ) -> CapabilityFuture<'a, Result<CasResult, CapabilityError>>;
392
393    fn limits(&self) -> CapabilityLimits;
394}