GET /nodes, GET /node/{id}, and SSE /sse all serialize a node’s full
meta object to any caller holding the cluster bearer token. Two SDK meta
types carry a live, user-configured credential as one of their fields: the
SMTP executor’s outbound-mail password, and the outgoing-webhook executor’s
auth-shaped HTTP header values (e.g. a bearer/API-key header the user added).
The SDK’s own KDoc on those fields says “stored server-side; never sent to
read clients” — but nothing in the server actually enforced that. Every
bearer-holding read caller (the Compose UI, an MCP agent, or — on a
public-demo deployment (krill#915) — anyone) received the plaintext
credential value in the response body.
Pre-existing since these node types shipped — not a regression. /nodes,
/node/{id}, and /sse all serialize whatever Node the persistence layer
hands back, with no distinction between “fields safe to echo to a read
client” and “fields that exist only so the server itself can act on them.”
Node.redactSecrets() (shared/.../node/SharedNodeFunctions.kt) — a
pure rewrite that blanks SMTPMetaData.token and any WebHookOutMetaData
header whose key looks like a credential (Authorization, X-Api-Key,
Cookie, etc. — an explicit allowlist, not a blanket wipe of all headers,
so ordinary routing headers like Content-Type still round-trip).Routes.kt: the /nodes list
map, every /node/{id} response branch, and — as a single chokepoint
covering every send() in the handler — the /sse route’s serialize
callback.krillapp/executor/{smtp,webhook}/Edit*.kt) hydrates its local editable
state directly from a node read, then a LaunchedEffect pushes that state
straight back to the server on every recomposition — including the very
first one, before the user has touched anything. Left alone, opening the
SMTP or webhook editor would now silently overwrite the real credential
with blank on load. Added preserveRedactedSecrets(incoming, persisted)
(server/.../NodeSecretMerge.kt), wired into ServerNodeManager.update()
right after it reads the persisted row: if the incoming credential field is
blank and the persisted row already has a real value, the persisted value
wins; an explicit non-blank value from the caller always wins over it.MetaData type carries something described
that way, grep for every route that serializes that type and verify the
claim is actually true before trusting the comment.Node → Node rewrites with no I/O
(RedactSecretsTest, NodeSecretMergeTest), so the credential-preserving
behavior is covered without a live server, per the project’s test-stability
rules.