1pub mod a2a_setup;
7pub mod admin;
8pub mod answers_crypto;
9pub mod bundle;
10pub mod bundle_source;
11pub mod capabilities;
12pub mod card_setup;
13pub mod cli_args;
14pub mod cli_commands;
15pub mod cli_helpers;
16pub mod cli_i18n;
17pub mod config_envelope;
18pub mod deployment_targets;
19pub mod discovery;
20pub mod doctor;
21pub mod engine;
22pub mod env_deploy;
23pub mod env_mode;
24pub mod env_wizard;
25pub mod flow;
26pub mod generated_secrets;
27pub mod gtbundle;
28pub mod http_client;
29pub mod mcp_setup;
30pub mod no_ui_oauth;
31pub mod oauth_callback;
32pub mod oauth_device;
33pub mod plan;
34pub mod platform_setup;
35pub mod provider_commands;
36pub mod provider_registry;
37pub mod provider_state;
38pub(crate) mod release_index;
39pub mod reload;
40pub mod schema_validation;
41pub mod secret_name;
42pub mod secrets;
43pub mod setup_actions;
44pub mod setup_backend_contract;
45pub mod setup_final_actions;
46pub mod setup_input;
47pub mod setup_machine;
48pub mod setup_to_formspec;
49pub mod setup_tunnel;
50pub mod shared_tunnel;
51pub mod tenant_config;
52pub mod webhook;
53
54#[cfg(feature = "ui")]
55pub mod ui;
56
57pub mod qa {
58 pub mod bridge;
61 pub mod persist;
62 pub mod prompts;
63 pub mod shared_questions;
64 pub mod wizard;
65}
66
67pub use bundle_source::BundleSource;
68pub use engine::SetupEngine;
69pub use plan::{SetupMode, SetupPlan, SetupStep, SetupStepKind};
70
71pub use qa::wizard::{
73 ProviderFormSpec, SHARED_QUESTION_IDS, SharedQuestionsResult, build_provider_form_specs,
74 collect_shared_questions, prompt_shared_questions, run_qa_setup_with_shared,
75};
76
77pub fn version() -> &'static str {
79 env!("CARGO_PKG_VERSION")
80}
81
82pub const DEFAULT_ENV_ID: &str = "local";
86
87pub const LEGACY_ENV_ID: &str = "dev";
91
92pub const DISABLE_ALIAS_ENV_VAR: &str = "GREENTIC_DISABLE_DEV_ALIAS";
99
100pub fn resolve_env(override_env: Option<&str>) -> String {
109 let raw = override_env
110 .map(|v| v.to_string())
111 .or_else(|| std::env::var("GREENTIC_ENV").ok())
112 .unwrap_or_else(|| DEFAULT_ENV_ID.to_string());
113 compat_alias::apply_dev_alias(&raw)
114}
115
116mod compat_alias {
117 use std::sync::atomic::{AtomicBool, Ordering};
124
125 use super::{DEFAULT_ENV_ID, DISABLE_ALIAS_ENV_VAR, LEGACY_ENV_ID};
126
127 static WARNED: AtomicBool = AtomicBool::new(false);
128
129 pub fn apply_dev_alias(env: &str) -> String {
134 if env != LEGACY_ENV_ID {
135 return env.to_string();
136 }
137 if alias_disabled() {
138 panic!(
142 "environment `{LEGACY_ENV_ID}` is no longer accepted (set via {DISABLE_ALIAS_ENV_VAR}=1). \
143 Migrate to `{DEFAULT_ENV_ID}` via `gtc op env migrate-dev {DEFAULT_ENV_ID} --check` then `--apply`, \
144 or pass `--env {DEFAULT_ENV_ID}` / unset $GREENTIC_ENV.",
145 );
146 }
147 if !WARNED.swap(true, Ordering::SeqCst) {
148 tracing::warn!(
149 target: "greentic_setup::compat_alias",
150 legacy = LEGACY_ENV_ID,
151 target_env = DEFAULT_ENV_ID,
152 "env `{LEGACY_ENV_ID}` is deprecated; resolving as `{DEFAULT_ENV_ID}` for this process. \
153 Plan the migration with `gtc op env migrate-dev {DEFAULT_ENV_ID} --check`; \
154 set {DISABLE_ALIAS_ENV_VAR}=1 to hard-fail on `{LEGACY_ENV_ID}` in CI.",
155 );
156 }
157 DEFAULT_ENV_ID.to_string()
158 }
159
160 fn alias_disabled() -> bool {
161 std::env::var(DISABLE_ALIAS_ENV_VAR)
162 .ok()
163 .map(|v| {
164 let v = v.trim().to_ascii_lowercase();
165 matches!(v.as_str(), "1" | "true" | "yes" | "on")
166 })
167 .unwrap_or(false)
168 }
169
170 #[cfg(test)]
173 pub(super) fn reset_warning_latch_for_tests() {
174 WARNED.store(false, Ordering::SeqCst);
175 }
176}
177
178pub fn canonical_secret_uri(
187 env: &str,
188 tenant: &str,
189 team: Option<&str>,
190 provider: &str,
191 key: &str,
192) -> String {
193 let team_segment = greentic_secrets_lib::normalize_team(team)
194 .unwrap_or_else(|| greentic_secrets_lib::TEAM_PLACEHOLDER.to_string());
195 let provider_segment = if provider.is_empty() {
200 "messaging".to_string()
201 } else {
202 secret_name::canonical_secret_name(provider)
203 };
204 let normalized_key = secret_name::canonical_secret_name(key);
205 format!("secrets://{env}/{tenant}/{team_segment}/{provider_segment}/{normalized_key}")
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn with_clean_env<R>(body: impl FnOnce() -> R) -> R {
222 let _guard = crate::secrets::test_support::env_lock();
223 let prev_env = std::env::var_os("GREENTIC_ENV");
224 let prev_disable = std::env::var_os(DISABLE_ALIAS_ENV_VAR);
225 unsafe {
229 std::env::remove_var("GREENTIC_ENV");
230 std::env::remove_var(DISABLE_ALIAS_ENV_VAR);
231 }
232 compat_alias::reset_warning_latch_for_tests();
233 let out = body();
234 unsafe {
235 match prev_env {
236 Some(v) => std::env::set_var("GREENTIC_ENV", v),
237 None => std::env::remove_var("GREENTIC_ENV"),
238 }
239 match prev_disable {
240 Some(v) => std::env::set_var(DISABLE_ALIAS_ENV_VAR, v),
241 None => std::env::remove_var(DISABLE_ALIAS_ENV_VAR),
242 }
243 }
244 out
245 }
246
247 #[test]
248 fn version_is_correct() {
249 assert!(version().starts_with("1.2"));
250 }
251
252 #[test]
253 fn secret_uri_basic() {
254 let uri = canonical_secret_uri("dev", "demo", None, "messaging-telegram", "bot_token");
255 assert_eq!(uri, "secrets://dev/demo/_/messaging_telegram/bot_token");
256 }
257
258 #[test]
259 fn secret_uri_with_team() {
260 let uri = canonical_secret_uri("dev", "acme", Some("ops"), "state-redis", "redis_url");
261 assert_eq!(uri, "secrets://dev/acme/ops/state_redis/redis_url");
262 }
263
264 #[test]
265 fn secret_uri_default_team_becomes_wildcard() {
266 let uri = canonical_secret_uri(
267 "dev",
268 "demo",
269 Some("default"),
270 "messaging-slack",
271 "bot_token",
272 );
273 assert_eq!(uri, "secrets://dev/demo/_/messaging_slack/bot_token");
274 }
275
276 #[test]
291 fn webex_secret_uri_contract_do_not_change() {
292 assert_eq!(
295 canonical_secret_uri(
296 "local",
297 "demo",
298 Some("default"),
299 "messaging-webex",
300 "webex_bot_token"
301 ),
302 "secrets://local/demo/_/messaging_webex/webex_bot_token",
303 "setup↔runtime secret-uri contract broke — read the DO-NOT-CHANGE doc above",
304 );
305 }
306
307 #[test]
308 fn secret_uri_normalizes_provider_segment() {
309 let stored = canonical_secret_uri(
313 "dev",
314 "demo",
315 None,
316 "messaging-webchat-gui",
317 "jwt_signing_key",
318 );
319 let fetched = canonical_secret_uri(
320 "dev",
321 "demo",
322 None,
323 "messaging.webchat-gui",
324 "jwt_signing_key",
325 );
326 assert_eq!(stored, fetched);
327 assert_eq!(
328 stored,
329 "secrets://dev/demo/_/messaging_webchat_gui/jwt_signing_key"
330 );
331 }
332
333 #[test]
334 fn resolve_env_returns_local_by_default() {
335 with_clean_env(|| {
336 assert_eq!(resolve_env(None), "local");
337 });
338 }
339
340 #[test]
341 fn resolve_env_passes_through_non_legacy_override() {
342 with_clean_env(|| {
343 assert_eq!(resolve_env(Some("staging")), "staging");
344 assert_eq!(resolve_env(Some("prod")), "prod");
345 assert_eq!(resolve_env(Some("local")), "local");
346 });
347 }
348
349 #[test]
350 fn resolve_env_remaps_dev_override_to_local() {
351 with_clean_env(|| {
352 assert_eq!(resolve_env(Some("dev")), "local");
353 });
354 }
355
356 #[test]
357 fn resolve_env_remaps_dev_env_var_to_local() {
358 with_clean_env(|| {
359 unsafe {
361 std::env::set_var("GREENTIC_ENV", "dev");
362 }
363 assert_eq!(resolve_env(None), "local");
364 });
365 }
366
367 #[test]
368 fn alias_warning_fires_only_once_per_process() {
369 with_clean_env(|| {
373 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
379 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
380 compat_alias::reset_warning_latch_for_tests();
383 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
384 });
385 }
386
387 #[test]
388 fn disable_alias_env_var_panics_on_dev() {
389 with_clean_env(|| {
390 unsafe {
392 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
393 }
394 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
395 assert!(
396 result.is_err(),
397 "resolve_env should panic when alias is disabled and input is `dev`"
398 );
399 });
400 }
401
402 #[test]
403 fn disable_alias_accepts_truthy_strings() {
404 for value in ["1", "true", "TRUE", "yes", "YES", "on", " true "] {
405 with_clean_env(|| {
406 unsafe {
408 std::env::set_var(DISABLE_ALIAS_ENV_VAR, value);
409 }
410 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
411 assert!(
412 result.is_err(),
413 "DISABLE value `{value}` should hard-fail on dev resolution"
414 );
415 });
416 }
417 }
418
419 #[test]
420 fn disable_alias_does_not_panic_on_non_legacy_values() {
421 with_clean_env(|| {
422 unsafe {
424 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
425 }
426 assert_eq!(resolve_env(Some("local")), "local");
429 assert_eq!(resolve_env(Some("staging")), "staging");
430 assert_eq!(resolve_env(None), "local");
431 });
432 }
433}