chore: bump product to 0.6.0; multi-owner startup warning

Phase 3.5 ships → product minor bump under pre-1.0 rules (any surface
bump triggers minor). Schema is now 6 (0006_users_authz.sql); API
remains 1 (additive endpoints + new credential type, no breaking
shape changes). docs/versioning.md updated.

main.rs gets warn_on_multi_owner_install() which fires once after
bootstrap when more than one active owner exists — points the
operator at PATCH /admin/admins/{id} for cleanup. Soft-fail on DB
error (does not block startup).

The api-test schema assertion was updated to expect 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-26 22:15:45 +02:00
parent d229120df6
commit 30a1584667
7 changed files with 42 additions and 16 deletions

View File

@@ -45,6 +45,7 @@ async fn run_server() -> anyhow::Result<()> {
let auth = AuthDeps::from_pool(pool.clone());
bootstrap_first_admin(&*auth.users).await?;
warn_on_multi_owner_install(&*auth.users).await;
// Seed Hello World into the default app when this is a fresh
// install (no scripts and no routes). Idempotent on upgrades.
@@ -79,6 +80,31 @@ async fn run_server() -> anyhow::Result<()> {
Ok(())
}
/// Multi-owner startup warning — Phase 3.5 migration upgraded every
/// pre-existing admin_users row to `Owner` via DEFAULT, which for
/// installs with several Phase 3a admins means several co-owners.
/// Surface this once at boot so the operator can demote extras via
/// `PATCH /api/v1/admin/admins/{id}` with `instance_role: "admin"`.
/// Soft-fail: a DB blip should not block startup.
async fn warn_on_multi_owner_install(users: &dyn AdminUserRepository) {
match users.list_active_owners().await {
Ok(owners) if owners.len() > 1 => {
let names: Vec<String> = owners.into_iter().map(|u| u.username).collect();
tracing::warn!(
count = names.len(),
owners = ?names,
"multiple active owners detected — Phase 3.5 promoted every \
pre-existing admin to owner. Demote extras via \
PATCH /api/v1/admin/admins/{{id}} with instance_role."
);
}
Ok(_) => {}
Err(err) => {
tracing::warn!(?err, "could not count active owners for multi-owner startup check");
}
}
}
fn spawn_session_pruner(sessions: Arc<dyn AdminSessionRepository>) {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(600));

View File

@@ -822,7 +822,7 @@ async fn version_includes_public_base_url(pool: PgPool) {
let v: Value = r.json();
assert!(v["public_base_url"].is_string());
assert_eq!(v["api"], 1);
assert_eq!(v["schema"], 5);
assert_eq!(v["schema"], 6);
assert_eq!(v["sdk"], "1.1");
}