bestool_canopy/lib.rs
1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5mod backup;
6mod client;
7pub mod registration;
8mod reqwest_transport;
9#[cfg(test)]
10mod test_support;
11mod transport;
12
13/// Wire types generated at build time from canopy's OpenAPI document.
14///
15/// These are the canonical request and response types for canopy's API, and the
16/// ones to reach for first. The build script fetches the live spec and
17/// regenerates them, so they track canopy as it evolves and nothing here is
18/// hand-maintained or committed. Each type carries the schema's own description
19/// as rustdoc. (A failed fetch fails the build rather than silently using the
20/// committed snapshot, which is reserved for docs.rs and explicit offline
21/// builds — see the build script.)
22///
23/// Naming follows canopy's schema: request bodies are `…Args` (e.g.
24/// [`BackupCredentialsArgs`], [`ReportArgs`], [`BackupCapabilitiesArgs`]), and
25/// credentials come back as [`CredentialProcessOutput`].
26///
27/// The generated source is rewritten in two ways the raw JSON Schema can't
28/// express (see the build script): timestamp fields are [`jiff::Timestamp`]
29/// rather than strings, and credential secrets (`secret_access_key`,
30/// `session_token`, `repo_password`) are wrapped in [`Redacted`] so they never
31/// surface in `Debug` output or logs — read them through the inner value.
32///
33/// [`CanopyClient`] has one generated method per endpoint (also emitted from the
34/// spec into this module — e.g. `backup_credentials`, `restore_worklist`,
35/// `tags`), taking and returning these types; that's how you call canopy. The
36/// method name is the path (`/backup-credentials` → `backup_credentials`), verb-
37/// prefixed only where a path is served by several verbs. `backup_target`'s
38/// dormant-device case is read from its result via [`TargetOutcome::from_result`];
39/// any non-2xx surfaces as [`CanopyHttpError`]. The generic
40/// `get`/`request`/`request_json` escape hatch is behind the off-by-default
41/// `raw-requests` feature — reach for it only for something the generated methods
42/// don't cover.
43///
44/// To check these types are current, [`schema::OPENAPI_BLAKE3`] is the blake3
45/// digest of the OpenAPI document they were generated from (compare it against
46/// `curl -fsS https://meta.tamanu.app/api/openapi.json | b3sum`), and
47/// [`schema::OPENAPI_SOURCE`] records whether that document was fetched live or
48/// read from the committed snapshot.
49///
50/// [`BackupCredentialsArgs`]: schema::BackupCredentialsArgs
51/// [`ReportArgs`]: schema::ReportArgs
52/// [`BackupCapabilitiesArgs`]: schema::BackupCapabilitiesArgs
53/// [`CredentialProcessOutput`]: schema::CredentialProcessOutput
54pub mod schema {
55 include!(concat!(env!("OUT_DIR"), "/canopy_schema.rs"));
56}
57
58pub use async_trait::async_trait;
59pub use backup::{ContainerCreds, TargetOutcome};
60pub use client::{CanopyClient, CanopyHttpError};
61pub use reqwest_transport::{
62 CERT_RENEW_AFTER, ClientBuilderFactory, DEFAULT_CANOPY_URL, ReqwestTransport, TAILSCALE_URL,
63 device_identity, tailscale_client,
64};
65pub use transport::{CanopyRequest, CanopyResponse, CanopyTransport};
66pub use {bytes, http, reqwest};
67
68/// Wraps a sensitive value so its `Debug` output doesn't leak the contents.
69#[derive(Clone)]
70pub struct Redacted<T>(pub T);
71
72impl<T> fmt::Debug for Redacted<T> {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str("<redacted>")
75 }
76}
77
78impl<T> std::ops::Deref for Redacted<T> {
79 type Target = T;
80 fn deref(&self) -> &T {
81 &self.0
82 }
83}
84
85impl<T: Serialize> Serialize for Redacted<T> {
86 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
87 self.0.serialize(serializer)
88 }
89}
90
91impl<'de, T: Deserialize<'de>> Deserialize<'de> for Redacted<T> {
92 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
93 T::deserialize(deserializer).map(Redacted)
94 }
95}