Skip to main content

pubhubs/api/
phc.rs

1//! Additional endpoints provided by PubHubs Central
2use crate::api::*;
3
4use std::collections::{HashMap, HashSet};
5
6use actix_web::http::{self, header};
7use serde::{Deserialize, Serialize};
8
9use crate::attr;
10use crate::handle;
11use crate::id::Id;
12use crate::misc::serde_ext::bytes_wrapper::B64UU;
13use crate::servers::Constellation;
14
15/// `.ph/hub/...` endpoints, used by hubs
16pub mod hub {
17    use super::*;
18
19    /// Used by a hub to request a ticket (see [`TicketContent`]) from PubHubs Central.
20    /// The request must be signed for the `verifying_key` advertised by the hub info endoint
21    /// (see [`crate::api::hub::InfoEP`]).
22    ///
23    /// If the signature cannot be verified, [`ErrorCode::BadRequest`] is returned.
24    pub struct TicketEP {}
25    impl EndpointDetails for TicketEP {
26        type RequestType = Signed<TicketReq>;
27        type ResponseType = Result<TicketResp>;
28
29        const METHOD: http::Method = http::Method::POST;
30        const PATH: &'static str = ".ph/hub/ticket";
31    }
32
33    having_message_code!(TicketReq, PhcHubTicketReq);
34
35    #[derive(Serialize, Deserialize, Debug, Clone)]
36    #[serde(deny_unknown_fields)]
37    pub struct TicketReq {
38        pub handle: crate::handle::Handle,
39    }
40
41    /// What [`TicketEP`] returns
42    #[derive(Serialize, Deserialize, Debug, Clone)]
43    #[serde(deny_unknown_fields)]
44    #[must_use]
45    pub enum TicketResp {
46        Success(Ticket),
47
48        /// No hub known with this handle
49        UnknownHub,
50
51        /// Hub has no verifying key set
52        NoVerifyingKey,
53    }
54
55    pub type Ticket = Signed<TicketContent>;
56
57    /// A ticket, a [`Signed`] [`TicketContent`], certifies that the hub uses the given
58    /// `verifying_key`.
59    #[derive(Serialize, Deserialize, Debug, Clone)]
60    #[serde(deny_unknown_fields)]
61    pub struct TicketContent {
62        pub handle: crate::handle::Handle,
63        pub verifying_key: VerifyingKey,
64    }
65
66    having_message_code!(TicketContent, PhcHubTicket);
67
68    /// A [`Signed`] message together with a [`Ticket`].
69    #[derive(Serialize, Deserialize, Debug, Clone)]
70    #[serde(deny_unknown_fields)]
71    pub struct TicketSigned<T> {
72        pub ticket: Ticket,
73        signed: Signed<T>,
74    }
75
76    impl<T> TicketSigned<T> {
77        /// Opens this [`TicketSigned`], checking the signature on `signed` using the verifying key in
78        /// the provided `ticket`, and checking the `ticket` using `key`.
79        pub fn open(
80            self,
81            key: &ed25519_dalek::VerifyingKey,
82        ) -> std::result::Result<(T, crate::handle::Handle), TicketOpenError>
83        where
84            T: Signable,
85        {
86            let ticket_content: TicketContent = self
87                .ticket
88                .open(key, None)
89                .map_err(TicketOpenError::Ticket)?;
90
91            let msg: T = self
92                .signed
93                .open(&*ticket_content.verifying_key, None)
94                .map_err(TicketOpenError::Signed)?;
95
96            Ok((msg, ticket_content.handle))
97        }
98
99        pub fn new(ticket: Ticket, signed: Signed<T>) -> Self {
100            Self { ticket, signed }
101        }
102    }
103
104    /// Error returned by [`TicketSigned::open`].
105    #[derive(thiserror::Error, Debug)]
106    pub enum TicketOpenError {
107        #[error("ticket is invalid")]
108        Ticket(#[source] OpenError),
109
110        #[error("ticket is valid, but not the signature made using it")]
111        Signed(#[source] OpenError),
112    }
113
114    impl TicketOpenError {
115        /// Standard verdict for this error: respond with `retry_response` (when the hub can
116        /// recover by obtaining a new ticket), or fail the handler with an [`ErrorCode`].
117        ///
118        /// `retry_response` is typically the handler's `RetryWithNewTicket` response variant.
119        pub fn default_verdict<R>(self, retry_response: R) -> Result<R> {
120            match self {
121                // Ticket failed PHC's signature check or has aged past its TTL: requesting a fresh
122                // ticket from PHC fixes both cases.
123                Self::Ticket(OpenError::InvalidSignature) | Self::Ticket(OpenError::Expired) => {
124                    Ok(retry_response)
125                }
126                // Internal crypto failure.  `Ticket(OtherConstellation)` is defensive: the ticket
127                // is not constellation-bound (see [`Signable::CONSTELLATION_BOUND`]), so this
128                // arm would only fire on a code bug.
129                Self::Ticket(OpenError::InternalError)
130                | Self::Ticket(OpenError::OtherConstellation(..))
131                | Self::Signed(OpenError::InternalError) => Err(ErrorCode::InternalError),
132                // The hub just created the inner `Signed` for this request, so a bad/expired
133                // signature, a malformed JWT, or a constellation mismatch all point to a
134                // hub-side bug; surface as `BadRequest` so the hub investigates rather than
135                // loops on a fresh ticket.  (`Signed(OtherConstellation)` is currently
136                // unreachable: [`TicketSigned`] does not support constellation-bound inner
137                // messages.  If support is added, that arm should get its own
138                // `RetryWithNewConstellation` response variant instead.)
139                Self::Ticket(OpenError::OtherwiseInvalid)
140                | Self::Signed(OpenError::OtherwiseInvalid)
141                | Self::Signed(OpenError::InvalidSignature)
142                | Self::Signed(OpenError::Expired)
143                | Self::Signed(OpenError::OtherConstellation(..)) => Err(ErrorCode::BadRequest),
144            }
145        }
146    }
147}
148
149/// `.ph/user/...` endpoints, used by the ('global') web client
150pub mod user {
151    use super::*;
152
153    /// Provides the global client with basic details about the current PubHubs setup.
154    pub struct WelcomeEP {}
155    impl EndpointDetails for WelcomeEP {
156        type RequestType = NoPayload;
157        type ResponseType = Result<WelcomeResp>;
158
159        const METHOD: http::Method = http::Method::GET;
160        const PATH: &'static str = ".ph/user/welcome";
161    }
162
163    /// Returned by [`WelcomeEP`].
164    #[derive(Serialize, Deserialize, Debug, Clone)]
165    #[serde(deny_unknown_fields)]
166    pub struct WelcomeResp {
167        pub constellation: Constellation,
168        pub hubs: HashMap<handle::Handle, crate::hub::BasicInfo>,
169    }
170
171    /// Provides the global client with cached details about the hubs
172    #[derive(Debug)]
173    pub struct CachedHubInfoEP {}
174    impl EndpointDetails for CachedHubInfoEP {
175        type RequestType = NoPayload;
176        type ResponseType = Result<CachedHubInfoResp>;
177
178        const METHOD: http::Method = http::Method::GET;
179        const PATH: &'static str = ".ph/user/cached-hub-info";
180    }
181
182    /// Returned by [`CachedHubInfoEP`].
183    #[derive(Serialize, Deserialize, Debug, Clone)]
184    #[serde(deny_unknown_fields)]
185    pub struct CachedHubInfoResp {
186        pub hubs: HashMap<handle::Handle, Option<crate::api::hub::InfoResp>>,
187    }
188
189    /// Login (and register if needed)
190    pub struct EnterEP {}
191    impl EndpointDetails for EnterEP {
192        type RequestType = EnterReq;
193        type ResponseType = Result<EnterResp>;
194
195        const METHOD: http::Method = http::Method::POST;
196        const PATH: &'static str = ".ph/user/enter";
197    }
198
199    /// Request to log in to an existing account, or register a new one.
200    ///
201    /// Also used to add attributes to the new or existing user account.
202    ///
203    /// May fail with [`ErrorCode::BadRequest`] when:
204    ///  - [`identifying_attr`] is not identifying
205    ///  - The same attribute appears twice among [`add_attrs`] and [`identifying_attr`].
206    ///  - A non-addable attribute is in `add_attrs` (such as a pubhubs card attribute not obtained
207    ///    via the [`auths::CardEP`] endpoint.
208    ///  - Neither an identifying nor a auth token (via the `Authorization` header) is provided.
209    ///  - When the auth token is used, but the mode is not login.
210    ///
211    /// [`identifying_attr`]: Self::identifying_attr
212    /// [`add_attrs`]: Self::add_attrs
213    #[derive(Serialize, Deserialize, Debug, Clone, Default)]
214    #[serde(deny_unknown_fields)]
215    pub struct EnterReq {
216        /// [`Attr`]ibute identifying the user.
217        ///
218        /// If omitted, an `AuthToken` must be passed via the `Authorization` header instead.
219        ///
220        /// [`Attr`]: attr::Attr
221        #[serde(default)]
222        #[serde(skip_serializing_if = "Option::is_none")]
223        pub identifying_attr: Option<Signed<attr::Attr>>,
224
225        /// The mode determines whether we want to create an account if none exists,
226        /// and whether we expect an account to exist.
227        #[serde(default)]
228        pub mode: EnterMode,
229
230        /// Add these attributes to your account, required, for example, when registering a new
231        /// account, or when no bannable attribute is registered for this account.
232        #[serde(default)]
233        #[serde(skip_serializing_if = "Vec::is_empty")]
234        pub add_attrs: Vec<Signed<attr::Attr>>,
235
236        /// When the registration of a new user account is needed for this request, check that none
237        /// of the provided attributes already bans another user.  If one of the supplied
238        /// attributes does ban another user, [`EnterResp::AttributeAlreadyTaken`] is returned.
239        ///
240        /// Checking for this condition is useful when an end-user already has an account, supplied
241        /// one attribute that bans it, but not an identifying attribute tied to their original
242        /// account.  If this check is not performed, a second account is created, which is not
243        /// what the user might want.  With this check, the frontend can prompt the user to confirm
244        /// that they really do want to create a (potential) second account.
245        #[serde(default)]
246        #[serde(skip_serializing_if = "std::ops::Not::not")]
247        pub register_only_with_unique_attrs: bool,
248    }
249
250    /// Returned by [`EnterEP`].
251    #[derive(Serialize, Deserialize, Debug, Clone)]
252    #[serde(deny_unknown_fields)]
253    #[serde(rename = "snake_case")]
254    #[must_use]
255    pub enum EnterResp {
256        /// Happens only in [`EnterMode::Login`]
257        AccountDoesNotExist,
258
259        /// This attribute is banned and therefore cannot be used.
260        AttributeBanned(attr::Attr),
261
262        /// Cannot login, because this account is banned.
263        Banned,
264
265        /// The given identifying attribute (in [`EnterReq::add_attrs`] or [`EnterReq::identifying_attr`])
266        /// is already tied to another account.
267        ///
268        /// If [`EnterReq::register_only_with_unique_attrs`] is set, this variant will also be returned if
269        /// a registration is attempted, but one of the supplied attributes already bans another
270        /// user.
271        ///
272        /// May occasionally happen under the [`EnterMode::LoginOrRegister`] mode if the account
273        /// was created by some parallel invocation of [`EnterEP`] at about the same time.
274        AttributeAlreadyTaken {
275            #[serde(flatten)]
276            attr: attr::Attr,
277
278            /// Set if this attribute was taken in the sense that it already bans another user.
279            #[serde(default)]
280            #[serde(skip_serializing_if = "std::ops::Not::not")]
281            bans_other_user: bool,
282        },
283
284        /// Cannot register an account with these attributes:  no bannable attribute provided.
285        NoBannableAttribute,
286
287        /// Signature on identifying attribute is invalid or expired; please reobtain the
288        /// identifying attribute and retry.  If this fails even with a fresh attribute something
289        /// is wrong with the server.
290        RetryWithNewIdentifyingAttr,
291
292        /// An authtoken was passed via the Authorization header that is expired or otherwise invalid.  
293        /// Obtain a new one and retry.
294        RetryWithNewAuthToken,
295
296        /// Signature on [`EnterReq::add_attrs`]  attribute is invalid or expired; please reobtain the
297        /// attribute and retry.  If this fails even with a fresh attribute something
298        /// is wrong with the server.
299        RetryWithNewAddAttr {
300            /// `add_attrs[index]` is the offending attribute
301            index: usize,
302        },
303
304        /// The given identifying attribute (now) grants access to a pubhubs account.
305        Entered {
306            /// Whether we created a new account
307            new_account: bool,
308
309            /// An access token identifying the user towards pubhubs central.
310            ///
311            /// May not be provided, for example, when the user is banned, or if no bannable
312            /// attribute is currently associated to the user's account.
313            auth_token_package: std::result::Result<AuthTokenPackage, AuthTokenDeniedReason>,
314
315            attr_status: Vec<(attr::Attr, AttrAddStatus)>,
316        },
317    }
318
319    /// Why no auth token was granted
320    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
321    #[serde(deny_unknown_fields)]
322    pub enum AuthTokenDeniedReason {
323        /// No bannable attribute associated to account.
324        ///
325        /// May happen when a bannable attribute was provided in the [`EnterReq`], but adding this
326        /// attribute failed for some reason.  Just try to add the bannable attribute again.
327        NoBannableAttribute,
328
329        /// This account is banned.  Only returned in [`RefreshResp`] (since [`EnterResp`] has
330        /// [`EnterResp::Banned`]).
331        Banned,
332    }
333
334    /// Whether to login, register, or both.
335    #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
336    #[serde(deny_unknown_fields)]
337    pub enum EnterMode {
338        /// Log in to an existing account
339        #[default]
340        Login,
341
342        /// Register a new account
343        Register,
344
345        /// Log in to an existing account, or register one first if needed
346        LoginOrRegister,
347    }
348
349    /// Result of trying to add an attribute via [`EnterEP`].
350    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
351    #[serde(deny_unknown_fields)]
352    #[serde(rename = "snake_case")]
353    pub enum AttrAddStatus {
354        /// Did nothing - the attribute was already there
355        AlreadyThere,
356
357        /// The attribute was added
358        Added,
359
360        /// Adding this attribute (partially) failed.
361        PleaseTryAgain,
362    }
363
364    /// Refresh authentication token.  Requires authentication, but the access token used to
365    /// authenticate may be expired.
366    pub struct RefreshEP {}
367    impl EndpointDetails for RefreshEP {
368        type RequestType = NoPayload;
369        type ResponseType = Result<RefreshResp>;
370
371        const METHOD: http::Method = http::Method::GET;
372        const PATH: &'static str = ".ph/user/refresh";
373    }
374
375    /// Returned by [`RefreshEP`].
376    #[derive(Serialize, Deserialize, Debug, Clone)]
377    #[serde(deny_unknown_fields)]
378    #[serde(rename = "snake_case")]
379    #[must_use]
380    pub enum RefreshResp {
381        /// Something is wrong with the provided auth token.  Please obtain a new one via the
382        /// [`EnterEP`].
383        ReobtainAuthToken,
384
385        /// Cannot issue authentication token for the given [`AuthTokenDeniedReason`]
386        Denied(AuthTokenDeniedReason),
387
388        /// The refreshed authentication token
389        Success(AuthTokenPackage),
390    }
391
392    /// An [`AuthToken`] with some additional information.
393    #[derive(Serialize, Deserialize, Debug, Clone)]
394    #[serde(deny_unknown_fields)]
395    pub struct AuthTokenPackage {
396        /// The actual authentication token
397        pub auth_token: AuthToken,
398
399        /// When [`Self::auth_token`] expires
400        pub expires: NumericDate,
401    }
402
403    /// An opaque token used to identify the user towards pubhubs central via the
404    /// `Authorization` header.  The token can be obtained via the [`EnterEP`],
405    /// and be refreshed via the [`RefreshEP`].
406    #[derive(Serialize, Deserialize, Debug, Clone)]
407    #[serde(transparent)]
408    pub struct AuthToken {
409        pub(crate) inner: B64UU,
410    }
411
412    impl std::fmt::Display for AuthToken {
413        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414            write!(f, "{}", self.inner)
415        }
416    }
417
418    /// So [`AuthToken`] can be used as the value of a [`clap`] flag.
419    impl std::str::FromStr for AuthToken {
420        type Err = <B64UU as std::str::FromStr>::Err;
421
422        fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
423            Ok(Self {
424                inner: B64UU::from_str(s)?,
425            })
426        }
427    }
428
429    impl header::TryIntoHeaderValue for AuthToken {
430        type Error = std::convert::Infallible;
431
432        fn try_into_value(self) -> std::result::Result<header::HeaderValue, Self::Error> {
433            let vec: Vec<u8> = self.inner.to_string().into_bytes();
434
435            Ok(header::HeaderValue::try_from(vec).unwrap())
436        }
437    }
438
439    impl header::Header for AuthToken {
440        fn name() -> header::HeaderName {
441            header::AUTHORIZATION
442        }
443
444        fn parse<M: actix_web::HttpMessage>(
445            msg: &M,
446        ) -> std::result::Result<Self, actix_web::error::ParseError> {
447            Ok(AuthToken {
448                inner: header::from_one_raw_str(msg.headers().get(Self::name()))?,
449            })
450        }
451    }
452
453    /// Get state of the current user
454    pub struct StateEP {}
455    impl EndpointDetails for StateEP {
456        type RequestType = NoPayload;
457        type ResponseType = Result<StateResp>;
458
459        const METHOD: http::Method = http::Method::GET;
460        const PATH: &'static str = ".ph/user/state";
461    }
462
463    /// Result of retrieving a user's state
464    #[derive(Serialize, Deserialize, Debug, Clone)]
465    #[serde(deny_unknown_fields)]
466    #[serde(rename = "snake_case")]
467    #[must_use]
468    pub enum StateResp {
469        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
470        RetryWithNewAuthToken,
471
472        /// Retrieval of [`UserState`] was successful
473        State(UserState),
474    }
475
476    /// State of a user's account at pubhubs as shown to the user.
477    #[derive(Serialize, Deserialize, Debug, Clone)]
478    #[serde(deny_unknown_fields)]
479    pub struct UserState {
480        /// Attributes that may be used to log in as this user.
481        pub allow_login_by: HashSet<Id>,
482
483        /// Attributes that when banned ban this user.
484        pub could_be_banned_by: HashSet<Id>,
485
486        /// Objects stored for this user
487        pub stored_objects: HashMap<handle::Handle, UserObjectDetails>,
488        // TODO: add information on Quota
489    }
490
491    /// Details on an object stored at pubhubs central for a user.
492    #[derive(Serialize, Deserialize, Debug, Clone)]
493    #[serde(deny_unknown_fields)]
494    pub struct UserObjectDetails {
495        /// Identifier for this object - does not change
496        pub hash: Id,
497
498        /// Needs to be provided to the [`GetObjectEP`] when retrieving this object.  May change.
499        pub hmac: Id,
500
501        /// Size of the object in bytes
502        pub size: u32,
503    }
504
505    /// Retrieves a user object with the given `hash` from PubHubs central
506    ///
507    /// Authorization happens not via an access token, but using the [`UserObjectDetails::hmac`].
508    /// This allows HTTP caching without leaking the access token to the cache.
509    pub struct GetObjectEP {}
510    impl EndpointDetails for GetObjectEP {
511        type RequestType = NoPayload;
512
513        /// Generally the API endpoints return `application/json` encoding `Result<ResponseType>`,
514        /// but this endpoint is different.  It returns either an `application/json` encoding an
515        /// `Result<GetObjectResp>` (when there's a problem) or an `application/octet-stream` containing just `bytes::Bytes`.
516        type ResponseType = Payload<Result<GetObjectResp>>;
517
518        const METHOD: http::Method = http::Method::GET;
519        const PATH: &'static str = ".ph/user/obj/by-hash/{hash}/{hmac}";
520
521        /// Responses should be cached indefinitely
522        fn immutable_response() -> bool {
523            true
524        }
525    }
526
527    /// Returned by [`GetObjectEP`] when there's a problem.  When there's no problem an octet
528    /// stream is returned instead.
529    #[derive(Serialize, Deserialize, Debug, Clone)]
530    #[serde(deny_unknown_fields)]
531    #[serde(rename = "snake_case")]
532    #[must_use]
533    pub enum GetObjectResp {
534        /// The `hmac` you sent is invalid, probably because it is outdated.
535        ///
536        /// Please retry after obtaining the current `hmac` from [`StateEP`].
537        RetryWithNewHmac,
538
539        /// The `hmac` was correct, so the object you requested probably did exist at one point,
540        /// but it does not longer.  Please reload the list of stored objects via [`StateEP`].
541        NotFound,
542    }
543
544    /// Stores a new object at pubhubs central, under the given `handle`.
545    pub struct NewObjectEP {}
546    impl EndpointDetails for NewObjectEP {
547        type RequestType = BytesPayload;
548        type ResponseType = Result<StoreObjectResp>;
549
550        const METHOD: http::Method = http::Method::POST;
551        const PATH: &'static str = ".ph/user/obj/by-handle/{handle}";
552    }
553
554    /// Stores an object at pubhubs central under the given `handle`, overwriting the previous
555    /// object stored there.
556    pub struct OverwriteObjectEP {}
557    impl EndpointDetails for OverwriteObjectEP {
558        type RequestType = BytesPayload;
559        type ResponseType = Result<StoreObjectResp>;
560
561        const METHOD: http::Method = http::Method::POST;
562        const PATH: &'static str = ".ph/user/obj/by-hash/{handle}/{overwrite_hash}";
563    }
564
565    /// Returned by [`NewObjectEP`] and [`OverwriteObjectEP`].
566    #[derive(Serialize, Deserialize, Debug, Clone)]
567    #[serde(deny_unknown_fields)]
568    #[serde(rename = "snake_case")]
569    #[must_use]
570    pub enum StoreObjectResp {
571        /// Please retry the same request again.  This may happen when another call changed the
572        /// user's state. The purpose of letting the client make the same call again (instead of
573        /// letting the server retry) is that the client gets feedback about this.
574        PleaseRetry,
575
576        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
577        RetryWithNewAuthToken,
578
579        /// Returned when using [`NewObjectEP`], but there is already an object stored under that handle.  
580        /// To make sure that you're not overriding recent changes made by another global client,
581        /// you must pass the hash of the object you want to overwrite by using the
582        /// [`OverwriteObjectEP`] instead.
583        MissingHash,
584
585        /// Returned when [`OverwriteObjectEP`] is used, but there is no (longer) an object
586        /// stored under that handle.  Use [`NewObjectEP`] to create a new one.
587        NotFound,
588
589        /// Returned when using [`OverwriteObjectEP`] but the object stored at that handle
590        /// has a different hash, presumably because it has been changed in the meantime by another
591        /// global client.
592        HashDidNotMatch,
593
594        /// The object that you sent did not differ from the object already stored.  Doing this
595        /// should be avoided.
596        NoChanges,
597
598        /// Cannot perform this request, because the user has (or would have) reached the named
599        /// quotum.
600        ///
601        /// This should only happen when the user is trying to abuse PubHubs central as object
602        /// store, or when the global client is storing more than it should.
603        QuotumReached(QuotumName),
604
605        /// The object was stored succesfully. The user objects that are currently stored for this user are returned.
606        Stored {
607            stored_objects: HashMap<handle::Handle, UserObjectDetails>,
608        },
609    }
610
611    /// Quota for a user
612    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
613    #[serde(deny_unknown_fields)]
614    pub struct Quota {
615        /// Total number of objects allowed for a user
616        pub object_count: u16,
617
618        /// The sum total of all bytes of all objects of a user cannot exceed this
619        pub object_bytes_total: u32,
620    }
621
622    impl Default for Quota {
623        fn default() -> Self {
624            Self {
625                object_count: 5,
626                object_bytes_total: 1024 * 1024, // 1 mb
627            }
628        }
629    }
630
631    /// The different quota used in [`Quota`].
632    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
633    #[serde(deny_unknown_fields)]
634    #[serde(rename_all = "snake_case")]
635    pub enum QuotumName {
636        ObjectCount,
637        ObjectBytesTotal,
638    }
639
640    impl std::fmt::Display for QuotumName {
641        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642            self.serialize(f)
643        }
644    }
645
646    /// Requests an [`sso::PolymorphicPseudonymPackage`].  Requires authentication.
647    pub struct PppEP {}
648    impl EndpointDetails for PppEP {
649        type RequestType = NoPayload;
650        type ResponseType = Result<PppResp>;
651
652        const METHOD: http::Method = http::Method::POST;
653        const PATH: &'static str = ".ph/user/ppp";
654    }
655
656    /// Returned by [`PppEP`].
657    #[derive(Serialize, Deserialize, Debug, Clone)]
658    #[serde(deny_unknown_fields)]
659    #[serde(rename = "snake_case")]
660    #[must_use]
661    pub enum PppResp {
662        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
663        RetryWithNewAuthToken,
664
665        /// The requested polymorphic pseudonym package (PPP).  Must be used only once lest the
666        /// transcryptor can track the user by the PPP used.
667        Success(Sealed<sso::PolymorphicPseudonymPackage>),
668    }
669
670    /// Type of [`sso::PolymorphicPseudonymPackage::nonce`]
671    #[derive(Serialize, Deserialize, Debug, Clone)]
672    #[serde(transparent)]
673    pub struct PpNonce {
674        pub(crate) inner: B64UU,
675    }
676
677    /// Requests an [`sso::HashedHubPseudonymPackage`].  Requires authentication.
678    pub struct HhppEP {}
679    impl EndpointDetails for HhppEP {
680        type RequestType = HhppReq;
681        type ResponseType = Result<HhppResp>;
682
683        const METHOD: http::Method = http::Method::POST;
684        const PATH: &'static str = ".ph/user/hhpp";
685    }
686
687    /// Request type for [`HhppEP`]
688    #[derive(Serialize, Deserialize, Debug, Clone)]
689    #[serde(deny_unknown_fields)]
690    #[serde(rename = "snake_case")]
691    pub struct HhppReq {
692        /// The encrypted pseudonym to hash. Can be obtained from [`tr::EhppEP`].
693        pub ehpp: Sealed<sso::EncryptedHubPseudonymPackage>,
694    }
695
696    /// Returned by [`HhppEP`].
697    #[derive(Serialize, Deserialize, Debug, Clone)]
698    #[serde(deny_unknown_fields)]
699    #[serde(rename = "snake_case")]
700    #[must_use]
701    pub enum HhppResp {
702        /// There's something wrong with the [`sso::EncryptedHubPseudonymPackage`].
703        /// You probably want to start at [`PppEP`] again.
704        RetryWithNewPpp,
705
706        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
707        RetryWithNewAuthToken,
708
709        /// The requested hashed hub pseudonym package (HHPP).  
710        Success(Signed<sso::HashedHubPseudonymPackage>),
711    }
712
713    /// A registration pseudonym used on pubhubs cards
714    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
715    #[serde(transparent)]
716    pub struct CardPseud(pub Id);
717
718    impl std::fmt::Display for CardPseud {
719        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720            self.0.fmt(f)
721        }
722    }
723
724    /// A registration pseudonym coupled with the registration date.
725    #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
726    pub struct CardPseudPackage {
727        pub card_pseud: CardPseud,
728        /// Registration date for this user.  Can be `None` for users that registered under v3.0.0.
729        pub registration_date: Option<NumericDate>,
730    }
731
732    having_message_code!(CardPseudPackage, CardPseudPackage);
733
734    /// Requests the 'registration pseudonym' used on PubHubs cards issued for this account.
735    /// Requires authentication.
736    pub struct CardPseudEP {}
737    impl EndpointDetails for CardPseudEP {
738        type RequestType = NoPayload;
739        type ResponseType = Result<CardPseudResp>;
740
741        const METHOD: http::Method = http::Method::POST;
742        const PATH: &'static str = ".ph/user/card-pseud";
743    }
744
745    /// Returned by [`CardPseudEP`].
746    #[derive(Serialize, Deserialize, Debug, Clone)]
747    #[serde(deny_unknown_fields)]
748    #[serde(rename = "snake_case")]
749    #[must_use]
750    pub enum CardPseudResp {
751        /// The auth provided is expired or otherwise invalid.  Obtain a new one and retry.
752        RetryWithNewAuthToken,
753
754        /// The requested registration pseudonym, signed by PHC's jwt key.
755        Success(Signed<CardPseudPackage>),
756    }
757
758    #[cfg(test)]
759    mod test {
760        use super::*;
761
762        #[test]
763        fn backwards_compat() {
764            let _: EnterResp = serde_json::from_value(serde_json::json!({
765                "AttributeAlreadyTaken": {
766                    "attr_type": "Fr7Gsfh73AU9k9N4eR9vDBINhMOImXm-Qqfkz0RxjwI",
767                    "value": "blurp"
768                }
769            }))
770            .unwrap();
771        }
772    }
773}