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)]
41pub struct DeletionPreview {
42    pub calls: u32,
43    pub recordings: u32,
44    pub transcripts: u32,
45    /// Share links that still resolve. Already-revoked ones are not
46    /// counted — they are gone as far as the holder of the link is
47    /// concerned.
48    pub shares: u32,
49    pub flows: u32,
50    pub contacts: u32,
51    pub prompts: u32,
52    pub projects: u32,
53    pub files: u32,
54    pub annotations: u32,
55    pub exports: u32,
56    pub models: u32,
57}
58
59impl Client {
60    /// Fetch the signed-in user from `/api/me`. The canonical way to
61    /// verify a freshly-minted token is reachable.
62    pub async fn whoami(&self) -> Result<Me> {
63        self.get_json("/api/me").await
64    }
65
66    /// Revoke the bearer token this client is using. After this returns
67    /// successfully, the same token will start producing 401s — drop the
68    /// `Client` (and clear whatever storage held the token).
69    pub async fn revoke_current_token(&self) -> Result<()> {
70        self.post_empty("/api/auth/cli/tokens/revoke-current").await
71    }
72
73    /// Fetch what deleting this account would take, for the
74    /// confirmation a consumer shows before calling
75    /// [`Client::delete_account`].
76    pub async fn deletion_preview(&self) -> Result<DeletionPreview> {
77        self.get_json("/api/me/deletion-preview").await
78    }
79
80    /// Delete the signed-in account.
81    ///
82    /// Credentials, live share links and the profile go immediately and
83    /// the account stops working at once; the remaining content and
84    /// stored audio are purged by the platform within 30 days. There is
85    /// no undo and no cancel, and signing in again with the same
86    /// provider identity is refused until that purge completes.
87    ///
88    /// Requires a credential minted in the last few minutes — otherwise
89    /// this returns [`Error::ReauthRequired`], and the caller should
90    /// sign in again and retry rather than treat the token as dead.
91    /// Once it succeeds, the token this client holds is gone: drop the
92    /// `Client` and clear whatever storage held it.
93    ///
94    /// [`Error::ReauthRequired`]: crate::Error::ReauthRequired
95    pub async fn delete_account(&self) -> Result<()> {
96        self.delete("/api/me").await
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn deletion_preview_reads_the_platform_wire_shape() {
106        // Verbatim from `GET /api/me/deletion-preview`. The desktop
107        // dialog renders these counts before an irreversible action, so
108        // a field renamed on the platform has to fail here rather than
109        // silently show a zero.
110        let json = r#"{
111            "accounts": 2, "calls": 6, "recordings": 1, "transcripts": 4,
112            "shares": 0, "flows": 3, "contacts": 9, "prompts": 2,
113            "projects": 1, "labelSets": 0, "files": 5, "annotations": 90,
114            "exports": 1, "models": 1
115        }"#;
116        let p: DeletionPreview = serde_json::from_str(json).unwrap();
117        assert_eq!(p.calls, 6);
118        assert_eq!(p.recordings, 1);
119        assert_eq!(p.annotations, 90);
120        assert_eq!(p.models, 1);
121    }
122
123    #[test]
124    fn deletion_preview_round_trips_for_a_consumer_to_forward() {
125        // The daemon deserializes the platform's answer and serializes
126        // it again for its renderer; losing a count in the middle would
127        // understate what a delete takes.
128        let p: DeletionPreview = serde_json::from_str(
129            r#"{"calls":6,"recordings":1,"transcripts":4,"shares":0,
130                "flows":3,"contacts":9,"prompts":2,"projects":1,"files":5,
131                "annotations":90,"exports":1,"models":1}"#,
132        )
133        .unwrap();
134        let back: DeletionPreview =
135            serde_json::from_str(&serde_json::to_string(&p).unwrap()).unwrap();
136        assert_eq!(back.calls, 6);
137        assert_eq!(back.annotations, 90);
138    }
139
140    #[test]
141    fn deletion_preview_needs_every_count() {
142        // Missing fields must be an error, not a default zero: "nothing
143        // will be deleted" is the one wrong answer this dialog can give.
144        let err = serde_json::from_str::<DeletionPreview>(r#"{"calls": 6}"#);
145        assert!(err.is_err(), "expected a decode error, got {err:?}");
146    }
147}