use crate::collect::verify_locked;
use crate::lint::banned_tokens;
use crate::lock::{Lock, read_lock};
use cratefield_core::lint_card_data;
use cratefield_core::{HARNESS_API, HARNESS_SIDECARS, Harness, VentureEnv, harness_api_mismatch};
use std::path::Path;
pub fn doctor(
harness: &Harness,
migrations_dir: &Path,
allow_no_captcha: Option<&str>,
sidecars: Option<&str>,
) -> Result<(), String> {
let mut failures: Vec<String> = Vec::new();
for module in harness.modules() {
if module.harness_api() != HARNESS_API {
failures.push(harness_api_mismatch(module.as_ref()));
}
}
if harness.venture().env == VentureEnv::Production {
production_port_checks(harness, allow_no_captcha, &mut failures);
}
let lock = match read_lock(migrations_dir) {
Ok(lock) => lock,
Err(err) => {
failures.push(err);
Lock::default()
}
};
for module in harness.modules() {
for migration in module.migrations().sqlite {
let key = format!("{}/{}", module.name(), migration.id);
match lock.get(&key) {
Some(entry) => {
if let Err(err) = verify_locked(migrations_dir, &key, entry) {
failures.push(err);
}
}
None => failures.push(format!(
"migration {key} is not collected yet — run `fz migrations collect`"
)),
}
}
}
match crate::sidecars::parse(sidecars) {
Ok(mounts) if !mounts.is_empty() => {
for mount in crate::sidecars::shadowing(harness, &mounts) {
failures.push(format!(
"sidecar mount `{}` names a module compiled into this venture; the runtime \
ignores the mount and serves the compiled-in module. Remove it from \
{HARNESS_SIDECARS} or remove the module from the composition",
mount.name
));
}
eprintln!(
"fz: warning: {} is sidecar-mounted, so its tables, migrations and SQL are not \
checked here — they live in its own repository (docs/MOUNTING.md)",
crate::sidecars::listed(&mounts)
);
}
Ok(_) => {}
Err(errors) => failures.extend(errors),
}
for module in harness.modules() {
if !module.migrations().postgres.is_empty() {
continue;
}
for migration in module.migrations().sqlite {
for (token, why) in banned_tokens(migration.sql) {
failures.push(format!(
"module `{}` migration {}: banned token `{token}` — {why}",
module.name(),
migration.id
));
}
}
}
for module in harness.modules() {
let migrations = module.migrations();
for migration in migrations.sqlite.iter().chain(migrations.postgres.iter()) {
for (fragment, why) in lint_card_data(migration.sql) {
failures.push(format!(
"module `{}` migration {}: `{fragment}` {why}",
module.name(),
migration.id
));
}
}
}
if failures.is_empty() {
Ok(())
} else {
Err(failures.join("\n - "))
}
}
fn production_port_checks(
harness: &Harness,
allow_no_captcha: Option<&str>,
failures: &mut Vec<String>,
) {
let public_writers: Vec<&str> = harness
.modules()
.iter()
.filter(|module| module.public_writes())
.map(|module| module.name())
.collect();
if !public_writers.is_empty() && !captcha_provided(harness) {
let message = format!(
"production venture with public writes from [{}] but no Captcha port \
— configure Turnstile (architecture section 11)",
public_writers.join(", ")
);
match allow_no_captcha {
Some(reason) => {
eprintln!("fz: warning: captcha override accepted ({reason}): {message}");
}
None => failures.push(message),
}
}
let webhook_secret_present =
std::env::var("STRIPE_WEBHOOK_SECRET").is_ok_and(|value| !value.trim().is_empty());
if let Some(message) =
payments_webhook_failure(payments_provided(harness), webhook_secret_present)
{
failures.push(message);
}
}
fn captcha_provided(harness: &Harness) -> bool {
harness
.runtime()
.is_some_and(|runtime| runtime.provides().contains(&cratefield_core::Port::Captcha))
}
fn payments_provided(harness: &Harness) -> bool {
harness.runtime().is_some_and(|runtime| {
runtime
.provides()
.contains(&cratefield_core::Port::Payments)
})
}
fn payments_webhook_failure(
payments_provided: bool,
webhook_secret_present: bool,
) -> Option<String> {
if payments_provided && !webhook_secret_present {
Some(
"production venture provides the Payments port but STRIPE_WEBHOOK_SECRET is unset \
— webhook signatures cannot be verified and forged events would be trusted \
(issue #102)"
.to_owned(),
)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::payments_webhook_failure;
#[test]
fn payments_in_production_needs_a_webhook_secret() {
assert!(payments_webhook_failure(true, false).is_some());
assert!(payments_webhook_failure(true, true).is_none());
assert!(payments_webhook_failure(false, false).is_none());
}
}