mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
feat: add typed secrets metadata and API
This commit is contained in:
parent
af97e6cb8e
commit
884c4d4314
21 changed files with 771 additions and 224 deletions
|
|
@ -1327,24 +1327,16 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretListResponse"
|
||||
|
||||
/api/v1/secrets/{name}:
|
||||
put:
|
||||
operationId: setSecret
|
||||
post:
|
||||
operationId: createSecret
|
||||
tags: [Secrets]
|
||||
summary: Store or update a secret
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SetSecretRequest"
|
||||
$ref: "#/components/schemas/CreateSecretRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Secret stored
|
||||
|
|
@ -1359,20 +1351,20 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
delete:
|
||||
operationId: deleteSecret
|
||||
operationId: deleteSecretByName
|
||||
tags: [Secrets]
|
||||
summary: Delete a stored secret
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteSecretRequest"
|
||||
responses:
|
||||
"204":
|
||||
description: Secret deleted
|
||||
"400":
|
||||
description: Invalid secret name
|
||||
description: Invalid secret name or request body
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
|
@ -4329,28 +4321,61 @@ components:
|
|||
description: Server version string.
|
||||
example: "0.176.2"
|
||||
|
||||
SetSecretRequest:
|
||||
description: Request to store a secret value.
|
||||
SecretType:
|
||||
description: The way a secret is consumed by the sandbox.
|
||||
type: string
|
||||
enum:
|
||||
- environment
|
||||
- file
|
||||
|
||||
CreateSecretRequest:
|
||||
description: Request to store or update a secret.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- value
|
||||
- type
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Secret name or destination path for file secrets.
|
||||
value:
|
||||
type: string
|
||||
description: The secret value to store.
|
||||
type:
|
||||
$ref: "#/components/schemas/SecretType"
|
||||
description:
|
||||
type: string
|
||||
description: Optional operator-facing description of the secret.
|
||||
|
||||
DeleteSecretRequest:
|
||||
description: Request to delete a secret by name.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Secret name or destination path for file secrets.
|
||||
|
||||
SecretMetadata:
|
||||
description: Metadata for a stored secret (value is never exposed).
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
- created_at
|
||||
- updated_at
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Secret key name.
|
||||
description: Secret key name or destination path.
|
||||
example: ANTHROPIC_API_KEY
|
||||
type:
|
||||
$ref: "#/components/schemas/SecretType"
|
||||
description:
|
||||
type: string
|
||||
description: Optional operator-facing description of the secret.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
|
|
|||
|
|
@ -489,12 +489,22 @@ pub(crate) struct SecretRmArgs {
|
|||
pub(crate) key: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
pub(crate) enum SecretTypeArg {
|
||||
Environment,
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub(crate) struct SecretSetArgs {
|
||||
/// Name of the secret
|
||||
pub(crate) key: String,
|
||||
pub(crate) key: String,
|
||||
/// Value to store
|
||||
pub(crate) value: String,
|
||||
pub(crate) value: String,
|
||||
#[arg(long, value_enum, default_value = "environment")]
|
||||
pub(crate) r#type: SecretTypeArg,
|
||||
#[arg(long)]
|
||||
pub(crate) description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|||
use dialoguer::console::Term;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use dialoguer::{MultiSelect, Select};
|
||||
use fabro_api::types::SetSecretRequest;
|
||||
use fabro_api::types::{CreateSecretRequest, SecretType as ApiSecretType};
|
||||
use fabro_config::user::SETTINGS_CONFIG_FILENAME;
|
||||
use fabro_config::{Storage, legacy_env};
|
||||
use fabro_model::Provider;
|
||||
use fabro_server::secret_store::SecretStore;
|
||||
use fabro_server::secret_store::{SecretStore, SecretType};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use rand::Rng;
|
||||
|
|
@ -687,10 +687,12 @@ async fn persist_install_secrets(
|
|||
let client = server_client::connect_api_client(storage_dir).await?;
|
||||
for (name, value) in secrets {
|
||||
client
|
||||
.set_secret()
|
||||
.name(name.clone())
|
||||
.body(SetSecretRequest {
|
||||
value: value.clone(),
|
||||
.create_secret()
|
||||
.body(CreateSecretRequest {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
type_: ApiSecretType::Environment,
|
||||
description: None,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
|
@ -700,7 +702,7 @@ async fn persist_install_secrets(
|
|||
|
||||
let mut store = SecretStore::load(Storage::new(storage_dir).secrets_path())?;
|
||||
for (name, value) in secrets {
|
||||
store.set(name, value)?;
|
||||
store.set(name, value, SecretType::Environment, None)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,9 +46,13 @@ pub(super) async fn login_command(
|
|||
for (name, value) in env_pairs {
|
||||
server
|
||||
.api()
|
||||
.set_secret()
|
||||
.name(name.clone())
|
||||
.body(types::SetSecretRequest { value })
|
||||
.create_secret()
|
||||
.body(types::CreateSecretRequest {
|
||||
name: name.clone(),
|
||||
value,
|
||||
type_: types::SecretType::Environment,
|
||||
description: None,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
fabro_util::printerr!(printer, " {} Saved {}", s.green.apply_to("✔"), name);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@ pub(super) async fn list_command(
|
|||
}
|
||||
let _ = args;
|
||||
for secret in secrets {
|
||||
fabro_util::printout!(printer, "{}\t{}", secret.name, secret.updated_at);
|
||||
fabro_util::printout!(
|
||||
printer,
|
||||
"{}\t{}\t{}",
|
||||
secret.name,
|
||||
secret.type_,
|
||||
secret.updated_at
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use fabro_api::Client;
|
||||
use fabro_api::{Client, types};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::{GlobalArgs, SecretRmArgs};
|
||||
|
|
@ -13,8 +13,10 @@ pub(super) async fn rm_command(
|
|||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
client
|
||||
.delete_secret()
|
||||
.name(args.key.clone())
|
||||
.delete_secret_by_name()
|
||||
.body(types::DeleteSecretRequest {
|
||||
name: args.key.clone(),
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.map_err(server_client::map_api_error)?;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ use anyhow::Result;
|
|||
use fabro_api::{Client, types};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::{GlobalArgs, SecretSetArgs};
|
||||
use crate::args::{GlobalArgs, SecretSetArgs, SecretTypeArg};
|
||||
use crate::server_client;
|
||||
use crate::shared::print_json_pretty;
|
||||
|
||||
fn api_secret_type(secret_type: SecretTypeArg) -> types::SecretType {
|
||||
match secret_type {
|
||||
SecretTypeArg::Environment => types::SecretType::Environment,
|
||||
SecretTypeArg::File => types::SecretType::File,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn set_command(
|
||||
client: &Client,
|
||||
args: &SecretSetArgs,
|
||||
|
|
@ -13,10 +20,12 @@ pub(super) async fn set_command(
|
|||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let meta = client
|
||||
.set_secret()
|
||||
.name(args.key.clone())
|
||||
.body(types::SetSecretRequest {
|
||||
value: args.value.clone(),
|
||||
.create_secret()
|
||||
.body(types::CreateSecretRequest {
|
||||
name: args.key.clone(),
|
||||
value: args.value.clone(),
|
||||
type_: api_secret_type(args.r#type),
|
||||
description: args.description.clone(),
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ fn test_secret_lifecycle() {
|
|||
// 2. list -> contains FOO
|
||||
secret(&["list"])
|
||||
.success()
|
||||
.stdout(predicates::str::contains("FOO"));
|
||||
.stdout(predicates::str::contains("FOO\tenvironment"));
|
||||
|
||||
// 3. update FOO
|
||||
secret(&["set", "FOO", "updated"]).success();
|
||||
|
|
@ -90,7 +90,7 @@ fn test_secret_list_alias_ls() {
|
|||
.args(["ls"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicates::str::contains("X"));
|
||||
.stdout(predicates::str::contains("X\tenvironment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -122,6 +122,36 @@ fn test_secret_value_with_equals() {
|
|||
.args(["list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicates::str::contains("URL"))
|
||||
.stdout(predicates::str::contains("URL\tenvironment"))
|
||||
.stdout(predicates::str::contains("https://x.com?a=1&b=2").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_secret_lifecycle() {
|
||||
let context = test_context!();
|
||||
|
||||
let secret =
|
||||
|args: &[&str]| -> assert_cmd::assert::Assert { context.secret().args(args).assert() };
|
||||
|
||||
secret(&[
|
||||
"set",
|
||||
"/tmp/test.pem",
|
||||
"pem-data",
|
||||
"--type",
|
||||
"file",
|
||||
"--description",
|
||||
"Test certificate",
|
||||
])
|
||||
.success();
|
||||
|
||||
secret(&["list"])
|
||||
.success()
|
||||
.stdout(predicates::str::contains("/tmp/test.pem\tfile"))
|
||||
.stdout(predicates::str::contains("pem-data").not());
|
||||
|
||||
secret(&["rm", "/tmp/test.pem"]).success();
|
||||
|
||||
secret(&["list"])
|
||||
.success()
|
||||
.stdout(predicates::str::contains("/tmp/test.pem").not());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ fn secret_list_json_returns_metadata_only() {
|
|||
.iter()
|
||||
.find(|entry| entry["name"] == "ANTHROPIC_API_KEY")
|
||||
.expect("secret list should include the saved key");
|
||||
assert_eq!(entry["type"], "environment");
|
||||
assert!(entry.get("updated_at").is_some());
|
||||
assert!(entry.get("value").is_none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ fn help() {
|
|||
<VALUE> Value to store
|
||||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||
-h, --help Print help
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--type <TYPE> [default: environment] [possible values: environment, file]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--description <DESCRIPTION>
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--verbose Enable verbose output [env: FABRO_VERBOSE=]
|
||||
-h, --help Print help
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use axum::extract::{Path, Query, State};
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::sse::{Event, Sse};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use fabro_api::types::RunArtifactListResponse;
|
||||
use fabro_api::types::{CreateSecretRequest, DeleteSecretRequest, RunArtifactListResponse};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::error::ApiError;
|
||||
|
|
@ -316,11 +316,13 @@ pub(crate) async fn list_secrets(
|
|||
"data": [
|
||||
{
|
||||
"name": "OPENAI_API_KEY",
|
||||
"type": "environment",
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"updated_at": "2026-04-05T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"name": "GITHUB_APP_PRIVATE_KEY",
|
||||
"type": "environment",
|
||||
"created_at": "2026-04-05T12:05:00Z",
|
||||
"updated_at": "2026-04-05T12:05:00Z"
|
||||
}
|
||||
|
|
@ -330,26 +332,27 @@ pub(crate) async fn list_secrets(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_secret(
|
||||
pub(crate) async fn create_secret(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(name): Path<String>,
|
||||
Json(body): Json<CreateSecretRequest>,
|
||||
) -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"name": name,
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"updated_at": "2026-04-05T12:00:00Z"
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert("name".to_string(), json!(body.name));
|
||||
payload.insert("type".to_string(), json!(body.type_));
|
||||
if let Some(description) = body.description {
|
||||
payload.insert("description".to_string(), json!(description));
|
||||
}
|
||||
payload.insert("created_at".to_string(), json!("2026-04-05T12:00:00Z"));
|
||||
payload.insert("updated_at".to_string(), json!("2026-04-05T12:00:00Z"));
|
||||
|
||||
(StatusCode::OK, Json(serde_json::Value::Object(payload))).into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_secret(
|
||||
pub(crate) async fn delete_secret_by_name(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(_name): Path<String>,
|
||||
Json(_body): Json<DeleteSecretRequest>,
|
||||
) -> Response {
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,35 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SecretType {
|
||||
#[default]
|
||||
Environment,
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SecretEntry {
|
||||
pub value: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub value: String,
|
||||
#[serde(rename = "type", default)]
|
||||
pub secret_type: SecretType,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SecretMetadata {
|
||||
pub name: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub secret_type: SecretType,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -66,16 +82,31 @@ impl SecretStore {
|
|||
Ok(Self { path, entries })
|
||||
}
|
||||
|
||||
pub fn set(&mut self, name: &str, value: &str) -> Result<SecretMetadata, SecretStoreError> {
|
||||
Self::validate_name(name)?;
|
||||
pub fn set(
|
||||
&mut self,
|
||||
name: &str,
|
||||
value: &str,
|
||||
secret_type: SecretType,
|
||||
description: Option<&str>,
|
||||
) -> Result<SecretMetadata, SecretStoreError> {
|
||||
Self::validate_name(name, secret_type)?;
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let created_at = self
|
||||
.entries
|
||||
.get(name)
|
||||
.map_or_else(|| now.clone(), |entry| entry.created_at.clone());
|
||||
let (created_at, description) = self.entries.get(name).map_or_else(
|
||||
|| (now.clone(), description.map(str::to_string)),
|
||||
|entry| {
|
||||
(
|
||||
entry.created_at.clone(),
|
||||
description
|
||||
.map(str::to_string)
|
||||
.or_else(|| entry.description.clone()),
|
||||
)
|
||||
},
|
||||
);
|
||||
let entry = SecretEntry {
|
||||
value: value.to_string(),
|
||||
value: value.to_string(),
|
||||
secret_type,
|
||||
description: description.clone(),
|
||||
created_at: created_at.clone(),
|
||||
updated_at: now.clone(),
|
||||
};
|
||||
|
|
@ -84,13 +115,14 @@ impl SecretStore {
|
|||
|
||||
Ok(SecretMetadata {
|
||||
name: name.to_string(),
|
||||
secret_type,
|
||||
description,
|
||||
created_at,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, name: &str) -> Result<(), SecretStoreError> {
|
||||
Self::validate_name(name)?;
|
||||
if self.entries.remove(name).is_none() {
|
||||
return Err(SecretStoreError::NotFound(name.to_string()));
|
||||
}
|
||||
|
|
@ -103,9 +135,11 @@ impl SecretStore {
|
|||
.entries
|
||||
.iter()
|
||||
.map(|(name, entry)| SecretMetadata {
|
||||
name: name.clone(),
|
||||
created_at: entry.created_at.clone(),
|
||||
updated_at: entry.updated_at.clone(),
|
||||
name: name.clone(),
|
||||
secret_type: entry.secret_type,
|
||||
description: entry.description.clone(),
|
||||
created_at: entry.created_at.clone(),
|
||||
updated_at: entry.updated_at.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
data.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
|
@ -119,11 +153,30 @@ impl SecretStore {
|
|||
pub fn snapshot(&self) -> HashMap<String, String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|(_, entry)| entry.secret_type == SecretType::Environment)
|
||||
.map(|(name, entry)| (name.clone(), entry.value.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn validate_name(name: &str) -> Result<(), SecretStoreError> {
|
||||
pub fn file_secrets(&self) -> Vec<(String, String)> {
|
||||
let mut data = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, entry)| entry.secret_type == SecretType::File)
|
||||
.map(|(name, entry)| (name.clone(), entry.value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
data.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
data
|
||||
}
|
||||
|
||||
pub fn validate_name(name: &str, secret_type: SecretType) -> Result<(), SecretStoreError> {
|
||||
match secret_type {
|
||||
SecretType::Environment => Self::validate_env_name(name),
|
||||
SecretType::File => Self::validate_file_name(name),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_env_name(name: &str) -> Result<(), SecretStoreError> {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(first) if first.is_ascii_alphabetic() || first == '_' => {}
|
||||
|
|
@ -137,6 +190,26 @@ impl SecretStore {
|
|||
}
|
||||
}
|
||||
|
||||
fn validate_file_name(name: &str) -> Result<(), SecretStoreError> {
|
||||
if !name.starts_with('/') || name.ends_with('/') || name.contains('\0') {
|
||||
return Err(SecretStoreError::InvalidName(name.to_string()));
|
||||
}
|
||||
|
||||
let path = Path::new(name);
|
||||
if !path.is_absolute() {
|
||||
return Err(SecretStoreError::InvalidName(name.to_string()));
|
||||
}
|
||||
|
||||
if path
|
||||
.components()
|
||||
.any(|component| matches!(component, Component::ParentDir))
|
||||
{
|
||||
return Err(SecretStoreError::InvalidName(name.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_atomic(&self) -> Result<(), SecretStoreError> {
|
||||
let parent = self
|
||||
.path
|
||||
|
|
@ -188,9 +261,13 @@ mod tests {
|
|||
let path = dir.path().join("secrets.json");
|
||||
let mut store = SecretStore::load(path.clone()).unwrap();
|
||||
|
||||
let meta = store.set("OPENAI_API_KEY", "secret").unwrap();
|
||||
let meta = store
|
||||
.set("OPENAI_API_KEY", "secret", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(meta.name, "OPENAI_API_KEY");
|
||||
assert_eq!(meta.secret_type, SecretType::Environment);
|
||||
assert_eq!(meta.description, None);
|
||||
assert_eq!(store.get("OPENAI_API_KEY"), Some("secret"));
|
||||
assert!(path.exists());
|
||||
}
|
||||
|
|
@ -201,8 +278,12 @@ mod tests {
|
|||
let path = dir.path().join("secrets.json");
|
||||
let mut store = SecretStore::load(path).unwrap();
|
||||
|
||||
let first = store.set("OPENAI_API_KEY", "first").unwrap();
|
||||
let second = store.set("OPENAI_API_KEY", "second").unwrap();
|
||||
let first = store
|
||||
.set("OPENAI_API_KEY", "first", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
let second = store
|
||||
.set("OPENAI_API_KEY", "second", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(first.created_at, second.created_at);
|
||||
assert_eq!(store.get("OPENAI_API_KEY"), Some("second"));
|
||||
|
|
@ -213,7 +294,9 @@ mod tests {
|
|||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("secrets.json");
|
||||
let mut store = SecretStore::load(path.clone()).unwrap();
|
||||
store.set("OPENAI_API_KEY", "secret").unwrap();
|
||||
store
|
||||
.set("OPENAI_API_KEY", "secret", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
|
||||
store.remove("OPENAI_API_KEY").unwrap();
|
||||
|
||||
|
|
@ -234,8 +317,12 @@ mod tests {
|
|||
fn list_returns_sorted_metadata_without_values() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
store.set("Z_KEY", "z").unwrap();
|
||||
store.set("A_KEY", "a").unwrap();
|
||||
store
|
||||
.set("Z_KEY", "z", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
store
|
||||
.set("A_KEY", "a", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
|
||||
let listed = store.list();
|
||||
|
||||
|
|
@ -252,7 +339,177 @@ mod tests {
|
|||
fn invalid_names_are_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
let error = store.set("NOT-VALID", "secret").unwrap_err();
|
||||
let error = store
|
||||
.set("NOT-VALID", "secret", SecretType::Environment, None)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.to_string(), "invalid secret name: NOT-VALID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_file_secret_stores_type() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("secrets.json");
|
||||
let mut store = SecretStore::load(path.clone()).unwrap();
|
||||
|
||||
let meta = store
|
||||
.set("/root/.ssh/id_rsa", "secret", SecretType::File, None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(meta.name, "/root/.ssh/id_rsa");
|
||||
assert_eq!(meta.secret_type, SecretType::File);
|
||||
|
||||
let reloaded = SecretStore::load(path).unwrap();
|
||||
let listed = reloaded.list();
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].secret_type, SecretType::File);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_file_secret_validates_absolute_path() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
|
||||
let error = store
|
||||
.set("relative/path", "secret", SecretType::File, None)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "invalid secret name: relative/path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_file_secret_rejects_traversal() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
|
||||
let error = store
|
||||
.set("/root/../id_rsa", "secret", SecretType::File, None)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "invalid secret name: /root/../id_rsa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_env_secret_rejects_path_names() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
|
||||
let error = store
|
||||
.set("/foo/bar", "secret", SecretType::Environment, None)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "invalid secret name: /foo/bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_excludes_file_secrets() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
store
|
||||
.set("OPENAI_API_KEY", "env", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
store
|
||||
.set("/tmp/test.pem", "file", SecretType::File, None)
|
||||
.unwrap();
|
||||
|
||||
let snapshot = store.snapshot();
|
||||
|
||||
assert_eq!(snapshot.get("OPENAI_API_KEY"), Some(&"env".to_string()));
|
||||
assert!(!snapshot.contains_key("/tmp/test.pem"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_includes_only_env_secrets() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
store
|
||||
.set("ANTHROPIC_API_KEY", "a", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
store
|
||||
.set("OPENAI_API_KEY", "b", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
|
||||
let snapshot = store.snapshot();
|
||||
|
||||
assert_eq!(snapshot.len(), 2);
|
||||
assert_eq!(snapshot.get("ANTHROPIC_API_KEY"), Some(&"a".to_string()));
|
||||
assert_eq!(snapshot.get("OPENAI_API_KEY"), Some(&"b".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_secrets_returns_only_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
store
|
||||
.set("OPENAI_API_KEY", "env", SecretType::Environment, None)
|
||||
.unwrap();
|
||||
store
|
||||
.set("/tmp/test.pem", "file", SecretType::File, None)
|
||||
.unwrap();
|
||||
|
||||
let files = store.file_secrets();
|
||||
|
||||
assert_eq!(files, vec![(
|
||||
"/tmp/test.pem".to_string(),
|
||||
"file".to_string()
|
||||
)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn description_round_trips() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("secrets.json");
|
||||
let mut store = SecretStore::load(path.clone()).unwrap();
|
||||
|
||||
let meta = store
|
||||
.set(
|
||||
"/tmp/test.pem",
|
||||
"file",
|
||||
SecretType::File,
|
||||
Some("Test certificate"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(meta.description.as_deref(), Some("Test certificate"));
|
||||
|
||||
let reloaded = SecretStore::load(path).unwrap();
|
||||
let listed = reloaded.list();
|
||||
assert_eq!(listed[0].description.as_deref(), Some("Test certificate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_json_defaults_to_environment() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("secrets.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"{
|
||||
"OPENAI_API_KEY": {
|
||||
"value": "secret",
|
||||
"created_at": "2026-04-12T00:00:00Z",
|
||||
"updated_at": "2026-04-12T00:00:00Z"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let store = SecretStore::load(path).unwrap();
|
||||
let listed = store.list();
|
||||
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(listed[0].secret_type, SecretType::Environment);
|
||||
assert_eq!(listed[0].description, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_allows_file_path_names() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = SecretStore::load(dir.path().join("secrets.json")).unwrap();
|
||||
store
|
||||
.set("/tmp/test.pem", "file", SecretType::File, None)
|
||||
.unwrap();
|
||||
|
||||
store.remove("/tmp/test.pem").unwrap();
|
||||
|
||||
assert!(store.list().is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, header};
|
|||
use axum::middleware::{self, Next};
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post, put};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use axum_extra::extract::cookie::Key;
|
||||
use base64::Engine as _;
|
||||
|
|
@ -26,16 +26,16 @@ pub use fabro_api::types::{
|
|||
ArtifactEntry, ArtifactListResponse, BilledTokenCounts as ApiBilledTokenCounts, BillingByModel,
|
||||
BillingStageRef, CompletionContentPart, CompletionMessage, CompletionMessageRole,
|
||||
CompletionResponse, CompletionToolChoiceMode, CompletionUsage, CreateCompletionRequest,
|
||||
DiskUsageResponse, DiskUsageRunRow, DiskUsageSummaryRow, EventEnvelope as ApiEventEnvelope,
|
||||
ModelReference, PaginatedEventList, PaginatedRunList, PaginationMeta, PreflightResponse,
|
||||
PreviewUrlRequest, PreviewUrlResponse, PruneRunEntry, PruneRunsRequest, PruneRunsResponse,
|
||||
QuestionType as ApiQuestionType, RenderWorkflowGraphDirection, RenderWorkflowGraphFormat,
|
||||
RenderWorkflowGraphRequest, RunArtifactEntry, RunArtifactListResponse, RunBilling,
|
||||
RunBillingStage, RunBillingTotals, RunControlAction as ApiRunControlAction, RunError,
|
||||
RunManifest, RunStatus, RunStatusResponse, SandboxFileEntry, SandboxFileListResponse,
|
||||
ServerSettings, SetSecretRequest, SshAccessRequest, SshAccessResponse, StartRunRequest,
|
||||
StatusReason as ApiStatusReason, SubmitAnswerRequest, SystemInfoResponse, SystemRunCounts,
|
||||
WriteBlobResponse,
|
||||
CreateSecretRequest, DeleteSecretRequest, DiskUsageResponse, DiskUsageRunRow,
|
||||
DiskUsageSummaryRow, EventEnvelope as ApiEventEnvelope, ModelReference, PaginatedEventList,
|
||||
PaginatedRunList, PaginationMeta, PreflightResponse, PreviewUrlRequest, PreviewUrlResponse,
|
||||
PruneRunEntry, PruneRunsRequest, PruneRunsResponse, QuestionType as ApiQuestionType,
|
||||
RenderWorkflowGraphDirection, RenderWorkflowGraphFormat, RenderWorkflowGraphRequest,
|
||||
RunArtifactEntry, RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals,
|
||||
RunControlAction as ApiRunControlAction, RunError, RunManifest, RunStatus, RunStatusResponse,
|
||||
SandboxFileEntry, SandboxFileListResponse, SecretType as ApiSecretType, ServerSettings,
|
||||
SshAccessRequest, SshAccessResponse, StartRunRequest, StatusReason as ApiStatusReason,
|
||||
SubmitAnswerRequest, SystemInfoResponse, SystemRunCounts, WriteBlobResponse,
|
||||
};
|
||||
use fabro_config::{Storage, resolve_server_from_file};
|
||||
use fabro_graphviz::render::GraphFormat;
|
||||
|
|
@ -112,7 +112,7 @@ use crate::error::ApiError;
|
|||
use crate::jwt_auth::{
|
||||
AuthMode, AuthenticatedService, AuthenticatedSubject, authenticate_service_parts,
|
||||
};
|
||||
use crate::secret_store::{SecretStore, SecretStoreError};
|
||||
use crate::secret_store::{SecretStore, SecretStoreError, SecretType as StoreSecretType};
|
||||
use crate::{demo, diagnostics, run_manifest, settings_view, static_files, web_auth};
|
||||
|
||||
pub fn default_page_limit() -> u32 {
|
||||
|
|
@ -1027,10 +1027,11 @@ fn demo_routes() -> Router<Arc<AppState>> {
|
|||
.route("/insights/history", get(demo::list_query_history))
|
||||
.route("/models", get(list_models))
|
||||
.route("/models/{id}/test", post(test_model))
|
||||
.route("/secrets", get(demo::list_secrets))
|
||||
.route(
|
||||
"/secrets/{name}",
|
||||
put(demo::set_secret).delete(demo::delete_secret),
|
||||
"/secrets",
|
||||
get(demo::list_secrets)
|
||||
.post(demo::create_secret)
|
||||
.delete(demo::delete_secret_by_name),
|
||||
)
|
||||
.route("/repos/github/{owner}/{name}", get(demo::get_github_repo))
|
||||
.route("/health/diagnostics", post(demo::run_diagnostics))
|
||||
|
|
@ -1106,8 +1107,12 @@ fn real_routes() -> Router<Arc<AppState>> {
|
|||
.route("/insights/history", get(not_implemented))
|
||||
.route("/models", get(list_models))
|
||||
.route("/models/{id}/test", post(test_model))
|
||||
.route("/secrets", get(list_secrets))
|
||||
.route("/secrets/{name}", put(set_secret).delete(delete_secret))
|
||||
.route(
|
||||
"/secrets",
|
||||
get(list_secrets)
|
||||
.post(create_secret)
|
||||
.delete(delete_secret_by_name),
|
||||
)
|
||||
.route("/repos/github/{owner}/{name}", get(get_github_repo))
|
||||
.route("/health/diagnostics", post(run_diagnostics))
|
||||
.route("/completions", post(create_completion))
|
||||
|
|
@ -1621,16 +1626,26 @@ async fn list_secrets(_auth: AuthenticatedService, State(state): State<Arc<AppSt
|
|||
(StatusCode::OK, Json(serde_json::json!({ "data": data }))).into_response()
|
||||
}
|
||||
|
||||
async fn set_secret(
|
||||
fn secret_type_from_api(secret_type: ApiSecretType) -> StoreSecretType {
|
||||
match secret_type {
|
||||
ApiSecretType::Environment => StoreSecretType::Environment,
|
||||
ApiSecretType::File => StoreSecretType::File,
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_secret(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(name): Path<String>,
|
||||
Json(body): Json<SetSecretRequest>,
|
||||
Json(body): Json<CreateSecretRequest>,
|
||||
) -> Response {
|
||||
let secret_type = secret_type_from_api(body.type_);
|
||||
let name = body.name;
|
||||
let value = body.value;
|
||||
let description = body.description;
|
||||
let state_for_write = Arc::clone(&state);
|
||||
let result = spawn_blocking(move || {
|
||||
let mut store = state_for_write.secret_store.blocking_write();
|
||||
store.set(&name, &body.value)
|
||||
store.set(&name, &value, secret_type, description.as_deref())
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -1658,11 +1673,12 @@ async fn set_secret(
|
|||
}
|
||||
}
|
||||
|
||||
async fn delete_secret(
|
||||
async fn delete_secret_by_name(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(name): Path<String>,
|
||||
Json(body): Json<DeleteSecretRequest>,
|
||||
) -> Response {
|
||||
let name = body.name;
|
||||
let state_for_write = Arc::clone(&state);
|
||||
let result = spawn_blocking(move || {
|
||||
let mut store = state_for_write.secret_store.blocking_write();
|
||||
|
|
@ -6199,6 +6215,80 @@ mod tests {
|
|||
format!("/api/v1{path}")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_secret_stores_file_secret_and_excludes_it_from_snapshot() {
|
||||
let state = create_app_state();
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api("/secrets"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"name": "/tmp/test.pem",
|
||||
"value": "pem-data",
|
||||
"type": "file",
|
||||
"description": "Test certificate",
|
||||
}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = body_json(response.into_body()).await;
|
||||
assert_eq!(body["name"], "/tmp/test.pem");
|
||||
assert_eq!(body["type"], "file");
|
||||
assert_eq!(body["description"], "Test certificate");
|
||||
|
||||
let store = state.secret_store.read().await;
|
||||
assert!(!store.snapshot().contains_key("/tmp/test.pem"));
|
||||
assert_eq!(store.file_secrets(), vec![(
|
||||
"/tmp/test.pem".to_string(),
|
||||
"pem-data".to_string()
|
||||
)]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_secret_by_name_removes_file_secret() {
|
||||
let state = create_app_state();
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
let create_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api("/secrets"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"name": "/tmp/test.pem",
|
||||
"value": "pem-data",
|
||||
"type": "file",
|
||||
}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
let create_response = app.clone().oneshot(create_req).await.unwrap();
|
||||
assert_eq!(create_response.status(), StatusCode::OK);
|
||||
|
||||
let delete_req = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(api("/secrets"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"name": "/tmp/test.pem",
|
||||
}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let delete_response = app.oneshot(delete_req).await.unwrap();
|
||||
assert_eq!(delete_response.status(), StatusCode::NO_CONTENT);
|
||||
assert!(state.secret_store.read().await.list().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subprocess_answer_transport_cancel_run_enqueues_cancel_message() {
|
||||
let (control_tx, mut control_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::json;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::secret_store::SecretType;
|
||||
use crate::server::AppState;
|
||||
|
||||
fn github_http_client(context: &str) -> Result<fabro_http::HttpClient, Response> {
|
||||
|
|
@ -653,7 +654,7 @@ async fn setup_register(
|
|||
{
|
||||
let mut store = state.secret_store.write().await;
|
||||
for (name, value) in secret_updates {
|
||||
if let Err(err) = store.set(name, &value) {
|
||||
if let Err(err) = store.set(name, &value, SecretType::Environment, None) {
|
||||
error!(error = %err, secret = name, "Setup register failed: could not save secret");
|
||||
return json_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ models/completion-tool-choice.ts
|
|||
models/completion-tool-definition.ts
|
||||
models/completion-usage.ts
|
||||
models/create-completion-request.ts
|
||||
models/create-secret-request.ts
|
||||
models/delete-secret-request.ts
|
||||
models/diagnostics-check.ts
|
||||
models/diagnostics-detail.ts
|
||||
models/diagnostics-report.ts
|
||||
|
|
@ -142,7 +144,7 @@ models/save-query-request.ts
|
|||
models/saved-query.ts
|
||||
models/secret-list-response.ts
|
||||
models/secret-metadata.ts
|
||||
models/set-secret-request.ts
|
||||
models/secret-type.ts
|
||||
models/ssh-access-request.ts
|
||||
models/ssh-access-response.ts
|
||||
models/stage-status.ts
|
||||
|
|
|
|||
|
|
@ -22,13 +22,15 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
|
|||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||
// @ts-ignore
|
||||
import type { CreateSecretRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { DeleteSecretRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { ErrorResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SecretListResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SecretMetadata } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SetSecretRequest } from '../models';
|
||||
/**
|
||||
* SecretsApi - axios parameter creator
|
||||
*/
|
||||
|
|
@ -36,16 +38,57 @@ export const SecretsApiAxiosParamCreator = function (configuration?: Configurati
|
|||
return {
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {string} name
|
||||
* @summary Store or update a secret
|
||||
* @param {CreateSecretRequest} createSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSecret: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('deleteSecret', 'name', name)
|
||||
const localVarPath = `/api/v1/secrets/{name}`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
createSecret: async (createSecretRequest: CreateSecretRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'createSecretRequest' is not null or undefined
|
||||
assertParamExists('createSecret', 'createSecretRequest', createSecretRequest)
|
||||
const localVarPath = `/api/v1/secrets`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(createSecretRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {DeleteSecretRequest} deleteSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSecretByName: async (deleteSecretRequest: DeleteSecretRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'deleteSecretRequest' is not null or undefined
|
||||
assertParamExists('deleteSecretByName', 'deleteSecretRequest', deleteSecretRequest)
|
||||
const localVarPath = `/api/v1/secrets`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
|
|
@ -64,11 +107,13 @@ export const SecretsApiAxiosParamCreator = function (configuration?: Configurati
|
|||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(deleteSecretRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
|
|
@ -107,52 +152,6 @@ export const SecretsApiAxiosParamCreator = function (configuration?: Configurati
|
|||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Store or update a secret
|
||||
* @param {string} name
|
||||
* @param {SetSecretRequest} setSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
setSecret: async (name: string, setSecretRequest: SetSecretRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('setSecret', 'name', name)
|
||||
// verify required parameter 'setSecretRequest' is not null or undefined
|
||||
assertParamExists('setSecret', 'setSecretRequest', setSecretRequest)
|
||||
const localVarPath = `/api/v1/secrets/{name}`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(setSecretRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
|
|
@ -169,15 +168,28 @@ export const SecretsApiFp = function(configuration?: Configuration) {
|
|||
return {
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {string} name
|
||||
* @summary Store or update a secret
|
||||
* @param {CreateSecretRequest} createSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteSecret(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSecret(name, options);
|
||||
async createSecret(createSecretRequest: CreateSecretRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SecretMetadata>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createSecret(createSecretRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SecretsApi.deleteSecret']?.[localVarOperationServerIndex]?.url;
|
||||
const localVarOperationServerBasePath = operationServerMap['SecretsApi.createSecret']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {DeleteSecretRequest} deleteSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteSecretByName(deleteSecretRequest: DeleteSecretRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSecretByName(deleteSecretRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SecretsApi.deleteSecretByName']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
|
|
@ -192,20 +204,6 @@ export const SecretsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['SecretsApi.listSecrets']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Store or update a secret
|
||||
* @param {string} name
|
||||
* @param {SetSecretRequest} setSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async setSecret(name: string, setSecretRequest: SetSecretRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SecretMetadata>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.setSecret(name, setSecretRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SecretsApi.setSecret']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -217,13 +215,23 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
|
|||
return {
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {string} name
|
||||
* @summary Store or update a secret
|
||||
* @param {CreateSecretRequest} createSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSecret(name: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteSecret(name, options).then((request) => request(axios, basePath));
|
||||
createSecret(createSecretRequest: CreateSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<SecretMetadata> {
|
||||
return localVarFp.createSecret(createSecretRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {DeleteSecretRequest} deleteSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSecretByName(deleteSecretRequest: DeleteSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteSecretByName(deleteSecretRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns stored secret names and timestamps. Secret values are never exposed.
|
||||
|
|
@ -234,17 +242,6 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
|
|||
listSecrets(options?: RawAxiosRequestConfig): AxiosPromise<SecretListResponse> {
|
||||
return localVarFp.listSecrets(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Store or update a secret
|
||||
* @param {string} name
|
||||
* @param {SetSecretRequest} setSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
setSecret(name: string, setSecretRequest: SetSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<SecretMetadata> {
|
||||
return localVarFp.setSecret(name, setSecretRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -254,13 +251,24 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
|
|||
export class SecretsApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {string} name
|
||||
* @summary Store or update a secret
|
||||
* @param {CreateSecretRequest} createSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public deleteSecret(name: string, options?: RawAxiosRequestConfig) {
|
||||
return SecretsApiFp(this.configuration).deleteSecret(name, options).then((request) => request(this.axios, this.basePath));
|
||||
public createSecret(createSecretRequest: CreateSecretRequest, options?: RawAxiosRequestConfig) {
|
||||
return SecretsApiFp(this.configuration).createSecret(createSecretRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Delete a stored secret
|
||||
* @param {DeleteSecretRequest} deleteSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public deleteSecretByName(deleteSecretRequest: DeleteSecretRequest, options?: RawAxiosRequestConfig) {
|
||||
return SecretsApiFp(this.configuration).deleteSecretByName(deleteSecretRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -272,17 +280,5 @@ export class SecretsApi extends BaseAPI {
|
|||
public listSecrets(options?: RawAxiosRequestConfig) {
|
||||
return SecretsApiFp(this.configuration).listSecrets(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Store or update a secret
|
||||
* @param {string} name
|
||||
* @param {SetSecretRequest} setSecretRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public setSecret(name: string, setSecretRequest: SetSecretRequest, options?: RawAxiosRequestConfig) {
|
||||
return SecretsApiFp(this.configuration).setSecret(name, setSecretRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SecretType } from './secret-type';
|
||||
|
||||
/**
|
||||
* Request to store or update a secret.
|
||||
*/
|
||||
export interface CreateSecretRequest {
|
||||
/**
|
||||
* Secret name or destination path for file secrets.
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
* The secret value to store.
|
||||
*/
|
||||
'value': string;
|
||||
'type': SecretType;
|
||||
/**
|
||||
* Optional operator-facing description of the secret.
|
||||
*/
|
||||
'description'?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Request to delete a secret by name.
|
||||
*/
|
||||
export interface DeleteSecretRequest {
|
||||
/**
|
||||
* Secret name or destination path for file secrets.
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
|
||||
|
|
@ -24,6 +24,8 @@ export * from './completion-tool-choice';
|
|||
export * from './completion-tool-definition';
|
||||
export * from './completion-usage';
|
||||
export * from './create-completion-request';
|
||||
export * from './create-secret-request';
|
||||
export * from './delete-secret-request';
|
||||
export * from './diagnostics-check';
|
||||
export * from './diagnostics-detail';
|
||||
export * from './diagnostics-report';
|
||||
|
|
@ -123,7 +125,7 @@ export * from './save-query-request';
|
|||
export * from './saved-query';
|
||||
export * from './secret-list-response';
|
||||
export * from './secret-metadata';
|
||||
export * from './set-secret-request';
|
||||
export * from './secret-type';
|
||||
export * from './ssh-access-request';
|
||||
export * from './ssh-access-response';
|
||||
export * from './stage-status';
|
||||
|
|
|
|||
|
|
@ -13,15 +13,23 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SecretType } from './secret-type';
|
||||
|
||||
/**
|
||||
* Metadata for a stored secret (value is never exposed).
|
||||
*/
|
||||
export interface SecretMetadata {
|
||||
/**
|
||||
* Secret key name.
|
||||
* Secret key name or destination path.
|
||||
*/
|
||||
'name': string;
|
||||
'type': SecretType;
|
||||
/**
|
||||
* Optional operator-facing description of the secret.
|
||||
*/
|
||||
'description'?: string;
|
||||
/**
|
||||
* When the secret was first stored.
|
||||
*/
|
||||
|
|
@ -32,3 +40,5 @@ export interface SecretMetadata {
|
|||
'updated_at': string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
29
lib/packages/fabro-api-client/src/models/secret-type.ts
Normal file
29
lib/packages/fabro-api-client/src/models/secret-type.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The way a secret is consumed by the sandbox.
|
||||
*/
|
||||
|
||||
export const SecretType = {
|
||||
ENVIRONMENT: 'environment',
|
||||
FILE: 'file'
|
||||
} as const;
|
||||
|
||||
export type SecretType = typeof SecretType[keyof typeof SecretType];
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue