Skip to main content

wavekat_platform_client/
me.rs

1//! `/api/me` — the typed shape of the signed-in user.
2//!
3//! Public because every consumer needs it: the CLI prints it after
4//! `wk login`/`wk me`, and the desktop daemon shows the same fields in
5//! its Platform settings page. Keeping the struct here (and re-exported
6//! from the crate root) means consumers don't redefine it.
7
8use serde::{Deserialize, Serialize};
9
10use crate::client::Client;
11use crate::error::Result;
12
13/// The signed-in user, as returned by `GET /api/me`.
14#[derive(Debug, Clone, Deserialize)]
15pub struct Me {
16    /// Opaque platform user id. A UUID string since wavekat-platform
17    /// switched `users.id` off integer surrogate keys (which leaked the
18    /// signup count). Treat as an opaque token — never parse or compare
19    /// numerically.
20    pub id: String,
21    pub login: String,
22    pub name: Option<String>,
23    pub email: Option<String>,
24    pub role: String,
25}
26
27/// Counts of what deleting the signed-in account would take, as
28/// returned by `GET /api/me/deletion-preview`.
29///
30/// Public for the same reason [`Me`] is: every consumer shows these
31/// before asking for confirmation, so nobody confirms an irreversible
32/// action without seeing what is in it. Counts are of the account's
33/// *own* content — anything it authored inside someone else's project
34/// is not counted, because the platform's purge will not take it.
35///
36/// `Serialize` as well as `Deserialize` because a consumer is rarely
37/// the thing that displays these: the desktop daemon fetches them and
38/// forwards them to its own renderer, and without a way to write them
39/// back out every consumer would redefine the struct to do it.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct DeletionPreview {
43    /// Phone lines whose settings were backed up to the account. Named
44    /// `accounts` on the wire because the platform calls them voice
45    /// accounts; to the person confirming, they are their saved lines.
46    pub accounts: u32,
47    pub calls: u32,
48    pub recordings: u32,
49    pub transcripts: u32,
50    /// Share links that still resolve. Already-revoked ones are not
51    /// counted — they are gone as far as the holder of the link is
52    /// concerned.
53    pub shares: u32,
54    pub flows: u32,
55    pub contacts: u32,
56    pub prompts: u32,
57    pub projects: u32,
58    /// Label sets that nothing else still references. One another
59    /// project's labelling still uses survives, so this is not simply
60    /// "sets this account created".
61    pub label_sets: u32,
62    pub files: u32,
63    pub annotations: u32,
64    pub exports: u32,
65    pub models: u32,
66}
67
68impl Client {
69    /// Fetch the signed-in user from `/api/me`. The canonical way to
70    /// verify a freshly-minted token is reachable.
71    pub async fn whoami(&self) -> Result<Me> {
72        self.get_json("/api/me").await
73    }
74
75    /// Revoke the bearer token this client is using. After this returns
76    /// successfully, the same token will start producing 401s — drop the
77    /// `Client` (and clear whatever storage held the token).
78    pub async fn revoke_current_token(&self) -> Result<()> {
79        self.post_empty("/api/auth/cli/tokens/revoke-current").await
80    }
81
82    /// Fetch what deleting this account would take, for the
83    /// confirmation a consumer shows before calling
84    /// [`Client::delete_account`].
85    pub async fn deletion_preview(&self) -> Result<DeletionPreview> {
86        self.get_json("/api/me/deletion-preview").await
87    }
88
89    /// Delete the signed-in account.
90    ///
91    /// Credentials, live share links and the profile go immediately and
92    /// the account stops working at once; the remaining content and
93    /// stored audio are purged by the platform within 30 days. There is
94    /// no undo and no cancel, and signing in again with the same
95    /// provider identity is refused until that purge completes.
96    ///
97    /// Requires a credential minted in the last few minutes — otherwise
98    /// this returns [`Error::ReauthRequired`], and the caller should
99    /// sign in again and retry rather than treat the token as dead.
100    /// Once it succeeds, the token this client holds is gone: drop the
101    /// `Client` and clear whatever storage held it.
102    ///
103    /// [`Error::ReauthRequired`]: crate::Error::ReauthRequired
104    pub async fn delete_account(&self) -> Result<()> {
105        self.delete("/api/me").await
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn deletion_preview_reads_the_platform_wire_shape() {
115        // Verbatim from `GET /api/me/deletion-preview`. The desktop
116        // dialog renders these counts before an irreversible action, so
117        // a field renamed on the platform has to fail here rather than
118        // silently show a zero.
119        let json = r#"{
120            "accounts": 2, "calls": 6, "recordings": 1, "transcripts": 4,
121            "shares": 0, "flows": 3, "contacts": 9, "prompts": 2,
122            "projects": 1, "labelSets": 0, "files": 5, "annotations": 90,
123            "exports": 1, "models": 1
124        }"#;
125        let p: DeletionPreview = serde_json::from_str(json).unwrap();
126        assert_eq!(p.accounts, 2);
127        assert_eq!(p.calls, 6);
128        assert_eq!(p.label_sets, 0);
129        assert_eq!(p.recordings, 1);
130        assert_eq!(p.annotations, 90);
131        assert_eq!(p.models, 1);
132    }
133
134    #[test]
135    fn deletion_preview_round_trips_for_a_consumer_to_forward() {
136        // The daemon deserializes the platform's answer and serializes
137        // it again for its renderer; losing a count in the middle would
138        // understate what a delete takes.
139        let p: DeletionPreview = serde_json::from_str(
140            r#"{"accounts":2,"calls":6,"recordings":1,"transcripts":4,"shares":0,
141                "flows":3,"contacts":9,"prompts":2,"projects":1,"labelSets":0,
142                "files":5,"annotations":90,"exports":1,"models":1}"#,
143        )
144        .unwrap();
145        let back: DeletionPreview =
146            serde_json::from_str(&serde_json::to_string(&p).unwrap()).unwrap();
147        assert_eq!(back.calls, 6);
148        assert_eq!(back.annotations, 90);
149    }
150
151    #[test]
152    fn deletion_preview_needs_every_count() {
153        // Missing fields must be an error, not a default zero: "nothing
154        // will be deleted" is the one wrong answer this dialog can give.
155        let err = serde_json::from_str::<DeletionPreview>(r#"{"calls": 6}"#);
156        assert!(err.is_err(), "expected a decode error, got {err:?}");
157    }
158}