@@ -180,10 +180,12 @@ async fn serve_with_listener(
180180 listener : tokio:: net:: TcpListener ,
181181 enable_ctrl_c : bool ,
182182) -> anyhow:: Result < ( ) > {
183- // No KV store is attached here — this path is used by `AxumDevServer::run()`
184- // which is the manifest-unaware embedding API. Callers that need KV should
185- // use `run_app()` (manifest-driven) or attach a `KvHandle` directly via
186- // `EdgeZeroAxumService::with_kv_handle`.
183+ // No KV store or secret store is attached here — this path is used by
184+ // `AxumDevServer::run()`, which is the manifest-unaware embedding API.
185+ // Callers that need KV should use `run_app()` (manifest-driven) or attach
186+ // a `KvHandle` directly via `EdgeZeroAxumService::with_kv_handle`.
187+ // Callers that need secrets should use `run_app()` or attach a
188+ // `SecretHandle` directly via `EdgeZeroAxumService::with_secret_handle`.
187189 serve_with_listener_and_kv_path ( router, listener, enable_ctrl_c, None ) . await
188190}
189191
@@ -196,19 +198,23 @@ async fn serve_with_listener_and_kv_path(
196198 let kv_handle = kv_path
197199 . map ( |kv_path| kv_handle_from_path ( Path :: new ( kv_path) ) )
198200 . transpose ( ) ?;
199- serve_with_listener_and_kv_handle ( router, listener, enable_ctrl_c, kv_handle) . await
201+ serve_with_listener_and_stores ( router, listener, enable_ctrl_c, kv_handle, None ) . await
200202}
201203
202- async fn serve_with_listener_and_kv_handle (
204+ async fn serve_with_listener_and_stores (
203205 router : RouterService ,
204206 listener : tokio:: net:: TcpListener ,
205207 enable_ctrl_c : bool ,
206208 kv_handle : Option < edgezero_core:: key_value_store:: KvHandle > ,
209+ secret_handle : Option < edgezero_core:: secret_store:: SecretHandle > ,
207210) -> anyhow:: Result < ( ) > {
208211 let mut service = EdgeZeroAxumService :: new ( router) ;
209212 if let Some ( kv_handle) = kv_handle {
210213 service = service. with_kv_handle ( kv_handle) ;
211214 }
215+ if let Some ( secret_handle) = secret_handle {
216+ service = service. with_secret_handle ( secret_handle) ;
217+ }
212218
213219 let service = service;
214220 let router = Router :: new ( ) . fallback_service ( service_fn ( move |req| {
@@ -243,6 +249,7 @@ pub fn run_app<A: Hooks>(manifest_src: &str) -> anyhow::Result<()> {
243249 let kv_init_requirement = kv_init_requirement ( manifest) ;
244250 let kv_store_name = manifest. kv_store_name ( "axum" ) . to_string ( ) ;
245251 let kv_path = kv_store_path ( & kv_store_name) ;
252+ let has_secret_store = manifest. secret_store_enabled ( "axum" ) ;
246253
247254 let level: LevelFilter = logging. level . into ( ) ;
248255 let level = if logging. echo_stdout . unwrap_or ( true ) {
@@ -294,7 +301,22 @@ pub fn run_app<A: Hooks>(manifest_src: &str) -> anyhow::Result<()> {
294301 }
295302 }
296303 } ;
297- serve_with_listener_and_kv_handle ( router, listener, config. enable_ctrl_c , kv_handle) . await
304+ let secret_handle = if has_secret_store {
305+ log:: info!( "Secret store: reading from environment variables" ) ;
306+ Some ( edgezero_core:: secret_store:: SecretHandle :: new (
307+ std:: sync:: Arc :: new ( crate :: secret_store:: EnvSecretStore :: new ( ) ) ,
308+ ) )
309+ } else {
310+ None
311+ } ;
312+ serve_with_listener_and_stores (
313+ router,
314+ listener,
315+ config. enable_ctrl_c ,
316+ kv_handle,
317+ secret_handle,
318+ )
319+ . await
298320 } )
299321}
300322
@@ -427,8 +449,10 @@ name = "EDGEZERO_KV"
427449#[ cfg( test) ]
428450mod integration_tests {
429451 use super :: * ;
452+ use edgezero_core:: action;
430453 use edgezero_core:: context:: RequestContext ;
431454 use edgezero_core:: error:: EdgeError ;
455+ use edgezero_core:: extractor:: Secrets ;
432456 use edgezero_core:: router:: RouterService ;
433457 use std:: time:: { Duration , Instant } ;
434458
@@ -781,4 +805,117 @@ mod integration_tests {
781805
782806 server. handle . abort ( ) ;
783807 }
808+
809+ // -----------------------------------------------------------------------
810+ // Secret store helpers
811+ // -----------------------------------------------------------------------
812+
813+ struct TestServerSecrets {
814+ base_url : String ,
815+ handle : tokio:: task:: JoinHandle < ( ) > ,
816+ }
817+
818+ async fn start_test_server_with_secret_handle (
819+ router : RouterService ,
820+ secret_handle : Option < edgezero_core:: secret_store:: SecretHandle > ,
821+ ) -> TestServerSecrets {
822+ let listener = tokio:: net:: TcpListener :: bind ( "127.0.0.1:0" )
823+ . await
824+ . expect ( "bind secrets test server" ) ;
825+ let addr = listener. local_addr ( ) . expect ( "local addr" ) ;
826+ let handle = tokio:: spawn ( async move {
827+ let _ =
828+ super :: serve_with_listener_and_stores ( router, listener, false , None , secret_handle)
829+ . await ;
830+ } ) ;
831+ TestServerSecrets {
832+ base_url : format ! ( "http://{}" , addr) ,
833+ handle,
834+ }
835+ }
836+
837+ #[ action]
838+ async fn secret_value_handler ( Secrets ( store) : Secrets ) -> Result < String , EdgeError > {
839+ store
840+ . require_str ( "test-store" , "API_KEY" )
841+ . await
842+ . map_err ( EdgeError :: from)
843+ }
844+
845+ // -----------------------------------------------------------------------
846+ // Secret store integration tests
847+ // -----------------------------------------------------------------------
848+
849+ #[ tokio:: test( flavor = "multi_thread" ) ]
850+ async fn secret_present_returns_value ( ) {
851+ use edgezero_core:: secret_store:: { InMemorySecretStore , SecretHandle } ;
852+ use std:: sync:: Arc ;
853+
854+ let router = RouterService :: builder ( )
855+ . get ( "/secret" , secret_value_handler)
856+ . build ( ) ;
857+ let store =
858+ InMemorySecretStore :: new ( [ ( "test-store/API_KEY" , bytes:: Bytes :: from ( "s3cr3t" ) ) ] ) ;
859+ let handle = SecretHandle :: new ( Arc :: new ( store) ) ;
860+ let server = start_test_server_with_secret_handle ( router, Some ( handle) ) . await ;
861+
862+ let client = reqwest:: Client :: new ( ) ;
863+ let url = format ! ( "{}/secret" , server. base_url) ;
864+ let response = send_with_retry ( & client, |c| c. get ( url. as_str ( ) ) ) . await ;
865+
866+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: OK ) ;
867+ assert_eq ! ( response. text( ) . await . unwrap( ) , "s3cr3t" ) ;
868+
869+ server. handle . abort ( ) ;
870+ }
871+
872+ #[ tokio:: test( flavor = "multi_thread" ) ]
873+ async fn secret_missing_returns_500 ( ) {
874+ use edgezero_core:: secret_store:: { InMemorySecretStore , SecretHandle } ;
875+ use std:: sync:: Arc ;
876+
877+ let router = RouterService :: builder ( )
878+ . get ( "/secret" , secret_value_handler)
879+ . build ( ) ;
880+ let store = InMemorySecretStore :: new ( std:: iter:: empty :: < ( & str , bytes:: Bytes ) > ( ) ) ;
881+ let handle = SecretHandle :: new ( Arc :: new ( store) ) ;
882+ let server = start_test_server_with_secret_handle ( router, Some ( handle) ) . await ;
883+
884+ let client = reqwest:: Client :: new ( ) ;
885+ let url = format ! ( "{}/secret" , server. base_url) ;
886+ let response = send_with_retry ( & client, |c| c. get ( url. as_str ( ) ) ) . await ;
887+
888+ assert_eq ! (
889+ response. status( ) ,
890+ reqwest:: StatusCode :: INTERNAL_SERVER_ERROR
891+ ) ;
892+ let body = response. text ( ) . await . unwrap ( ) ;
893+ assert ! ( !body. contains( "API_KEY" ) ) ;
894+ assert ! ( body. contains( "required secret is not configured" ) ) ;
895+
896+ server. handle . abort ( ) ;
897+ }
898+
899+ #[ tokio:: test( flavor = "multi_thread" ) ]
900+ async fn no_secret_store_configured_returns_500 ( ) {
901+ let router = RouterService :: builder ( )
902+ . get ( "/secret" , secret_value_handler)
903+ . build ( ) ;
904+ let server = start_test_server_with_secret_handle ( router, None ) . await ;
905+
906+ let client = reqwest:: Client :: new ( ) ;
907+ let url = format ! ( "{}/secret" , server. base_url) ;
908+ let response = send_with_retry ( & client, |c| c. get ( url. as_str ( ) ) ) . await ;
909+
910+ assert_eq ! (
911+ response. status( ) ,
912+ reqwest:: StatusCode :: INTERNAL_SERVER_ERROR
913+ ) ;
914+ let body = response. text ( ) . await . unwrap ( ) ;
915+ assert ! ( body. contains(
916+ "no secret store configured -- check [stores.secrets] in edgezero.toml and platform bindings"
917+ ) ) ;
918+
919+ server. handle . abort ( ) ;
920+ }
784921}
0 commit comments