1pub mod admin;
7pub mod answers_crypto;
8pub mod bundle;
9pub mod bundle_source;
10pub mod capabilities;
11pub mod card_setup;
12pub mod cli_args;
13pub mod cli_commands;
14pub mod cli_helpers;
15pub mod cli_i18n;
16pub mod config_envelope;
17pub mod deployment_targets;
18pub mod discovery;
19pub mod doctor;
20pub mod engine;
21pub mod env_mode;
22pub mod env_wizard;
23pub mod flow;
24pub mod generated_secrets;
25pub mod gtbundle;
26pub mod no_ui_oauth;
27pub mod oauth_callback;
28pub mod oauth_device;
29pub mod plan;
30pub mod platform_setup;
31pub mod provider_state;
32pub mod reload;
33pub mod schema_validation;
34pub mod secret_name;
35pub mod secrets;
36pub mod setup_actions;
37pub mod setup_backend_contract;
38pub mod setup_final_actions;
39pub mod setup_input;
40pub mod setup_machine;
41pub mod setup_to_formspec;
42pub mod setup_tunnel;
43pub mod shared_tunnel;
44pub mod tenant_config;
45pub mod webhook;
46
47#[cfg(feature = "ui")]
48pub mod ui;
49
50pub mod qa {
51 pub mod bridge;
54 pub mod persist;
55 pub mod prompts;
56 pub mod shared_questions;
57 pub mod wizard;
58}
59
60pub use bundle_source::BundleSource;
61pub use engine::SetupEngine;
62pub use plan::{SetupMode, SetupPlan, SetupStep, SetupStepKind};
63
64pub use qa::wizard::{
66 ProviderFormSpec, SHARED_QUESTION_IDS, SharedQuestionsResult, build_provider_form_specs,
67 collect_shared_questions, prompt_shared_questions, run_qa_setup_with_shared,
68};
69
70pub fn version() -> &'static str {
72 env!("CARGO_PKG_VERSION")
73}
74
75pub const DEFAULT_ENV_ID: &str = "local";
79
80pub const LEGACY_ENV_ID: &str = "dev";
84
85pub const DISABLE_ALIAS_ENV_VAR: &str = "GREENTIC_DISABLE_DEV_ALIAS";
92
93pub fn resolve_env(override_env: Option<&str>) -> String {
102 let raw = override_env
103 .map(|v| v.to_string())
104 .or_else(|| std::env::var("GREENTIC_ENV").ok())
105 .unwrap_or_else(|| DEFAULT_ENV_ID.to_string());
106 compat_alias::apply_dev_alias(&raw)
107}
108
109mod compat_alias {
110 use std::sync::atomic::{AtomicBool, Ordering};
117
118 use super::{DEFAULT_ENV_ID, DISABLE_ALIAS_ENV_VAR, LEGACY_ENV_ID};
119
120 static WARNED: AtomicBool = AtomicBool::new(false);
121
122 pub fn apply_dev_alias(env: &str) -> String {
127 if env != LEGACY_ENV_ID {
128 return env.to_string();
129 }
130 if alias_disabled() {
131 panic!(
135 "environment `{LEGACY_ENV_ID}` is no longer accepted (set via {DISABLE_ALIAS_ENV_VAR}=1). \
136 Migrate to `{DEFAULT_ENV_ID}` via `gtc op env migrate-dev {DEFAULT_ENV_ID} --check` then `--apply`, \
137 or pass `--env {DEFAULT_ENV_ID}` / unset $GREENTIC_ENV.",
138 );
139 }
140 if !WARNED.swap(true, Ordering::SeqCst) {
141 tracing::warn!(
142 target: "greentic_setup::compat_alias",
143 legacy = LEGACY_ENV_ID,
144 target_env = DEFAULT_ENV_ID,
145 "env `{LEGACY_ENV_ID}` is deprecated; resolving as `{DEFAULT_ENV_ID}` for this process. \
146 Plan the migration with `gtc op env migrate-dev {DEFAULT_ENV_ID} --check`; \
147 set {DISABLE_ALIAS_ENV_VAR}=1 to hard-fail on `{LEGACY_ENV_ID}` in CI.",
148 );
149 }
150 DEFAULT_ENV_ID.to_string()
151 }
152
153 fn alias_disabled() -> bool {
154 std::env::var(DISABLE_ALIAS_ENV_VAR)
155 .ok()
156 .map(|v| {
157 let v = v.trim().to_ascii_lowercase();
158 matches!(v.as_str(), "1" | "true" | "yes" | "on")
159 })
160 .unwrap_or(false)
161 }
162
163 #[cfg(test)]
166 pub(super) fn reset_warning_latch_for_tests() {
167 WARNED.store(false, Ordering::SeqCst);
168 }
169}
170
171pub fn canonical_secret_uri(
180 env: &str,
181 tenant: &str,
182 team: Option<&str>,
183 provider: &str,
184 key: &str,
185) -> String {
186 let team_segment = greentic_secrets_lib::normalize_team(team)
187 .unwrap_or_else(|| greentic_secrets_lib::TEAM_PLACEHOLDER.to_string());
188 let provider_segment = if provider.is_empty() {
193 "messaging".to_string()
194 } else {
195 secret_name::canonical_secret_name(provider)
196 };
197 let normalized_key = secret_name::canonical_secret_name(key);
198 format!("secrets://{env}/{tenant}/{team_segment}/{provider_segment}/{normalized_key}")
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use std::sync::Mutex;
205
206 static ENV_LOCK: Mutex<()> = Mutex::new(());
210
211 fn with_clean_env<R>(body: impl FnOnce() -> R) -> R {
212 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
213 let prev_env = std::env::var_os("GREENTIC_ENV");
214 let prev_disable = std::env::var_os(DISABLE_ALIAS_ENV_VAR);
215 unsafe {
219 std::env::remove_var("GREENTIC_ENV");
220 std::env::remove_var(DISABLE_ALIAS_ENV_VAR);
221 }
222 compat_alias::reset_warning_latch_for_tests();
223 let out = body();
224 unsafe {
225 match prev_env {
226 Some(v) => std::env::set_var("GREENTIC_ENV", v),
227 None => std::env::remove_var("GREENTIC_ENV"),
228 }
229 match prev_disable {
230 Some(v) => std::env::set_var(DISABLE_ALIAS_ENV_VAR, v),
231 None => std::env::remove_var(DISABLE_ALIAS_ENV_VAR),
232 }
233 }
234 out
235 }
236
237 #[test]
238 fn version_is_correct() {
239 assert!(version().starts_with("1.2"));
240 }
241
242 #[test]
243 fn secret_uri_basic() {
244 let uri = canonical_secret_uri("dev", "demo", None, "messaging-telegram", "bot_token");
245 assert_eq!(uri, "secrets://dev/demo/_/messaging_telegram/bot_token");
246 }
247
248 #[test]
249 fn secret_uri_with_team() {
250 let uri = canonical_secret_uri("dev", "acme", Some("ops"), "state-redis", "redis_url");
251 assert_eq!(uri, "secrets://dev/acme/ops/state_redis/redis_url");
252 }
253
254 #[test]
255 fn secret_uri_default_team_becomes_wildcard() {
256 let uri = canonical_secret_uri(
257 "dev",
258 "demo",
259 Some("default"),
260 "messaging-slack",
261 "bot_token",
262 );
263 assert_eq!(uri, "secrets://dev/demo/_/messaging_slack/bot_token");
264 }
265
266 #[test]
267 fn secret_uri_normalizes_provider_segment() {
268 let stored = canonical_secret_uri(
272 "dev",
273 "demo",
274 None,
275 "messaging-webchat-gui",
276 "jwt_signing_key",
277 );
278 let fetched = canonical_secret_uri(
279 "dev",
280 "demo",
281 None,
282 "messaging.webchat-gui",
283 "jwt_signing_key",
284 );
285 assert_eq!(stored, fetched);
286 assert_eq!(
287 stored,
288 "secrets://dev/demo/_/messaging_webchat_gui/jwt_signing_key"
289 );
290 }
291
292 #[test]
293 fn resolve_env_returns_local_by_default() {
294 with_clean_env(|| {
295 assert_eq!(resolve_env(None), "local");
296 });
297 }
298
299 #[test]
300 fn resolve_env_passes_through_non_legacy_override() {
301 with_clean_env(|| {
302 assert_eq!(resolve_env(Some("staging")), "staging");
303 assert_eq!(resolve_env(Some("prod")), "prod");
304 assert_eq!(resolve_env(Some("local")), "local");
305 });
306 }
307
308 #[test]
309 fn resolve_env_remaps_dev_override_to_local() {
310 with_clean_env(|| {
311 assert_eq!(resolve_env(Some("dev")), "local");
312 });
313 }
314
315 #[test]
316 fn resolve_env_remaps_dev_env_var_to_local() {
317 with_clean_env(|| {
318 unsafe {
320 std::env::set_var("GREENTIC_ENV", "dev");
321 }
322 assert_eq!(resolve_env(None), "local");
323 });
324 }
325
326 #[test]
327 fn alias_warning_fires_only_once_per_process() {
328 with_clean_env(|| {
332 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
338 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
339 compat_alias::reset_warning_latch_for_tests();
342 assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
343 });
344 }
345
346 #[test]
347 fn disable_alias_env_var_panics_on_dev() {
348 with_clean_env(|| {
349 unsafe {
351 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
352 }
353 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
354 assert!(
355 result.is_err(),
356 "resolve_env should panic when alias is disabled and input is `dev`"
357 );
358 });
359 }
360
361 #[test]
362 fn disable_alias_accepts_truthy_strings() {
363 for value in ["1", "true", "TRUE", "yes", "YES", "on", " true "] {
364 with_clean_env(|| {
365 unsafe {
367 std::env::set_var(DISABLE_ALIAS_ENV_VAR, value);
368 }
369 let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
370 assert!(
371 result.is_err(),
372 "DISABLE value `{value}` should hard-fail on dev resolution"
373 );
374 });
375 }
376 }
377
378 #[test]
379 fn disable_alias_does_not_panic_on_non_legacy_values() {
380 with_clean_env(|| {
381 unsafe {
383 std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
384 }
385 assert_eq!(resolve_env(Some("local")), "local");
388 assert_eq!(resolve_env(Some("staging")), "staging");
389 assert_eq!(resolve_env(None), "local");
390 });
391 }
392}