1#[cfg(not(target_arch = "wasm32"))]
33pub mod cli;
34#[cfg(not(target_arch = "wasm32"))]
35mod logging;
36#[cfg(target_arch = "wasm32")]
37mod logging_wasm;
38pub mod secrets;
39
40#[cfg(not(target_arch = "wasm32"))]
41pub use cli::MaArgs;
42pub use secrets::SecretBundle;
43
44#[cfg(target_arch = "wasm32")]
45use std::path::PathBuf;
46#[cfg(not(target_arch = "wasm32"))]
47use std::path::{Path, PathBuf};
48
49use crate::error::{Error, Result};
50use base64::engine::general_purpose::STANDARD as B64;
51use base64::Engine;
52use serde::{Deserialize, Serialize};
53
54const DEFAULT_LOG_LEVEL: &str = "info";
57const DEFAULT_LOG_LEVEL_STDOUT: &str = "info";
58const DEFAULT_DID_RESOLVER_POSITIVE_TTL_SECS: u64 = 60;
59const DEFAULT_DID_RESOLVER_NEGATIVE_TTL_SECS: u64 = 10;
60#[cfg(not(target_arch = "wasm32"))]
61const DEFAULT_KUBO_RPC_URL: &str = "http://127.0.0.1:5001";
62
63#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct RemotePinConfig {
67 pub service: String,
68 pub name: String,
69}
70
71#[derive(Debug, Clone)]
78pub struct Config {
79 pub slug: String,
82
83 pub log_level: String,
85
86 pub log_level_stdout: String,
88
89 pub did_resolver_positive_ttl_secs: u64,
92
93 pub did_resolver_negative_ttl_secs: u64,
96
97 pub log_file: Option<PathBuf>,
100
101 #[cfg(not(target_arch = "wasm32"))]
102 pub kubo_rpc_url: String,
104
105 #[cfg(not(target_arch = "wasm32"))]
106 pub kubo_key_alias: String,
108
109 pub secret_bundle: Option<PathBuf>,
111
112 pub secret_bundle_passphrase: Option<String>,
115
116 pub config_path: Option<PathBuf>,
118
119 pub pin_remote: bool,
121
122 pub pin_remote_service: Option<String>,
124
125 pub pin_remote_name: Option<String>,
127
128 pub extra: serde_yaml::Mapping,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct BrowserIdentityExport {
139 pub version: u8,
140 pub config_yaml: String,
141 pub encrypted_secret_bundle_base64: String,
142}
143
144impl BrowserIdentityExport {
145 pub fn new(config_yaml: String, encrypted_secret_bundle: &[u8]) -> Self {
146 Self {
147 version: 1,
148 config_yaml,
149 encrypted_secret_bundle_base64: B64.encode(encrypted_secret_bundle),
150 }
151 }
152
153 pub fn encrypted_secret_bundle_bytes(&self) -> Result<Vec<u8>> {
154 B64.decode(self.encrypted_secret_bundle_base64.as_bytes())
155 .map_err(|e| Error::Config(format!("invalid encrypted bundle base64: {e}")))
156 }
157
158 pub fn to_json_string(&self) -> Result<String> {
159 serde_json::to_string(self)
160 .map_err(|e| Error::Config(format!("failed to serialize browser export: {e}")))
161 }
162
163 pub fn from_json_str(json: &str) -> Result<Self> {
164 serde_json::from_str(json)
165 .map_err(|e| Error::Config(format!("failed to parse browser export JSON: {e}")))
166 }
167}
168
169#[cfg(not(target_arch = "wasm32"))]
172fn project_dirs() -> Result<directories::ProjectDirs> {
173 directories::ProjectDirs::from("", "ma", "ma")
174 .ok_or_else(|| Error::Config("cannot determine XDG base directories".to_string()))
175}
176
177#[cfg(not(target_arch = "wasm32"))]
179pub fn default_config_path(slug: &str) -> Result<PathBuf> {
180 Ok(project_dirs()?.config_dir().join(format!("{slug}.yaml")))
181}
182
183#[cfg(not(target_arch = "wasm32"))]
185pub fn default_secret_bundle_path(slug: &str) -> Result<PathBuf> {
186 Ok(project_dirs()?.config_dir().join(format!("{slug}.bin")))
187}
188
189#[cfg(not(target_arch = "wasm32"))]
191pub fn default_log_file_path(slug: &str) -> Result<PathBuf> {
192 Ok(project_dirs()?.data_dir().join(format!("{slug}.log")))
193}
194
195#[cfg(not(target_arch = "wasm32"))]
202pub(crate) fn write_secure(path: &Path, data: &[u8]) -> Result<()> {
203 use std::io::Write;
204
205 if let Some(parent) = path.parent() {
206 std::fs::create_dir_all(parent).map_err(|e| {
207 Error::Config(format!("failed to create dir {}: {e}", parent.display()))
208 })?;
209 }
210
211 #[cfg(unix)]
212 let mut file = {
213 use std::os::unix::fs::OpenOptionsExt;
214 std::fs::OpenOptions::new()
215 .write(true)
216 .create(true)
217 .truncate(true)
218 .mode(0o600)
219 .open(path)
220 .map_err(|e| Error::Config(format!("failed to open {}: {e}", path.display())))?
221 };
222
223 #[cfg(not(unix))]
224 let mut file = std::fs::OpenOptions::new()
225 .write(true)
226 .create(true)
227 .truncate(true)
228 .open(path)
229 .map_err(|e| Error::Config(format!("failed to open {}: {e}", path.display())))?;
230
231 file.write_all(data)
232 .map_err(|e| Error::Config(format!("failed to write {}: {e}", path.display())))?;
233
234 #[cfg(unix)]
237 {
238 use std::os::unix::fs::PermissionsExt;
239 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(|e| {
240 Error::Config(format!(
241 "failed to set permissions on {}: {e}",
242 path.display()
243 ))
244 })?;
245 }
246
247 Ok(())
248}
249
250#[cfg(all(not(target_arch = "wasm32"), unix))]
253fn check_permissions(path: &Path) {
254 use std::os::unix::fs::MetadataExt;
255 if let Ok(meta) = std::fs::metadata(path) {
256 let mode = meta.mode() & 0o777;
257 if mode > 0o600 {
258 tracing::warn!(
259 path = %path.display(),
260 mode = format!("{mode:04o}"),
261 "config file has permissions wider than 0600 — consider `chmod 0600 {}`",
262 path.display()
263 );
264 }
265 }
266}
267
268#[cfg(all(not(target_arch = "wasm32"), not(unix)))]
269fn check_permissions(_path: &Path) {}
270
271#[cfg(not(target_arch = "wasm32"))]
274fn load_yaml_mapping(path: &Path) -> Result<serde_yaml::Mapping> {
275 let content = std::fs::read_to_string(path)
276 .map_err(|e| Error::Config(format!("failed to read {}: {e}", path.display())))?;
277 let val: serde_yaml::Value = serde_yaml::from_str(&content)
278 .map_err(|e| Error::Config(format!("invalid YAML in {}: {e}", path.display())))?;
279 if let serde_yaml::Value::Mapping(m) = val {
280 Ok(m)
281 } else {
282 Err(Error::Config(format!(
283 "config file {} must be a YAML mapping",
284 path.display()
285 )))
286 }
287}
288
289#[cfg(not(target_arch = "wasm32"))]
290fn yaml_str(m: &serde_yaml::Mapping, key: &str) -> Option<String> {
291 m.get(serde_yaml::Value::String(key.to_string()))
292 .and_then(|v| v.as_str())
293 .map(String::from)
294}
295
296#[cfg(not(target_arch = "wasm32"))]
297fn yaml_path(m: &serde_yaml::Mapping, key: &str) -> Option<PathBuf> {
298 m.get(serde_yaml::Value::String(key.to_string()))
299 .and_then(|v| v.as_str())
300 .map(PathBuf::from)
301}
302
303#[cfg(not(target_arch = "wasm32"))]
304fn yaml_u64(m: &serde_yaml::Mapping, key: &str) -> Option<u64> {
305 m.get(serde_yaml::Value::String(key.to_string()))
306 .and_then(|v| match v {
307 serde_yaml::Value::Number(n) => n.as_u64(),
308 serde_yaml::Value::String(s) => s.parse::<u64>().ok(),
309 _ => None,
310 })
311}
312
313#[cfg(not(target_arch = "wasm32"))]
314fn yaml_bool(m: &serde_yaml::Mapping, key: &str) -> Option<bool> {
315 m.get(serde_yaml::Value::String(key.to_string()))
316 .and_then(|v| match v {
317 serde_yaml::Value::Bool(b) => Some(*b),
318 serde_yaml::Value::String(s) => s.parse::<bool>().ok(),
319 _ => None,
320 })
321}
322
323impl Config {
326 pub fn new_for_storage(slug: impl AsRef<str>) -> Self {
330 let slug = slug.as_ref().to_string();
331 Self {
332 slug: slug.clone(),
333 log_level: DEFAULT_LOG_LEVEL.to_string(),
334 log_level_stdout: DEFAULT_LOG_LEVEL_STDOUT.to_string(),
335 did_resolver_positive_ttl_secs: DEFAULT_DID_RESOLVER_POSITIVE_TTL_SECS,
336 did_resolver_negative_ttl_secs: DEFAULT_DID_RESOLVER_NEGATIVE_TTL_SECS,
337 log_file: None,
338 #[cfg(not(target_arch = "wasm32"))]
339 kubo_rpc_url: DEFAULT_KUBO_RPC_URL.to_string(),
340 #[cfg(not(target_arch = "wasm32"))]
341 kubo_key_alias: slug,
342 secret_bundle: None,
343 secret_bundle_passphrase: None,
344 config_path: None,
345 pin_remote: false,
346 pin_remote_service: None,
347 pin_remote_name: None,
348 extra: serde_yaml::Mapping::new(),
349 }
350 }
351
352 pub fn from_yaml_str(yaml_text: &str) -> Result<Self> {
354 let val: serde_yaml::Value = serde_yaml::from_str(yaml_text)
355 .map_err(|e| Error::Config(format!("failed to parse config YAML: {e}")))?;
356 let mut m = match val {
357 serde_yaml::Value::Mapping(m) => m,
358 _ => {
359 return Err(Error::Config(
360 "config YAML must be a mapping at the top level".to_string(),
361 ));
362 }
363 };
364
365 let take_str = |map: &mut serde_yaml::Mapping, key: &str| {
366 map.remove(serde_yaml::Value::String(key.to_string()))
367 .and_then(|v| v.as_str().map(ToOwned::to_owned))
368 };
369
370 let take_path = |map: &mut serde_yaml::Mapping, key: &str| {
371 map.remove(serde_yaml::Value::String(key.to_string()))
372 .and_then(|v| v.as_str().map(PathBuf::from))
373 };
374
375 let take_u64 = |map: &mut serde_yaml::Mapping, key: &str| {
376 map.remove(serde_yaml::Value::String(key.to_string()))
377 .and_then(|v| match v {
378 serde_yaml::Value::Number(n) => n.as_u64(),
379 serde_yaml::Value::String(s) => s.parse::<u64>().ok(),
380 _ => None,
381 })
382 };
383
384 let take_bool = |map: &mut serde_yaml::Mapping, key: &str| {
385 map.remove(serde_yaml::Value::String(key.to_string()))
386 .and_then(|v| match v {
387 serde_yaml::Value::Bool(b) => Some(b),
388 serde_yaml::Value::String(s) => s.parse::<bool>().ok(),
389 _ => None,
390 })
391 };
392
393 let slug = take_str(&mut m, "slug").unwrap_or_else(|| "ma".to_string());
394 let log_level =
395 take_str(&mut m, "log_level").unwrap_or_else(|| DEFAULT_LOG_LEVEL.to_string());
396 let log_level_stdout = take_str(&mut m, "log_level_stdout")
397 .unwrap_or_else(|| DEFAULT_LOG_LEVEL_STDOUT.to_string());
398 let did_resolver_positive_ttl_secs = take_u64(&mut m, "did_resolver_positive_ttl_secs")
399 .unwrap_or(DEFAULT_DID_RESOLVER_POSITIVE_TTL_SECS);
400 let did_resolver_negative_ttl_secs = take_u64(&mut m, "did_resolver_negative_ttl_secs")
401 .unwrap_or(DEFAULT_DID_RESOLVER_NEGATIVE_TTL_SECS);
402 let _ignored_config_path = take_path(&mut m, "config_path");
404 #[cfg(not(target_arch = "wasm32"))]
405 let kubo_rpc_url =
406 take_str(&mut m, "kubo_rpc_url").unwrap_or_else(|| DEFAULT_KUBO_RPC_URL.to_string());
407 #[cfg(not(target_arch = "wasm32"))]
408 let kubo_key_alias = take_str(&mut m, "kubo_key_alias").unwrap_or_else(|| slug.clone());
409
410 Ok(Self {
411 slug,
412 log_level,
413 log_level_stdout,
414 did_resolver_positive_ttl_secs,
415 did_resolver_negative_ttl_secs,
416 log_file: take_path(&mut m, "log_file"),
417 #[cfg(not(target_arch = "wasm32"))]
418 kubo_rpc_url,
419 #[cfg(not(target_arch = "wasm32"))]
420 kubo_key_alias,
421 secret_bundle: take_path(&mut m, "secret_bundle"),
422 secret_bundle_passphrase: take_str(&mut m, "secret_bundle_passphrase"),
423 config_path: None,
424 pin_remote: take_bool(&mut m, "pin_remote").unwrap_or(false),
425 pin_remote_service: take_str(&mut m, "pin_remote_service"),
426 pin_remote_name: take_str(&mut m, "pin_remote_name"),
427 extra: m,
428 })
429 }
430
431 pub fn to_yaml_string(&self) -> Result<String> {
433 let mut m = self.extra.clone();
434
435 let mut set = |k: &str, v: serde_yaml::Value| {
436 m.insert(serde_yaml::Value::String(k.to_string()), v);
437 };
438
439 set(
443 "log_level",
444 serde_yaml::Value::String(self.log_level.clone()),
445 );
446 set(
447 "log_level_stdout",
448 serde_yaml::Value::String(self.log_level_stdout.clone()),
449 );
450 set(
451 "did_resolver_positive_ttl_secs",
452 serde_yaml::Value::Number(serde_yaml::Number::from(
453 self.did_resolver_positive_ttl_secs,
454 )),
455 );
456 set(
457 "did_resolver_negative_ttl_secs",
458 serde_yaml::Value::Number(serde_yaml::Number::from(
459 self.did_resolver_negative_ttl_secs,
460 )),
461 );
462 #[cfg(not(target_arch = "wasm32"))]
463 set(
464 "kubo_rpc_url",
465 serde_yaml::Value::String(self.kubo_rpc_url.clone()),
466 );
467 #[cfg(not(target_arch = "wasm32"))]
468 set(
469 "kubo_key_alias",
470 serde_yaml::Value::String(self.kubo_key_alias.clone()),
471 );
472
473 if let Some(ref p) = self.log_file {
474 set(
475 "log_file",
476 serde_yaml::Value::String(p.to_string_lossy().into_owned()),
477 );
478 }
479 if let Some(ref p) = self.secret_bundle {
480 set(
481 "secret_bundle",
482 serde_yaml::Value::String(p.to_string_lossy().into_owned()),
483 );
484 }
485 if let Some(ref pw) = self.secret_bundle_passphrase {
486 set(
487 "secret_bundle_passphrase",
488 serde_yaml::Value::String(pw.clone()),
489 );
490 }
491 set("pin_remote", serde_yaml::Value::Bool(self.pin_remote));
492 if let Some(ref service) = self.pin_remote_service {
493 set(
494 "pin_remote_service",
495 serde_yaml::Value::String(service.clone()),
496 );
497 }
498 if let Some(ref name) = self.pin_remote_name {
499 set("pin_remote_name", serde_yaml::Value::String(name.clone()));
500 }
501
502 serde_yaml::to_string(&serde_yaml::Value::Mapping(m))
503 .map_err(|e| Error::Config(format!("failed to serialize config: {e}")))
504 }
505
506 pub fn to_yaml_string_without_passphrase(&self) -> Result<String> {
511 let mut copy = self.clone();
512 copy.secret_bundle_passphrase = None;
513 copy.to_yaml_string()
514 }
515
516 #[cfg(not(target_arch = "wasm32"))]
517 #[allow(clippy::too_many_lines)]
545 pub fn from_args(args: &MaArgs, default_slug: &'static str) -> Result<Self> {
546 let slug = args
548 .slug
549 .clone()
550 .unwrap_or_else(|| default_slug.to_string());
551
552 let config_path = if let Some(ref p) = args.config {
554 p.clone()
555 } else {
556 default_config_path(&slug)?
557 };
558
559 let yaml = if config_path.exists() {
561 check_permissions(&config_path);
562 Some(load_yaml_mapping(&config_path)?)
563 } else {
564 None
565 };
566
567 let resolve_str = |cli: Option<String>, env_key: &str, default: &str| -> String {
571 cli.or_else(|| std::env::var(format!("MA_{env_key}")).ok())
572 .or_else(|| {
573 yaml.as_ref()
574 .and_then(|m| yaml_str(m, &env_key.to_lowercase()))
575 })
576 .unwrap_or_else(|| default.to_string())
577 };
578
579 let resolve_opt_str = |cli: Option<String>, env_key: &str| -> Option<String> {
580 cli.or_else(|| std::env::var(format!("MA_{env_key}")).ok())
581 .or_else(|| {
582 yaml.as_ref()
583 .and_then(|m| yaml_str(m, &env_key.to_lowercase()))
584 })
585 };
586
587 let resolve_opt_path = |cli: Option<PathBuf>, env_key: &str| -> Option<PathBuf> {
588 cli.or_else(|| {
589 std::env::var(format!("MA_{env_key}"))
590 .ok()
591 .map(PathBuf::from)
592 })
593 .or_else(|| {
594 yaml.as_ref()
595 .and_then(|m| yaml_path(m, &env_key.to_lowercase()))
596 })
597 };
598
599 let resolve_u64 = |cli: Option<u64>, env_key: &str, default: u64| -> u64 {
600 cli.or_else(|| {
601 std::env::var(format!("MA_{env_key}"))
602 .ok()
603 .and_then(|v| v.parse::<u64>().ok())
604 })
605 .or_else(|| {
606 yaml.as_ref()
607 .and_then(|m| yaml_u64(m, &env_key.to_lowercase()))
608 })
609 .unwrap_or(default)
610 };
611
612 let resolve_bool = |cli: Option<bool>, env_key: &str, default: bool| -> bool {
613 cli.or_else(|| {
614 std::env::var(format!("MA_{env_key}"))
615 .ok()
616 .and_then(|v| v.parse::<bool>().ok())
617 })
618 .or_else(|| {
619 yaml.as_ref()
620 .and_then(|m| yaml_bool(m, &env_key.to_lowercase()))
621 })
622 .unwrap_or(default)
623 };
624
625 let log_level = resolve_str(args.log_level.clone(), "LOG_LEVEL", DEFAULT_LOG_LEVEL);
626 let log_level_stdout = resolve_str(
627 args.log_level_stdout.clone(),
628 "LOG_LEVEL_STDOUT",
629 DEFAULT_LOG_LEVEL_STDOUT,
630 );
631 let log_file = resolve_opt_path(args.log_file.clone(), "LOG_FILE");
632 let did_resolver_positive_ttl_secs = resolve_u64(
633 args.did_resolver_positive_ttl_secs,
634 "DID_RESOLVER_POSITIVE_TTL_SECS",
635 DEFAULT_DID_RESOLVER_POSITIVE_TTL_SECS,
636 );
637 let did_resolver_negative_ttl_secs = resolve_u64(
638 args.did_resolver_negative_ttl_secs,
639 "DID_RESOLVER_NEGATIVE_TTL_SECS",
640 DEFAULT_DID_RESOLVER_NEGATIVE_TTL_SECS,
641 );
642 let kubo_rpc_url = resolve_str(
643 args.kubo_rpc_url.clone(),
644 "KUBO_RPC_URL",
645 DEFAULT_KUBO_RPC_URL,
646 );
647 let kubo_key_alias =
648 resolve_str(args.kubo_key_alias.clone(), "KUBO_KEY_ALIAS", &slug.clone());
649 let secret_bundle = resolve_opt_path(args.secret_bundle.clone(), "SECRET_BUNDLE");
650 let secret_bundle_passphrase = resolve_opt_str(
651 args.secret_bundle_passphrase.clone(),
652 "SECRET_BUNDLE_PASSPHRASE",
653 );
654 let pin_remote = resolve_bool(args.pin_remote, "PIN_REMOTE", false);
655 let pin_remote_service =
656 resolve_opt_str(args.pin_remote_service.clone(), "PIN_REMOTE_SERVICE");
657 let pin_remote_name = resolve_opt_str(args.pin_remote_name.clone(), "PIN_REMOTE_NAME");
658
659 let known: &[&str] = &[
661 "slug",
662 "log_level",
663 "log_level_stdout",
664 "log_file",
665 "did_resolver_positive_ttl_secs",
666 "did_resolver_negative_ttl_secs",
667 "kubo_rpc_url",
668 "kubo_key_alias",
669 "secret_bundle",
670 "secret_bundle_passphrase",
671 "pin_remote",
672 "pin_remote_service",
673 "pin_remote_name",
674 "config_path",
676 ];
677 let extra = yaml
678 .map(|mut m| {
679 for k in known {
680 m.remove(serde_yaml::Value::String((*k).to_string()));
681 }
682 m
683 })
684 .unwrap_or_default();
685
686 Ok(Config {
687 slug,
688 log_level,
689 log_level_stdout,
690 did_resolver_positive_ttl_secs,
691 did_resolver_negative_ttl_secs,
692 log_file,
693 #[cfg(not(target_arch = "wasm32"))]
694 kubo_rpc_url,
695 #[cfg(not(target_arch = "wasm32"))]
696 kubo_key_alias,
697 secret_bundle,
698 secret_bundle_passphrase,
699 config_path: Some(config_path),
700 pin_remote,
701 pin_remote_service,
702 pin_remote_name,
703 extra,
704 })
705 }
706
707 pub fn remote_pin_config_with_default_name(
710 &self,
711 default_name: impl Into<String>,
712 ) -> Result<Option<RemotePinConfig>> {
713 if !self.pin_remote {
714 return Ok(None);
715 }
716 let service = self
717 .pin_remote_service
718 .as_deref()
719 .map(str::trim)
720 .filter(|value| !value.is_empty())
721 .ok_or_else(|| Error::Config("pin_remote requires pin_remote_service".to_string()))?;
722 let default_name = default_name.into();
723 let name = self
724 .pin_remote_name
725 .as_deref()
726 .map(str::trim)
727 .filter(|value| !value.is_empty())
728 .unwrap_or(default_name.as_str());
729 Ok(Some(RemotePinConfig {
730 service: service.to_string(),
731 name: name.to_string(),
732 }))
733 }
734
735 #[cfg(not(target_arch = "wasm32"))]
738 pub fn effective_log_file(&self) -> Result<PathBuf> {
739 if let Some(ref p) = self.log_file {
740 Ok(p.clone())
741 } else {
742 default_log_file_path(&self.slug)
743 }
744 }
745
746 #[cfg(not(target_arch = "wasm32"))]
749 pub fn effective_secret_bundle(&self) -> Result<PathBuf> {
750 if let Some(ref p) = self.secret_bundle {
751 Ok(p.clone())
752 } else {
753 default_secret_bundle_path(&self.slug)
754 }
755 }
756
757 #[must_use]
762 pub fn ipfs_gateway_resolver(&self) -> crate::ipfs::IpfsGatewayResolver {
763 crate::ipfs::IpfsGatewayResolver::default().with_cache_ttls(
764 web_time::Duration::from_secs(self.did_resolver_positive_ttl_secs),
765 web_time::Duration::from_secs(self.did_resolver_negative_ttl_secs),
766 )
767 }
768
769 #[cfg(not(target_arch = "wasm32"))]
775 pub fn save(&self) -> Result<()> {
776 let path = self
777 .config_path
778 .as_ref()
779 .ok_or_else(|| Error::Config("cannot save config: no config_path set".to_string()))?;
780
781 let yaml_text = self.to_yaml_string()?;
782
783 write_secure(path, yaml_text.as_bytes())
784 }
785
786 #[cfg(not(target_arch = "wasm32"))]
800 pub fn gen_headless(args: &MaArgs, default_slug: &'static str) -> Result<()> {
801 let slug = args.slug.as_deref().unwrap_or(default_slug).to_string();
802
803 let config_path = if let Some(ref p) = args.config {
804 p.clone()
805 } else {
806 default_config_path(&slug)?
807 };
808 let bundle_path = if let Some(ref p) = args.secret_bundle {
809 p.clone()
810 } else {
811 default_secret_bundle_path(&slug)?
812 };
813
814 if config_path.exists() {
815 return Err(Error::Config(format!(
816 "config file already exists: {} (remove it first or use --config)",
817 config_path.display()
818 )));
819 }
820 if bundle_path.exists() {
821 return Err(Error::Config(format!(
822 "secret bundle already exists: {} (remove it first or use --secret-bundle)",
823 bundle_path.display()
824 )));
825 }
826
827 let passphrase = if let Some(ref p) = args.secret_bundle_passphrase {
829 p.clone()
830 } else {
831 SecretBundle::generate_passphrase()
832 };
833
834 let bundle = SecretBundle::generate();
836 bundle.save(&bundle_path, &passphrase)?;
837
838 let config = Config {
840 slug: slug.clone(),
841 log_level: DEFAULT_LOG_LEVEL.to_string(),
842 log_level_stdout: DEFAULT_LOG_LEVEL_STDOUT.to_string(),
843 did_resolver_positive_ttl_secs: DEFAULT_DID_RESOLVER_POSITIVE_TTL_SECS,
844 did_resolver_negative_ttl_secs: DEFAULT_DID_RESOLVER_NEGATIVE_TTL_SECS,
845 log_file: None,
846 #[cfg(not(target_arch = "wasm32"))]
847 kubo_rpc_url: DEFAULT_KUBO_RPC_URL.to_string(),
848 #[cfg(not(target_arch = "wasm32"))]
849 kubo_key_alias: slug.clone(),
850 secret_bundle: Some(bundle_path.clone()),
851 secret_bundle_passphrase: Some(passphrase),
852 config_path: Some(config_path.clone()),
853 pin_remote: false,
854 pin_remote_service: None,
855 pin_remote_name: None,
856 extra: serde_yaml::Mapping::new(),
857 };
858 config.save()?;
859
860 println!("Config: {}", config_path.display());
861 println!("Secret bundle: {}", bundle_path.display());
862
863 Ok(())
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870
871 #[test]
872 fn remote_pin_config_uses_caller_default_name() {
873 let config = Config::from_yaml_str(
874 r"
875pin_remote: true
876pin_remote_service: pinata
877",
878 )
879 .unwrap();
880
881 let remote = config
882 .remote_pin_config_with_default_name("ma-runtime-ma-root")
883 .unwrap()
884 .unwrap();
885 assert_eq!(remote.service, "pinata");
886 assert_eq!(remote.name, "ma-runtime-ma-root");
887 }
888
889 #[test]
890 fn remote_pin_config_requires_service_when_enabled() {
891 let config = Config::from_yaml_str("pin_remote: true\n").unwrap();
892
893 let err = config
894 .remote_pin_config_with_default_name("ma-runtime-ma-root")
895 .unwrap_err();
896 assert!(err.to_string().contains("pin_remote_service"));
897 }
898
899 #[cfg(not(target_arch = "wasm32"))]
900 #[test]
901 fn cli_pin_remote_overrides_yaml_default() {
902 let config = Config::from_args(
903 &MaArgs {
904 pin_remote: Some(true),
905 pin_remote_service: Some("pinata".to_string()),
906 pin_remote_name: Some("custom-root".to_string()),
907 ..MaArgs::default()
908 },
909 "test",
910 )
911 .unwrap();
912
913 assert!(config.pin_remote);
914 assert_eq!(config.pin_remote_service.as_deref(), Some("pinata"));
915 assert_eq!(config.pin_remote_name.as_deref(), Some("custom-root"));
916 }
917}