@opencode-cloud/core 1.0.8 → 3.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ //! Configuration validation with actionable error messages
2
+ //!
3
+ //! Validates the configuration and provides exact commands to fix issues.
4
+
5
+ use super::schema::{Config, validate_bind_address};
6
+ use console::style;
7
+
8
+ /// A configuration validation error with an actionable fix command
9
+ #[derive(Debug, Clone)]
10
+ pub struct ValidationError {
11
+ /// The config field that has an error
12
+ pub field: String,
13
+ /// Description of what's wrong
14
+ pub message: String,
15
+ /// Exact occ command to fix the issue
16
+ pub fix_command: String,
17
+ }
18
+
19
+ /// A configuration validation warning (non-fatal)
20
+ #[derive(Debug, Clone)]
21
+ pub struct ValidationWarning {
22
+ /// The config field with a potential issue
23
+ pub field: String,
24
+ /// Description of the warning
25
+ pub message: String,
26
+ /// Suggested occ command to address the warning
27
+ pub fix_command: String,
28
+ }
29
+
30
+ /// Validate configuration and return warnings or first error
31
+ ///
32
+ /// Returns Ok(warnings) if validation passes (possibly with non-fatal warnings).
33
+ /// Returns Err(error) on the first fatal validation error encountered.
34
+ ///
35
+ /// Validation is performed in order, stopping at the first error.
36
+ pub fn validate_config(config: &Config) -> Result<Vec<ValidationWarning>, ValidationError> {
37
+ let mut warnings = Vec::new();
38
+
39
+ // Port validation
40
+ if config.opencode_web_port < 1024 {
41
+ return Err(ValidationError {
42
+ field: "opencode_web_port".to_string(),
43
+ message: "Port must be >= 1024 (non-privileged)".to_string(),
44
+ fix_command: "occ config set opencode_web_port 3000".to_string(),
45
+ });
46
+ }
47
+ // Note: No need to check > 65535 - u16 type enforces this limit
48
+
49
+ // Bind address validation
50
+ if let Err(msg) = validate_bind_address(&config.bind_address) {
51
+ return Err(ValidationError {
52
+ field: "bind_address".to_string(),
53
+ message: msg,
54
+ fix_command: "occ config set bind_address 127.0.0.1".to_string(),
55
+ });
56
+ }
57
+
58
+ // Boot mode validation
59
+ if config.boot_mode != "user" && config.boot_mode != "system" {
60
+ return Err(ValidationError {
61
+ field: "boot_mode".to_string(),
62
+ message: "boot_mode must be 'user' or 'system'".to_string(),
63
+ fix_command: "occ config set boot_mode user".to_string(),
64
+ });
65
+ }
66
+
67
+ // Rate limit validation
68
+ if config.rate_limit_attempts == 0 {
69
+ return Err(ValidationError {
70
+ field: "rate_limit_attempts".to_string(),
71
+ message: "rate_limit_attempts must be > 0".to_string(),
72
+ fix_command: "occ config set rate_limit_attempts 5".to_string(),
73
+ });
74
+ }
75
+
76
+ if config.rate_limit_window_seconds == 0 {
77
+ return Err(ValidationError {
78
+ field: "rate_limit_window_seconds".to_string(),
79
+ message: "rate_limit_window_seconds must be > 0".to_string(),
80
+ fix_command: "occ config set rate_limit_window_seconds 60".to_string(),
81
+ });
82
+ }
83
+
84
+ // Warnings (non-fatal)
85
+
86
+ // Network exposure without auth
87
+ if config.is_network_exposed()
88
+ && config.users.is_empty()
89
+ && !config.allow_unauthenticated_network
90
+ {
91
+ warnings.push(ValidationWarning {
92
+ field: "bind_address".to_string(),
93
+ message: "Network exposed without authentication".to_string(),
94
+ fix_command: "occ user add".to_string(),
95
+ });
96
+ }
97
+
98
+ // Legacy auth fields present
99
+ if let Some(ref username) = config.auth_username {
100
+ if !username.is_empty() {
101
+ warnings.push(ValidationWarning {
102
+ field: "auth_username".to_string(),
103
+ message: "Legacy auth fields present; consider using 'occ user add' instead"
104
+ .to_string(),
105
+ fix_command: "occ config set auth_username ''".to_string(),
106
+ });
107
+ }
108
+ }
109
+
110
+ if let Some(ref password) = config.auth_password {
111
+ if !password.is_empty() {
112
+ warnings.push(ValidationWarning {
113
+ field: "auth_password".to_string(),
114
+ message: "Legacy auth fields present; consider using 'occ user add' instead"
115
+ .to_string(),
116
+ fix_command: "occ config set auth_password ''".to_string(),
117
+ });
118
+ }
119
+ }
120
+
121
+ Ok(warnings)
122
+ }
123
+
124
+ /// Display a validation error with styled formatting
125
+ pub fn display_validation_error(error: &ValidationError) {
126
+ eprintln!();
127
+ eprintln!("{}", style("Error: Configuration error").red().bold());
128
+ eprintln!();
129
+ eprintln!(" {} {}", style("Field:").dim(), error.field);
130
+ eprintln!(" {} {}", style("Problem:").dim(), error.message);
131
+ eprintln!();
132
+ eprintln!("{}:", style("To fix, run").dim());
133
+ eprintln!(" {}", style(&error.fix_command).cyan());
134
+ eprintln!();
135
+ }
136
+
137
+ /// Display a validation warning with styled formatting
138
+ pub fn display_validation_warning(warning: &ValidationWarning) {
139
+ eprintln!();
140
+ eprintln!(
141
+ "{}",
142
+ style("Warning: Configuration warning").yellow().bold()
143
+ );
144
+ eprintln!();
145
+ eprintln!(" {} {}", style("Field:").dim(), warning.field);
146
+ eprintln!(" {} {}", style("Issue:").dim(), warning.message);
147
+ eprintln!();
148
+ eprintln!("{}:", style("To address, run").dim());
149
+ eprintln!(" {}", style(&warning.fix_command).cyan());
150
+ eprintln!();
151
+ }
152
+
153
+ #[cfg(test)]
154
+ mod tests {
155
+ use super::*;
156
+
157
+ #[test]
158
+ fn test_valid_config_passes() {
159
+ let config = Config::default();
160
+ let result = validate_config(&config);
161
+ assert!(result.is_ok());
162
+ }
163
+
164
+ #[test]
165
+ fn test_port_too_low() {
166
+ let config = Config {
167
+ opencode_web_port: 80,
168
+ ..Config::default()
169
+ };
170
+ let result = validate_config(&config);
171
+ assert!(result.is_err());
172
+ let err = result.unwrap_err();
173
+ assert_eq!(err.field, "opencode_web_port");
174
+ assert!(err.message.contains("1024"));
175
+ }
176
+
177
+ // Note: No test for port > 65535 - u16 type enforces this limit at compile time
178
+
179
+ #[test]
180
+ fn test_invalid_bind_address() {
181
+ let config = Config {
182
+ bind_address: "not-an-ip".to_string(),
183
+ ..Config::default()
184
+ };
185
+ let result = validate_config(&config);
186
+ assert!(result.is_err());
187
+ let err = result.unwrap_err();
188
+ assert_eq!(err.field, "bind_address");
189
+ }
190
+
191
+ #[test]
192
+ fn test_invalid_boot_mode() {
193
+ let config = Config {
194
+ boot_mode: "invalid".to_string(),
195
+ ..Config::default()
196
+ };
197
+ let result = validate_config(&config);
198
+ assert!(result.is_err());
199
+ let err = result.unwrap_err();
200
+ assert_eq!(err.field, "boot_mode");
201
+ }
202
+
203
+ #[test]
204
+ fn test_rate_limit_attempts_zero() {
205
+ let config = Config {
206
+ rate_limit_attempts: 0,
207
+ ..Config::default()
208
+ };
209
+ let result = validate_config(&config);
210
+ assert!(result.is_err());
211
+ let err = result.unwrap_err();
212
+ assert_eq!(err.field, "rate_limit_attempts");
213
+ }
214
+
215
+ #[test]
216
+ fn test_rate_limit_window_zero() {
217
+ let config = Config {
218
+ rate_limit_window_seconds: 0,
219
+ ..Config::default()
220
+ };
221
+ let result = validate_config(&config);
222
+ assert!(result.is_err());
223
+ let err = result.unwrap_err();
224
+ assert_eq!(err.field, "rate_limit_window_seconds");
225
+ }
226
+
227
+ #[test]
228
+ fn test_network_exposed_without_auth_warning() {
229
+ let config = Config {
230
+ bind_address: "0.0.0.0".to_string(),
231
+ users: Vec::new(),
232
+ allow_unauthenticated_network: false,
233
+ ..Config::default()
234
+ };
235
+ let result = validate_config(&config);
236
+ assert!(result.is_ok());
237
+ let warnings = result.unwrap();
238
+ assert!(!warnings.is_empty());
239
+ assert!(
240
+ warnings
241
+ .iter()
242
+ .any(|w| w.message.contains("Network exposed"))
243
+ );
244
+ }
245
+
246
+ #[test]
247
+ fn test_legacy_auth_username_warning() {
248
+ let config = Config {
249
+ auth_username: Some("admin".to_string()),
250
+ ..Config::default()
251
+ };
252
+ let result = validate_config(&config);
253
+ assert!(result.is_ok());
254
+ let warnings = result.unwrap();
255
+ assert!(!warnings.is_empty());
256
+ assert!(warnings.iter().any(|w| w.field == "auth_username"));
257
+ }
258
+
259
+ #[test]
260
+ fn test_legacy_auth_password_warning() {
261
+ let config = Config {
262
+ auth_password: Some("secret".to_string()),
263
+ ..Config::default()
264
+ };
265
+ let result = validate_config(&config);
266
+ assert!(result.is_ok());
267
+ let warnings = result.unwrap();
268
+ assert!(!warnings.is_empty());
269
+ assert!(warnings.iter().any(|w| w.field == "auth_password"));
270
+ }
271
+ }