//! `VarsService` — the runtime read path for group-inherited config. //! //! Resolves the calling app's config (own rows + inherited group rows, //! env-filtered, proximity-first; see `config_resolver`) and exposes it to //! scripts as `vars::get(key)` / `vars::all()`. Read-only from scripts; //! writes go through the admin API. Like every SDK service, it derives the //! app from `cx.app_id` — never a script argument — so cross-app isolation //! holds. use std::collections::BTreeMap; use std::sync::Arc; use async_trait::async_trait; use picloud_shared::{SdkCallCx, VarsError, VarsService}; use serde_json::Value; use sqlx::PgPool; use crate::authz::{self, AuthzRepo, Capability}; use crate::config_resolver::{fetch_var_candidates, resolve}; pub struct VarsServiceImpl { pool: PgPool, authz: Arc, } impl VarsServiceImpl { #[must_use] pub fn new(pool: PgPool, authz: Arc) -> Self { Self { pool, authz } } /// Authed principals need `AppVarsRead`; anonymous public-HTTP scripts /// (`principal: None`) read freely under script-as-gate semantics. async fn check_read(&self, cx: &SdkCallCx) -> Result<(), VarsError> { if let Some(ref principal) = cx.principal { authz::require(&*self.authz, principal, Capability::AppVarsRead(cx.app_id)) .await .map_err(|_| VarsError::Forbidden)?; } Ok(()) } async fn resolved(&self, cx: &SdkCallCx) -> Result, VarsError> { let candidates = fetch_var_candidates(&self.pool, cx.app_id) .await .map_err(|e| VarsError::Backend(e.to_string()))?; let (values, _provenance) = resolve(candidates); Ok(values) } } #[async_trait] impl VarsService for VarsServiceImpl { async fn get(&self, cx: &SdkCallCx, key: &str) -> Result, VarsError> { self.check_read(cx).await?; Ok(self.resolved(cx).await?.remove(key)) } async fn all(&self, cx: &SdkCallCx) -> Result, VarsError> { self.check_read(cx).await?; self.resolved(cx).await } }