Jesús Pérez 93b0e5225c
feat(platform): control plane — NATS JetStream + SurrealDB + SOLID enforcement
New crates
  - platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
    subject prefixing under provisioning.>, 6 stream definitions on startup
  - platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
    WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL

  Service integrations
  - orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
    webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
  - control-center: solo_auth_middleware (intentional bypass, --mode solo only),
    NATS session events, WebSocket bridge via JetStream subscription (no polling)
  - vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
    SurrealDB storage backend with MVCC retry + exponential backoff
  - secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
  - extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
    cache invalidation via provisioning.workspace.*.deploy.done

  Clippy workspace clean
  cargo clippy --workspace -- -D warnings: 0 errors
  Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
  (let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
  redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
2026-02-17 23:58:14 +00:00

55 lines
1.6 KiB
Rust

use async_nats::jetstream::{
stream::{Config as StreamConfig, RetentionPolicy, StorageType},
Context,
};
use tracing::info;
use crate::Error;
/// Provision all six JetStream streams required by the provisioning platform.
/// Idempotent — uses `get_or_create_stream` so safe to call on every startup.
pub async fn ensure_provisioning_streams(js: &Context) -> Result<(), Error> {
let streams: &[(&str, &[&str], RetentionPolicy)] = &[
(
"TASKS",
&["provisioning.tasks.>"],
RetentionPolicy::WorkQueue,
),
(
"VAULT",
&["provisioning.vault.>"],
RetentionPolicy::Interest,
),
("AUTH", &["provisioning.auth.>"], RetentionPolicy::Interest),
(
"WORKSPACE",
&["provisioning.workspace.>"],
RetentionPolicy::Limits,
),
("AUDIT", &["provisioning.audit.>"], RetentionPolicy::Limits),
(
"HEALTH",
&["provisioning.health.>"],
RetentionPolicy::Interest,
),
];
for (name, subjects, retention) in streams {
let cfg = StreamConfig {
name: name.to_string(),
subjects: subjects.iter().map(|s| s.to_string()).collect(),
retention: *retention,
storage: StorageType::File,
..Default::default()
};
js.get_or_create_stream(cfg)
.await
.map_err(|e| Error::Stream(format!("failed to ensure stream {name}: {e}")))?;
info!(stream = name, "JetStream stream ready");
}
Ok(())
}