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}