//! `/api/v1/admin/groups/{id}/scripts*` — group-owned script admin (Phase 4). //! //! * `GET /groups/{id}/scripts` — list the group's own scripts (not //! inherited; just rows owned directly by this group). Gated by //! `GroupScriptsRead` (viewer+ on the group). //! * `POST /groups/{id}/scripts` — create a group-owned script. Gated by //! `GroupScriptsWrite` (editor+ on the group). //! //! A group script is a **template** inherited by every descendant app, //! resolved by name with nearest-owner-wins (CoW). Get / update / delete of an //! existing group script go through the by-id `/scripts/{id}` endpoints, which //! are owner-polymorphic — a group script there gates on `GroupScripts*`. //! //! **Phase 4-lite scope:** group ENDPOINT scripts only, and they must be //! self-contained — `kind=module` and any `import` are rejected here, because //! the origin-aware (lexical) module resolver is Phase 4b. The owner is //! resolved FIRST (slug-or-uuid) and `authz::require` binds the capability to //! the resolved group id, never to a caller-controlled path param. use std::sync::Arc; use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Json, Response}; use axum::routing::get; use axum::{Extension, Router}; use picloud_shared::{ GroupId, Principal, Script, ScriptKind, ScriptSandbox, ScriptValidator, ValidationError, }; use serde::Deserialize; use serde_json::json; use crate::authz::{require, AuthzDenied, AuthzError, AuthzRepo, Capability}; use crate::group_repo::GroupRepository; use crate::repo::{NewScript, ScriptRepository, ScriptRepositoryError}; use crate::sandbox::SandboxCeiling; #[derive(Clone)] pub struct GroupScriptsState { pub scripts: Arc, pub groups: Arc, pub authz: Arc, pub validator: Arc, pub sandbox_ceiling: SandboxCeiling, } pub fn group_scripts_router(state: GroupScriptsState) -> Router { Router::new() .route( "/groups/{group_id}/scripts", get(list_group_scripts).post(create_group_script), ) .with_state(state) } #[derive(Debug, Deserialize)] pub struct CreateGroupScriptRequest { pub name: String, pub description: Option, pub source: String, /// Phase 4-lite accepts only `endpoint`. A `module` is rejected (group /// modules + the lexical import resolver are Phase 4b). #[serde(default)] pub kind: ScriptKind, pub timeout_seconds: Option, pub memory_limit_mb: Option, #[serde(default)] pub sandbox: ScriptSandbox, } async fn list_group_scripts( State(s): State, Extension(principal): Extension, Path(id_or_slug): Path, ) -> Result>, GroupScriptsApiError> { let group_id = resolve_group(&*s.groups, &id_or_slug).await?; require( s.authz.as_ref(), &principal, Capability::GroupScriptsRead(group_id), ) .await?; Ok(Json(s.scripts.list_for_group(group_id).await?)) } async fn create_group_script( State(s): State, Extension(principal): Extension, Path(id_or_slug): Path, Json(input): Json, ) -> Result<(StatusCode, Json