@@ -9,6 +9,7 @@ use crate::tls::{
99 grpc_inference_client, require_tls_materials,
1010} ;
1111use bytes:: Bytes ;
12+ use chrono:: DateTime ;
1213use dialoguer:: { Confirm , Select , theme:: ColorfulTheme } ;
1314use futures:: StreamExt ;
1415use http_body_util:: Full ;
@@ -3758,13 +3759,57 @@ fn parse_credential_pairs(items: &[String]) -> Result<HashMap<String, String>> {
37583759 Ok ( map)
37593760}
37603761
3762+ pub fn parse_credential_expiry_cli_value ( value : & str ) -> std:: result:: Result < i64 , String > {
3763+ parse_credential_expiry_value ( value, None ) . map_err ( |err| err. to_string ( ) )
3764+ }
3765+
3766+ fn credential_expiry_value_error ( key : Option < & str > , detail : & str ) -> miette:: Report {
3767+ key. map_or_else (
3768+ || miette:: miette!( "--credential-expires-at value {detail}" ) ,
3769+ |key| miette:: miette!( "--credential-expires-at value for '{key}' {detail}" ) ,
3770+ )
3771+ }
3772+
3773+ fn parse_credential_expiry_value ( value : & str , key : Option < & str > ) -> Result < i64 > {
3774+ let value = value. trim ( ) ;
3775+ if value. is_empty ( ) {
3776+ return Err ( credential_expiry_value_error ( key, "cannot be empty" ) ) ;
3777+ }
3778+
3779+ if let Ok ( value_ms) = value. parse :: < i64 > ( ) {
3780+ if value_ms < 0 {
3781+ return Err ( credential_expiry_value_error (
3782+ key,
3783+ "must be greater than or equal to 0" ,
3784+ ) ) ;
3785+ }
3786+ return Ok ( value_ms) ;
3787+ }
3788+
3789+ let parsed = DateTime :: parse_from_rfc3339 ( value) . map_err ( |_| {
3790+ credential_expiry_value_error (
3791+ key,
3792+ "must be a Unix epoch millisecond timestamp or RFC3339 timestamp" ,
3793+ )
3794+ } ) ?;
3795+ let value_ms = parsed. timestamp_millis ( ) ;
3796+ if value_ms < 0 {
3797+ return Err ( credential_expiry_value_error (
3798+ key,
3799+ "must be greater than or equal to 0" ,
3800+ ) ) ;
3801+ }
3802+
3803+ Ok ( value_ms)
3804+ }
3805+
37613806fn parse_credential_expiry_pairs ( items : & [ String ] ) -> Result < HashMap < String , i64 > > {
37623807 let mut map = HashMap :: new ( ) ;
37633808
37643809 for item in items {
37653810 let Some ( ( key, value) ) = item. split_once ( '=' ) else {
37663811 return Err ( miette:: miette!(
3767- "--credential-expires-at expects KEY=TIMESTAMP_MS , got '{item}'"
3812+ "--credential-expires-at expects KEY=TIMESTAMP , got '{item}'"
37683813 ) ) ;
37693814 } ;
37703815 let key = key. trim ( ) ;
@@ -3773,16 +3818,7 @@ fn parse_credential_expiry_pairs(items: &[String]) -> Result<HashMap<String, i64
37733818 "--credential-expires-at key cannot be empty"
37743819 ) ) ;
37753820 }
3776- let value = value. trim ( ) . parse :: < i64 > ( ) . map_err ( |_| {
3777- miette:: miette!(
3778- "--credential-expires-at value for '{key}' must be a Unix epoch millisecond timestamp"
3779- )
3780- } ) ?;
3781- if value < 0 {
3782- return Err ( miette:: miette!(
3783- "--credential-expires-at value for '{key}' must be greater than or equal to 0"
3784- ) ) ;
3785- }
3821+ let value = parse_credential_expiry_value ( value, Some ( key) ) ?;
37863822 map. insert ( key. to_string ( ) , value) ;
37873823 }
37883824
@@ -6674,7 +6710,8 @@ mod tests {
66746710 gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
66756711 git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
66766712 inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
6677- parse_credential_pairs, plaintext_gateway_is_remote, progress_step_from_metadata,
6713+ parse_credential_expiry_cli_value, parse_credential_expiry_pairs, parse_credential_pairs,
6714+ plaintext_gateway_is_remote, progress_step_from_metadata,
66786715 provider_profile_allows_refresh_bootstrap, provisioning_timeout_message,
66796716 ready_false_condition_message, refresh_status_header, refresh_status_row, resolve_from,
66806717 sandbox_should_persist, service_expose_status_error, service_url_for_gateway,
@@ -6802,6 +6839,48 @@ mod tests {
68026839 ) ) ;
68036840 }
68046841
6842+ #[ test]
6843+ fn parse_credential_expiry_pairs_accepts_epoch_millis_and_rfc3339 ( ) {
6844+ let parsed = parse_credential_expiry_pairs ( & [
6845+ "API_TOKEN=1767225600000" . to_string ( ) ,
6846+ "MS_GRAPH_ACCESS_TOKEN=2026-01-01T00:00:00Z" . to_string ( ) ,
6847+ ] )
6848+ . expect ( "parse" ) ;
6849+
6850+ assert_eq ! ( parsed. get( "API_TOKEN" ) , Some ( & 1_767_225_600_000 ) ) ;
6851+ assert_eq ! (
6852+ parsed. get( "MS_GRAPH_ACCESS_TOKEN" ) ,
6853+ Some ( & 1_767_225_600_000 )
6854+ ) ;
6855+ }
6856+
6857+ #[ test]
6858+ fn parse_credential_expiry_pairs_accepts_zero_to_clear_expiry ( ) {
6859+ let parsed =
6860+ parse_credential_expiry_pairs ( & [ "API_TOKEN=0" . to_string ( ) ] ) . expect ( "parse zero" ) ;
6861+
6862+ assert_eq ! ( parsed. get( "API_TOKEN" ) , Some ( & 0 ) ) ;
6863+ }
6864+
6865+ #[ test]
6866+ fn parse_credential_expiry_rejects_invalid_timestamp ( ) {
6867+ let err = parse_credential_expiry_pairs ( & [ "API_TOKEN=next-week" . to_string ( ) ] )
6868+ . expect_err ( "invalid timestamp should error" ) ;
6869+
6870+ assert ! (
6871+ err. to_string( )
6872+ . contains( "must be a Unix epoch millisecond timestamp or RFC3339 timestamp" )
6873+ ) ;
6874+ }
6875+
6876+ #[ test]
6877+ fn parse_credential_expiry_cli_value_accepts_rfc3339_offsets ( ) {
6878+ let parsed = parse_credential_expiry_cli_value ( "2026-01-01T01:00:00+01:00" )
6879+ . expect ( "parse RFC3339 with offset" ) ;
6880+
6881+ assert_eq ! ( parsed, 1_767_225_600_000 ) ;
6882+ }
6883+
68056884 #[ test]
68066885 fn provider_attachment_table_formats_provider_counts ( ) {
68076886 let output = format_provider_attachment_table (
0 commit comments