use anyhow::Context;
use tandem_types::TenantContext;
use super::automation_webhook_store::{secret_digest, secret_material_key};
use crate::automation_v2::types::{
AutomationWebhookLinearVerification, AutomationWebhookSecretMetadata,
AutomationWebhookSignatureScheme, AutomationWebhookTriggerRecord,
};
use crate::util::time::now_ms;
use crate::AppState;
const MAX_IMPORTED_SECRET_LEN: usize = 1024;
impl AppState {
pub(crate) async fn import_automation_webhook_linear_secret(
&self,
tenant_context: &TenantContext,
automation_id: &str,
trigger_id: &str,
secret: &str,
actor_id: Option<String>,
) -> anyhow::Result<AutomationWebhookTriggerRecord> {
let secret = secret.trim();
if secret.is_empty() {
anyhow::bail!("linear signing secret is required");
}
if secret.len() > MAX_IMPORTED_SECRET_LEN {
anyhow::bail!("linear signing secret is unreasonably long");
}
let _guard = self.automation_webhook_persistence.lock().await;
let now = now_ms();
let current_trigger = {
let triggers = self.automation_webhook_triggers.read().await;
let trigger = triggers
.get(trigger_id)
.with_context(|| format!("webhook trigger `{trigger_id}` not found"))?
.clone();
if !trigger.tenant_matches(tenant_context) || trigger.automation_id != automation_id {
anyhow::bail!("webhook trigger tenant or automation mismatch");
}
trigger
};
if !matches!(
current_trigger.signature_scheme,
AutomationWebhookSignatureScheme::LinearHmacSha256
) {
anyhow::bail!(
"provider secret import is only supported for linear_hmac_sha256 webhook triggers"
);
}
let old_secret_ref = current_trigger.secret.secret_ref.clone();
let secret_version = current_trigger
.secret
.secret_version
.saturating_add(1)
.max(1);
let secret_ref = super::automation_webhook_store::secret_ref_for_trigger(
tenant_context,
trigger_id,
secret_version,
);
secret_ref
.validate_for_tenant(tenant_context)
.map_err(|error| anyhow::anyhow!("webhook secret ref tenant mismatch: {error:?}"))?;
let mut trigger = current_trigger.clone();
trigger.secret = AutomationWebhookSecretMetadata {
secret_ref: secret_ref.clone(),
secret_digest: secret_digest(secret, tenant_context, trigger_id),
secret_version,
created_at_ms: now,
rotated_at_ms: Some(now),
rotated_by: actor_id.clone(),
};
trigger
.linear_verification
.get_or_insert_with(AutomationWebhookLinearVerification::default)
.mark_secret_imported(now);
trigger.updated_at_ms = now;
trigger.updated_by = actor_id.clone();
let material = super::AutomationWebhookSecretMaterialRecord {
secret_ref: secret_ref.clone(),
tenant_context: tenant_context.clone(),
trigger_id: trigger_id.to_string(),
secret_version,
secret: secret.to_string(),
created_at_ms: now,
rotated_at_ms: Some(now),
rotated_by: actor_id,
};
let new_secret_key = secret_material_key(&secret_ref);
self.automation_webhook_secret_material
.write()
.await
.insert(new_secret_key.clone(), material);
if let Err(error) = self
.persist_automation_webhook_secret_material_locked()
.await
{
self.automation_webhook_secret_material
.write()
.await
.remove(&new_secret_key);
return Err(error.context("failed to persist imported webhook secret material"));
}
self.automation_webhook_triggers
.write()
.await
.insert(trigger_id.to_string(), trigger.clone());
if let Err(error) = self.persist_automation_webhook_triggers_locked().await {
self.automation_webhook_triggers
.write()
.await
.insert(trigger_id.to_string(), current_trigger);
self.automation_webhook_secret_material
.write()
.await
.remove(&new_secret_key);
if let Err(cleanup_error) = self
.persist_automation_webhook_secret_material_locked()
.await
{
tracing::warn!(
target: "tandem_server::state",
error = ?cleanup_error,
trigger_id,
"failed to clean up imported webhook secret material after trigger persist failure"
);
}
return Err(error.context("failed to persist webhook trigger metadata after import"));
}
let old_secret_key = secret_material_key(&old_secret_ref);
self.automation_webhook_secret_material
.write()
.await
.remove(&old_secret_key);
if let Err(error) = self
.persist_automation_webhook_secret_material_locked()
.await
{
tracing::warn!(
target: "tandem_server::state",
error = ?error,
trigger_id,
"failed to persist removal of superseded webhook secret material"
);
}
Ok(trigger)
}
}