@@ -16,13 +16,7 @@ use std::io::Write;
1616use std:: path:: PathBuf ;
1717use std:: time:: { SystemTime , UNIX_EPOCH } ;
1818
19- // The refresh path below (REFRESH_LEEWAY_SECONDS, now_unix, MintResponse,
20- // redact, refresh, session_from_response) mirrors sandbox_session.rs and is
21- // covered by tests, but has no production caller yet: it's reserved for when
22- // a child of `databases run` re-mints an expiring HOTDATA_DATABASE_TOKEN
23- // (the child-side ApiClient consumption is not wired up yet). Annotated
24- // #[allow(dead_code)] until that lands so the build stays warning-clean.
25- #[ allow( dead_code) ]
19+ /// Refresh ahead of expiry to avoid racing it.
2620const REFRESH_LEEWAY_SECONDS : u64 = 60 ;
2721
2822#[ derive( Debug , Clone , Default , Serialize , Deserialize ) ]
@@ -74,15 +68,13 @@ pub fn clear() {
7468 }
7569}
7670
77- #[ allow( dead_code) ] // Part of the reserved refresh path (see REFRESH_LEEWAY_SECONDS).
7871fn now_unix ( ) -> u64 {
7972 SystemTime :: now ( )
8073 . duration_since ( UNIX_EPOCH )
8174 . map ( |d| d. as_secs ( ) )
8275 . unwrap_or ( 0 )
8376}
8477
85- #[ allow( dead_code) ] // Part of the reserved refresh path (see REFRESH_LEEWAY_SECONDS).
8678#[ derive( Deserialize ) ]
8779pub ( crate ) struct MintResponse {
8880 token : String ,
@@ -92,15 +84,13 @@ pub(crate) struct MintResponse {
9284 refresh_expires_in : u64 ,
9385}
9486
95- #[ allow( dead_code) ] // Part of the reserved refresh path (see REFRESH_LEEWAY_SECONDS).
9687fn redact ( s : & str ) -> String {
9788 util:: mask_credential ( s)
9889}
9990
10091/// Trade a refresh token for a fresh database JWT (no rotation). Same
10192/// endpoint as the new-mint path: `POST /v1/auth/database` with
10293/// grant_type=refresh_token.
103- #[ allow( dead_code) ] // Part of the reserved refresh path (see REFRESH_LEEWAY_SECONDS).
10494pub fn refresh ( api_url : & str , refresh_token : & str ) -> Result < DatabaseSession , String > {
10595 let url = format ! ( "{}/auth/database" , api_url. trim_end_matches( '/' ) ) ;
10696 let body = serde_json:: json!( {
@@ -135,7 +125,6 @@ pub fn refresh(api_url: &str, refresh_token: &str) -> Result<DatabaseSession, St
135125/// to). For refresh, `workspace_id` is left blank — the caller fills it
136126/// from the prior session, since the database-id ↔ workspace mapping is
137127/// invariant across refreshes.
138- #[ allow( dead_code) ] // Part of the reserved refresh path (see REFRESH_LEEWAY_SECONDS).
139128pub ( crate ) fn session_from_response ( resp : MintResponse , workspace_id : String ) -> DatabaseSession {
140129 let now = now_unix ( ) ;
141130 DatabaseSession {
@@ -148,6 +137,81 @@ pub(crate) fn session_from_response(resp: MintResponse, workspace_id: String) ->
148137 }
149138}
150139
140+ /// Decode a JWT's payload (without verifying the signature) and pull
141+ /// out the named string claim. Returns `None` if the token is
142+ /// unparseable or the claim is missing.
143+ fn jwt_string_claim ( token : & str , claim : & str ) -> Option < String > {
144+ use base64:: Engine ;
145+ let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
146+ if parts. len ( ) < 2 {
147+ return None ;
148+ }
149+ let payload = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
150+ . decode ( parts[ 1 ] . as_bytes ( ) )
151+ . ok ( ) ?;
152+ let value: serde_json:: Value = serde_json:: from_slice ( & payload) . ok ( ) ?;
153+ value. get ( claim) . and_then ( |v| v. as_str ( ) ) . map ( String :: from)
154+ }
155+
156+ /// Decode the `exp` claim out of a JWT without verifying the signature.
157+ /// Returns `None` if the token is unparseable; in that case the caller
158+ /// should treat it as expired (force-refresh or fail).
159+ fn jwt_exp ( token : & str ) -> Option < u64 > {
160+ use base64:: Engine ;
161+ let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
162+ if parts. len ( ) < 2 {
163+ return None ;
164+ }
165+ let payload = base64:: engine:: general_purpose:: URL_SAFE_NO_PAD
166+ . decode ( parts[ 1 ] . as_bytes ( ) )
167+ . ok ( ) ?;
168+ let value: serde_json:: Value = serde_json:: from_slice ( & payload) . ok ( ) ?;
169+ value. get ( "exp" ) . and_then ( |v| v. as_u64 ( ) )
170+ }
171+
172+ /// If `HOTDATA_DATABASE_TOKEN` is set in the environment, return
173+ /// `(token, database_id)` — the database id read from the JWT's
174+ /// `database` claim. Returns `None` if no env var is set, or if the
175+ /// token isn't a parseable JWT (in which case we can still use it as
176+ /// a bearer but can't identify the database).
177+ pub fn database_token_in_use ( ) -> Option < ( String , Option < String > ) > {
178+ let token = std:: env:: var ( "HOTDATA_DATABASE_TOKEN" ) . ok ( ) ?;
179+ if token. is_empty ( ) {
180+ return None ;
181+ }
182+ let database_id = jwt_string_claim ( & token, "database" ) ;
183+ Some ( ( token, database_id) )
184+ }
185+
186+ /// In-child equivalent of a parent-side `ensure_access_token`: operates
187+ /// on env vars only. Used by [`crate::api::ApiClient`] when the parent
188+ /// `databases run` already passed in `HOTDATA_DATABASE_TOKEN` and
189+ /// `HOTDATA_DATABASE_REFRESH_TOKEN`. The new tokens are *not* persisted
190+ /// to disk — the child may not have write access to the parent's
191+ /// config dir (sandboxed FS), and re-doing the refresh on the next
192+ /// invocation costs one HTTP call.
193+ ///
194+ /// Falls back to the current `HOTDATA_DATABASE_TOKEN` value if a
195+ /// refresh isn't needed or fails.
196+ pub fn refresh_from_env ( api_url : & str ) -> Option < String > {
197+ let current = std:: env:: var ( "HOTDATA_DATABASE_TOKEN" ) . ok ( ) ?;
198+ let needs_refresh = match jwt_exp ( & current) {
199+ Some ( exp) => exp. saturating_sub ( REFRESH_LEEWAY_SECONDS ) <= now_unix ( ) ,
200+ None => true ,
201+ } ;
202+ if !needs_refresh {
203+ return Some ( current) ;
204+ }
205+ let rt = std:: env:: var ( "HOTDATA_DATABASE_REFRESH_TOKEN" ) . ok ( ) ?;
206+ if rt. is_empty ( ) {
207+ return Some ( current) ;
208+ }
209+ match refresh ( api_url, & rt) {
210+ Ok ( new_session) => Some ( new_session. access_token ) ,
211+ Err ( _) => Some ( current) ,
212+ }
213+ }
214+
151215#[ cfg( test) ]
152216mod tests {
153217 use super :: * ;
0 commit comments