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}