fix(relay): fail-visible on DB error resolving approval-note channel

enforce_latched_approval_note swallowed every get_workflow error via
.ok(), collapsing a transient DB error to None and skipping the latch
check. A PgPool blip during channel resolution would let a plaintext
approval note reach a latched DM — the leak this PR closes, reopened on
the security boundary itself. Match enforce_latched_body's posture:
NotFound passes (no resolvable channel, cannot be latched), any other
DbError is fail-visible.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Will Pfleger
2026-06-23 16:31:10 -04:00
committed by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 869685243e
commit 6604d5882e

View File

@@ -120,18 +120,22 @@ async fn enforce_latched_body(
/// latched-body rule on the approval note. Approvals reference no `h` tag of
/// their own — the channel is the workflow's (`get_workflow().channel_id`).
/// A workflow with no channel (global) cannot be latched, so it passes through.
///
/// The `get_workflow` lookup honors the same fail-VISIBLE posture as
/// [`enforce_latched_body`]: a missing workflow (`NotFound`) cannot be latched
/// and passes, but a genuine DB error rejects rather than silently skipping the
/// check — otherwise a transient pool blip would let a plaintext note slip into
/// a latched channel.
async fn enforce_latched_approval_note(
state: &Arc<AppState>,
content: &str,
workflow_id: Uuid,
) -> Result<(), IngestError> {
let channel_id = state
.db
.get_workflow(workflow_id)
.await
.ok()
.and_then(|w| w.channel_id);
enforce_latched_body(state, content, channel_id).await
match state.db.get_workflow(workflow_id).await {
Ok(w) => enforce_latched_body(state, content, w.channel_id).await,
Err(buzz_db::DbError::NotFound(_)) => Ok(()),
Err(e) => Err(IngestError::Rejected(format!("error: database error: {e}"))),
}
}
/// Persist a command event inside a transaction. Returns the OPEN transaction