@@ -14,8 +14,9 @@ pub struct ApiClient {
1414}
1515
1616impl ApiClient {
17- /// Create a new API client. Loads config, validates auth.
18- /// Pass `workspace_id` for endpoints that require it, or `None` for workspace-less endpoints.
17+ /// Create a new API client. Loads config, pre-flights a JWT session.
18+ /// Pass `workspace_id` for endpoints that require it, or `None` for
19+ /// workspace-less endpoints.
1920 pub fn new ( workspace_id : Option < & str > ) -> Self {
2021 let profile_config = match config:: load ( "default" ) {
2122 Ok ( c) => c,
@@ -25,17 +26,27 @@ impl ApiClient {
2526 }
2627 } ;
2728
28- let api_key = match & profile_config. api_key {
29- Some ( key) if key != "PLACEHOLDER" => key. clone ( ) ,
30- _ => {
31- eprintln ! ( "error: not authenticated. Run 'hotdata auth login' (or 'hotdata auth') to log in." ) ;
29+ let api_key_fallback = profile_config
30+ . api_key
31+ . as_deref ( )
32+ . filter ( |k| !k. is_empty ( ) && * k != "PLACEHOLDER" ) ;
33+
34+ // Pre-flight: return the cached JWT if valid, refresh it if
35+ // close to expiry, or mint a new one from the API key. The
36+ // returned string is a JWT — that's what we send on the wire.
37+ let access_token = match crate :: jwt:: ensure_access_token ( & profile_config, api_key_fallback)
38+ {
39+ Ok ( t) => t,
40+ Err ( e) => {
41+ eprintln ! ( "{}" , format!( "error: {e}" ) . red( ) ) ;
42+ eprintln ! ( "Run {} to log in, or pass --api-key." , "hotdata auth" . cyan( ) ) ;
3243 std:: process:: exit ( 1 ) ;
3344 }
3445 } ;
3546
3647 Self {
3748 client : reqwest:: blocking:: Client :: new ( ) ,
38- api_key,
49+ api_key : access_token ,
3950 api_url : profile_config. api_url . to_string ( ) ,
4051 workspace_id : workspace_id. map ( String :: from) ,
4152 sandbox_id : std:: env:: var ( "HOTDATA_SANDBOX" ) . ok ( ) . or_else ( || {
@@ -60,29 +71,6 @@ impl ApiClient {
6071 }
6172 }
6273
63- fn debug_headers ( & self ) -> Vec < ( & str , String ) > {
64- let masked = if self . api_key . len ( ) > 4 {
65- format ! ( "Bearer ...{}" , & self . api_key[ self . api_key. len( ) -4 ..] )
66- } else {
67- "Bearer ***" . to_string ( )
68- } ;
69- let mut headers = vec ! [ ( "Authorization" , masked) ] ;
70- if let Some ( ref ws) = self . workspace_id {
71- headers. push ( ( "X-Workspace-Id" , ws. clone ( ) ) ) ;
72- }
73- if let Some ( ref sid) = self . sandbox_id {
74- // Send both headers during the session→sandbox migration window.
75- headers. push ( ( "X-Session-Id" , sid. clone ( ) ) ) ;
76- headers. push ( ( "X-Sandbox-Id" , sid. clone ( ) ) ) ;
77- }
78- headers
79- }
80-
81- fn log_request ( & self , method : & str , url : & str , body : Option < & serde_json:: Value > ) {
82- let headers = self . debug_headers ( ) ;
83- let header_refs: Vec < ( & str , & str ) > = headers. iter ( ) . map ( |( k, v) | ( * k, v. as_str ( ) ) ) . collect ( ) ;
84- util:: debug_request ( method, url, & header_refs, body) ;
85- }
8674
8775 /// Prints an error for a non-2xx response and exits. On 4xx, first re-probes
8876 /// the API key: if it's actually invalid, a clear re-auth hint is shown
@@ -111,29 +99,25 @@ impl ApiClient {
11199 req
112100 }
113101
114- /// GET request with query parameters, returns parsed response.
115- /// Parameters with `None` values are omitted.
116- pub fn get_with_params < T : DeserializeOwned > ( & self , path : & str , params : & [ ( & str , Option < String > ) ] ) -> T {
117- let filtered: Vec < ( & str , & String ) > = params. iter ( )
118- . filter_map ( |( k, v) | v. as_ref ( ) . map ( |val| ( * k, val) ) )
119- . collect ( ) ;
120- let url = format ! ( "{}{path}" , self . api_url) ;
121- self . log_request ( "GET" , & url, None ) ;
122-
123- let resp = match self . build_request ( reqwest:: Method :: GET , & url) . query ( & filtered) . send ( ) {
124- Ok ( r) => r,
102+ /// Send via `util::send_debug` and unwrap connection errors with the
103+ /// CLI's standard "error connecting" exit. All public HTTP methods
104+ /// route through here so debug logging is uniform.
105+ fn send (
106+ & self ,
107+ builder : reqwest:: blocking:: RequestBuilder ,
108+ body_for_log : Option < & serde_json:: Value > ,
109+ ) -> ( reqwest:: StatusCode , String ) {
110+ match util:: send_debug ( & self . client , builder, body_for_log) {
111+ Ok ( pair) => pair,
125112 Err ( e) => {
126113 eprintln ! ( "error connecting to API: {e}" ) ;
127114 std:: process:: exit ( 1 ) ;
128115 }
129- } ;
130-
131- let ( status, body) = util:: debug_response ( resp) ;
132- if !status. is_success ( ) {
133- self . fail_response ( status, body) ;
134116 }
117+ }
135118
136- match serde_json:: from_str ( & body) {
119+ fn parse_json < T : DeserializeOwned > ( body : & str ) -> T {
120+ match serde_json:: from_str ( body) {
137121 Ok ( v) => v,
138122 Err ( e) => {
139123 eprintln ! ( "error parsing response: {e}" ) ;
@@ -142,159 +126,83 @@ impl ApiClient {
142126 }
143127 }
144128
145- /// GET request, returns parsed response.
146- pub fn get < T : DeserializeOwned > ( & self , path : & str ) -> T {
129+ /// GET request with query parameters, returns parsed response.
130+ /// Parameters with `None` values are omitted.
131+ pub fn get_with_params < T : DeserializeOwned > ( & self , path : & str , params : & [ ( & str , Option < String > ) ] ) -> T {
132+ let filtered: Vec < ( & str , & String ) > = params. iter ( )
133+ . filter_map ( |( k, v) | v. as_ref ( ) . map ( |val| ( * k, val) ) )
134+ . collect ( ) ;
147135 let url = format ! ( "{}{path}" , self . api_url) ;
148- self . log_request ( "GET" , & url, None ) ;
149-
150- let resp = match self . build_request ( reqwest:: Method :: GET , & url) . send ( ) {
151- Ok ( r) => r,
152- Err ( e) => {
153- eprintln ! ( "error connecting to API: {e}" ) ;
154- std:: process:: exit ( 1 ) ;
155- }
156- } ;
157-
158- let ( status, body) = util:: debug_response ( resp) ;
136+ let req = self . build_request ( reqwest:: Method :: GET , & url) . query ( & filtered) ;
137+ let ( status, body) = self . send ( req, None ) ;
159138 if !status. is_success ( ) {
160139 self . fail_response ( status, body) ;
161140 }
141+ Self :: parse_json ( & body)
142+ }
162143
163- match serde_json:: from_str ( & body) {
164- Ok ( v) => v,
165- Err ( e) => {
166- eprintln ! ( "error parsing response: {e}" ) ;
167- std:: process:: exit ( 1 ) ;
168- }
144+ /// GET request, returns parsed response.
145+ pub fn get < T : DeserializeOwned > ( & self , path : & str ) -> T {
146+ let url = format ! ( "{}{path}" , self . api_url) ;
147+ let req = self . build_request ( reqwest:: Method :: GET , & url) ;
148+ let ( status, body) = self . send ( req, None ) ;
149+ if !status. is_success ( ) {
150+ self . fail_response ( status, body) ;
169151 }
152+ Self :: parse_json ( & body)
170153 }
171154
172155 /// GET request; returns `None` on HTTP 404. Other status codes use the same handling as
173156 /// [`Self::get`]. Used when probing many paths where a missing resource is normal.
174157 pub fn get_none_if_not_found < T : DeserializeOwned > ( & self , path : & str ) -> Option < T > {
175158 let url = format ! ( "{}{path}" , self . api_url) ;
176- self . log_request ( "GET" , & url, None ) ;
177-
178- let resp = match self . build_request ( reqwest:: Method :: GET , & url) . send ( ) {
179- Ok ( r) => r,
180- Err ( e) => {
181- eprintln ! ( "error connecting to API: {e}" ) ;
182- std:: process:: exit ( 1 ) ;
183- }
184- } ;
185-
186- let ( status, body) = util:: debug_response ( resp) ;
159+ let req = self . build_request ( reqwest:: Method :: GET , & url) ;
160+ let ( status, body) = self . send ( req, None ) ;
187161 if status == reqwest:: StatusCode :: NOT_FOUND {
188162 return None ;
189163 }
190164 if !status. is_success ( ) {
191165 self . fail_response ( status, body) ;
192166 }
193-
194- match serde_json:: from_str ( & body) {
195- Ok ( v) => Some ( v) ,
196- Err ( e) => {
197- eprintln ! ( "error parsing response: {e}" ) ;
198- std:: process:: exit ( 1 ) ;
199- }
200- }
167+ Some ( Self :: parse_json ( & body) )
201168 }
202169
203170 /// POST request with JSON body, returns parsed response.
204171 pub fn post < T : DeserializeOwned > ( & self , path : & str , body : & serde_json:: Value ) -> T {
205172 let url = format ! ( "{}{path}" , self . api_url) ;
206- self . log_request ( "POST" , & url, Some ( body) ) ;
207-
208- let resp = match self . build_request ( reqwest:: Method :: POST , & url)
209- . json ( body)
210- . send ( )
211- {
212- Ok ( r) => r,
213- Err ( e) => {
214- eprintln ! ( "error connecting to API: {e}" ) ;
215- std:: process:: exit ( 1 ) ;
216- }
217- } ;
218-
219- let ( status, resp_body) = util:: debug_response ( resp) ;
173+ let req = self . build_request ( reqwest:: Method :: POST , & url) . json ( body) ;
174+ let ( status, resp_body) = self . send ( req, Some ( body) ) ;
220175 if !status. is_success ( ) {
221176 self . fail_response ( status, resp_body) ;
222177 }
223-
224- match serde_json:: from_str ( & resp_body) {
225- Ok ( v) => v,
226- Err ( e) => {
227- eprintln ! ( "error parsing response: {e}" ) ;
228- std:: process:: exit ( 1 ) ;
229- }
230- }
178+ Self :: parse_json ( & resp_body)
231179 }
232180
233181 /// GET request, exits only on connection error, returns raw (status, body).
234182 /// Use for best-effort endpoints (e.g. health checks) where the caller wants
235183 /// to handle non-2xx responses gracefully instead of aborting.
236184 pub fn get_raw ( & self , path : & str ) -> ( reqwest:: StatusCode , String ) {
237185 let url = format ! ( "{}{path}" , self . api_url) ;
238- self . log_request ( "GET" , & url, None ) ;
239-
240- let resp = match self . build_request ( reqwest:: Method :: GET , & url) . send ( ) {
241- Ok ( r) => r,
242- Err ( e) => {
243- eprintln ! ( "error connecting to API: {e}" ) ;
244- std:: process:: exit ( 1 ) ;
245- }
246- } ;
247-
248- util:: debug_response ( resp)
186+ let req = self . build_request ( reqwest:: Method :: GET , & url) ;
187+ self . send ( req, None )
249188 }
250189
251190 /// POST request with JSON body, exits on error, returns raw (status, body).
252191 pub fn post_raw ( & self , path : & str , body : & serde_json:: Value ) -> ( reqwest:: StatusCode , String ) {
253192 let url = format ! ( "{}{path}" , self . api_url) ;
254- self . log_request ( "POST" , & url, Some ( body) ) ;
255-
256- let resp = match self . build_request ( reqwest:: Method :: POST , & url)
257- . json ( body)
258- . send ( )
259- {
260- Ok ( r) => r,
261- Err ( e) => {
262- eprintln ! ( "error connecting to API: {e}" ) ;
263- std:: process:: exit ( 1 ) ;
264- }
265- } ;
266-
267- util:: debug_response ( resp)
193+ let req = self . build_request ( reqwest:: Method :: POST , & url) . json ( body) ;
194+ self . send ( req, Some ( body) )
268195 }
269196
270197 /// PATCH request with JSON body, returns parsed response.
271198 pub fn patch < T : DeserializeOwned > ( & self , path : & str , body : & serde_json:: Value ) -> T {
272199 let url = format ! ( "{}{path}" , self . api_url) ;
273- self . log_request ( "PATCH" , & url, Some ( body) ) ;
274-
275- let resp = match self . build_request ( reqwest:: Method :: PATCH , & url)
276- . json ( body)
277- . send ( )
278- {
279- Ok ( r) => r,
280- Err ( e) => {
281- eprintln ! ( "error connecting to API: {e}" ) ;
282- std:: process:: exit ( 1 ) ;
283- }
284- } ;
285-
286- let ( status, resp_body) = util:: debug_response ( resp) ;
200+ let req = self . build_request ( reqwest:: Method :: PATCH , & url) . json ( body) ;
201+ let ( status, resp_body) = self . send ( req, Some ( body) ) ;
287202 if !status. is_success ( ) {
288203 self . fail_response ( status, resp_body) ;
289204 }
290-
291- match serde_json:: from_str ( & resp_body) {
292- Ok ( v) => v,
293- Err ( e) => {
294- eprintln ! ( "error parsing response: {e}" ) ;
295- std:: process:: exit ( 1 ) ;
296- }
297- }
205+ Self :: parse_json ( & resp_body)
298206 }
299207
300208 /// POST with a custom request body (for file uploads). Returns raw status and body.
@@ -306,24 +214,16 @@ impl ApiClient {
306214 content_length : Option < u64 > ,
307215 ) -> ( reqwest:: StatusCode , String ) {
308216 let url = format ! ( "{}{path}" , self . api_url) ;
309- self . log_request ( "POST" , & url, None ) ;
310-
311217 let mut req = self . build_request ( reqwest:: Method :: POST , & url)
312218 . header ( "Content-Type" , content_type) ;
313-
314219 if let Some ( len) = content_length {
315220 req = req. header ( "Content-Length" , len) ;
316221 }
317-
318- let resp = match req. body ( reqwest:: blocking:: Body :: new ( reader) ) . send ( ) {
319- Ok ( r) => r,
320- Err ( e) => {
321- eprintln ! ( "error connecting to API: {e}" ) ;
322- std:: process:: exit ( 1 ) ;
323- }
324- } ;
325-
326- util:: debug_response ( resp)
222+ let req = req. body ( reqwest:: blocking:: Body :: new ( reader) ) ;
223+ // Body is an opaque stream — nothing meaningful to print under
224+ // --debug, so pass `None`. Headers (including the masked
225+ // Authorization) still log.
226+ self . send ( req, None )
327227 }
328228
329229}
0 commit comments