From 2d310b838e1a90bac4e5da906960ff491e5a72b4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:50:56 -0700 Subject: [PATCH] feat(tinyplace): default relay endpoint to follow OPENHUMAN_APP_ENV (#4611) --- .env.example | 5 +- src/openhuman/tinyplace/state.rs | 104 +++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index d7c2f25b1..6afc6a6e0 100644 --- a/.env.example +++ b/.env.example @@ -437,8 +437,9 @@ RUST_BACKTRACE=1 # tiny.place Agent World integration # --------------------------------------------------------------------------- # [optional] Base URL for the tiny.place backend API. -# Default: https://api.tiny.place (production) -# Set to https://staging-api.tiny.place to test against staging. +# When unset, the default follows OPENHUMAN_APP_ENV: a `staging` app env uses +# https://staging-api.tiny.place, otherwise https://api.tiny.place (production). +# Set this to pin an explicit host — it always overrides the app-env default. # TINYPLACE_API_BASE_URL=https://staging-api.tiny.place # --------------------------------------------------------------------------- diff --git a/src/openhuman/tinyplace/state.rs b/src/openhuman/tinyplace/state.rs index d932e1d3b..ac8b4eae2 100644 --- a/src/openhuman/tinyplace/state.rs +++ b/src/openhuman/tinyplace/state.rs @@ -17,26 +17,36 @@ use tinyplace::{LocalSigner, TinyPlaceClient, TinyPlaceClientOptions}; const LOG_PREFIX: &str = "[tinyplace]"; +/// Production tiny.place relay/API host — the default outside a staging build. +const TINYPLACE_PROD_BASE_URL: &str = "https://api.tiny.place"; +/// Staging tiny.place relay/API host — the default when the OpenHuman app env +/// is `staging` and no explicit `TINYPLACE_API_BASE_URL` is set. +const TINYPLACE_STAGING_BASE_URL: &str = "https://staging-api.tiny.place"; + /// Shared tiny.place state: lazy-built client keyed to one base URL. pub(crate) struct TinyPlaceState { /// Lazily initialised on first [`TinyPlaceState::client`] call. client: OnceCell, - /// Backend base URL (from `TINYPLACE_API_BASE_URL` env or staging default). + /// Backend base URL (from `TINYPLACE_API_BASE_URL`, else app-env default). pub(crate) base_url: String, } impl TinyPlaceState { - /// Build from the environment. `TINYPLACE_API_BASE_URL` overrides the - /// default production endpoint (set it to the staging host for testing). + /// Build from the environment. + /// + /// Base URL precedence: an explicit `TINYPLACE_API_BASE_URL` always wins; + /// otherwise the default follows the OpenHuman app environment so a staging + /// build talks to staging tiny.place and a production build talks to prod + /// (previously the default was hardcoded to prod regardless of app env, + /// which silently 404'd a staging instance against prod tiny.place). pub(crate) fn from_env() -> Self { - // Treat a blank/whitespace override as unset so it can't produce an - // invalid base_url (mirrors wallet::defaults env handling). - let base_url = std::env::var("TINYPLACE_API_BASE_URL") - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "https://api.tiny.place".to_string()); - log::debug!("{LOG_PREFIX} state created base_url={base_url}"); + let explicit = std::env::var("TINYPLACE_API_BASE_URL").ok(); + let app_env = crate::api::config::app_env_from_env(); + let base_url = resolve_base_url(explicit.as_deref(), app_env.as_deref()); + log::debug!( + "{LOG_PREFIX} state created base_url={base_url} app_env={}", + app_env.as_deref().unwrap_or("") + ); Self { client: OnceCell::new(), base_url, @@ -71,3 +81,75 @@ impl TinyPlaceState { .await } } + +/// Resolve the tiny.place base URL from an explicit override + app env. +/// +/// A non-blank explicit value (from `TINYPLACE_API_BASE_URL`) always wins. +/// Blank/whitespace is treated as unset (mirrors `wallet::defaults` handling) +/// so it can't produce an invalid base URL. With no explicit override, the +/// default tracks the app environment: staging → staging host, else prod. +fn resolve_base_url(explicit: Option<&str>, app_env: Option<&str>) -> String { + if let Some(url) = explicit.map(str::trim).filter(|value| !value.is_empty()) { + return url.to_string(); + } + default_base_url_for_app_env(app_env).to_string() +} + +/// The tiny.place host to default to for a given OpenHuman app environment. +fn default_base_url_for_app_env(app_env: Option<&str>) -> &'static str { + if crate::api::config::is_staging_app_env(app_env) { + TINYPLACE_STAGING_BASE_URL + } else { + TINYPLACE_PROD_BASE_URL + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_override_always_wins() { + // Even a staging app env must not override an explicit URL. + assert_eq!( + resolve_base_url(Some("https://custom.example"), Some("staging")), + "https://custom.example" + ); + assert_eq!( + resolve_base_url(Some(" https://trimmed.example "), None), + "https://trimmed.example" + ); + } + + #[test] + fn blank_override_falls_through_to_app_env_default() { + // Blank/whitespace is treated as unset. + assert_eq!( + resolve_base_url(Some(" "), Some("staging")), + TINYPLACE_STAGING_BASE_URL + ); + assert_eq!(resolve_base_url(Some(""), None), TINYPLACE_PROD_BASE_URL); + } + + #[test] + fn default_follows_app_env() { + assert_eq!( + default_base_url_for_app_env(Some("staging")), + TINYPLACE_STAGING_BASE_URL + ); + assert_eq!( + default_base_url_for_app_env(Some("STAGING")), + TINYPLACE_STAGING_BASE_URL + ); + assert_eq!( + default_base_url_for_app_env(Some("production")), + TINYPLACE_PROD_BASE_URL + ); + // Unknown / unset env defaults to prod. + assert_eq!(default_base_url_for_app_env(None), TINYPLACE_PROD_BASE_URL); + assert_eq!( + default_base_url_for_app_env(Some("dev")), + TINYPLACE_PROD_BASE_URL + ); + } +}