mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 04:28:58 +02:00
fix(webhooks): reload route secrets per request (#70727)
* fix(webhooks): reload route secrets per request * docs(changelog): note webhook secret reload fix
This commit is contained in:
@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Anthropic Vertex: restore ADC-backed model discovery after the lightweight provider-discovery path by resolving emitted discovery entries, exposing synthetic auth on bootstrap discovery, and honoring copied env snapshots when probing the default GCP ADC path. Fixes #65715. (#65716) Thanks @feiskyer.
|
||||
- Codex harness/status: pin embedded harness selection per session, show active non-PI harness ids such as `codex` in `/status`, and keep legacy transcripts on PI until `/new` or `/reset` so config changes cannot hot-switch existing sessions.
|
||||
- Gateway/security: fail closed on agent-driven `gateway config.apply`/`config.patch` runtime edits by allowlisting a narrow set of agent-tunable prompt, model, and mention-gating paths (including Telegram topic-level `requireMention`) instead of relying on a hand-maintained denylist of protected subtrees that could miss new sensitive config keys. (#70726) Thanks @drobison00.
|
||||
- Webhooks/security: re-resolve `SecretRef`-backed webhook route secrets on each request so `openclaw secrets reload` revokes the previous secret immediately instead of waiting for a gateway restart. (#70727) Thanks @drobison00.
|
||||
|
||||
## 2026.4.22
|
||||
|
||||
|
||||
@@ -158,9 +158,10 @@ describe("createTaskFlowWebhookRequestHandler", () => {
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.body).toBe("unauthorized");
|
||||
expect(target.taskFlow.list()).toEqual([]);
|
||||
expect(hoisted.resolveConfiguredSecretInputStringMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("caches SecretRef resolution across requests for the same route", async () => {
|
||||
it("re-resolves SecretRef-backed secrets across requests", async () => {
|
||||
const runtime = createRuntimeTaskFlow();
|
||||
const target: TaskFlowWebhookTarget = {
|
||||
routeId: "cached",
|
||||
@@ -176,7 +177,10 @@ describe("createTaskFlowWebhookRequestHandler", () => {
|
||||
sessionKey: "agent:main:webhook-cached",
|
||||
}),
|
||||
};
|
||||
hoisted.resolveConfiguredSecretInputStringMock.mockResolvedValue({ value: "shared-secret" });
|
||||
hoisted.resolveConfiguredSecretInputStringMock
|
||||
.mockResolvedValueOnce({ value: "shared-secret" })
|
||||
.mockResolvedValueOnce({ value: "rotated-secret" })
|
||||
.mockResolvedValueOnce({ value: "rotated-secret" });
|
||||
const handler = createHandlerWithTarget(target);
|
||||
|
||||
const first = await dispatchJsonRequest({
|
||||
@@ -195,10 +199,20 @@ describe("createTaskFlowWebhookRequestHandler", () => {
|
||||
action: "list_flows",
|
||||
},
|
||||
});
|
||||
const third = await dispatchJsonRequest({
|
||||
handler,
|
||||
path: target.path,
|
||||
secret: "rotated-secret",
|
||||
body: {
|
||||
action: "list_flows",
|
||||
},
|
||||
});
|
||||
|
||||
expect(first.statusCode).toBe(200);
|
||||
expect(second.statusCode).toBe(200);
|
||||
expect(hoisted.resolveConfiguredSecretInputStringMock).toHaveBeenCalledTimes(1);
|
||||
expect(second.statusCode).toBe(401);
|
||||
expect(second.body).toBe("unauthorized");
|
||||
expect(third.statusCode).toBe(200);
|
||||
expect(hoisted.resolveConfiguredSecretInputStringMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("creates flows through the bound session and scrubs owner metadata from responses", async () => {
|
||||
|
||||
@@ -667,7 +667,6 @@ export function createTaskFlowWebhookRequestHandler(params: {
|
||||
targetsByPath: Map<string, TaskFlowWebhookTarget[]>;
|
||||
inFlightLimiter?: WebhookInFlightLimiter;
|
||||
}): (req: IncomingMessage, res: ServerResponse) => Promise<boolean> {
|
||||
const secretByTarget = new WeakMap<TaskFlowWebhookTarget, Promise<string | undefined>>();
|
||||
const rateLimiter = createFixedWindowRateLimiter({
|
||||
windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
|
||||
maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
|
||||
@@ -679,19 +678,19 @@ export function createTaskFlowWebhookRequestHandler(params: {
|
||||
maxInFlightPerKey: WEBHOOK_IN_FLIGHT_DEFAULTS.maxInFlightPerKey,
|
||||
maxTrackedKeys: WEBHOOK_IN_FLIGHT_DEFAULTS.maxTrackedKeys,
|
||||
});
|
||||
const resolveTargetSecret = (target: TaskFlowWebhookTarget): Promise<string | undefined> => {
|
||||
const cached = secretByTarget.get(target);
|
||||
if (cached) {
|
||||
return cached;
|
||||
const resolveTargetSecret = async (
|
||||
target: TaskFlowWebhookTarget,
|
||||
): Promise<string | undefined> => {
|
||||
if (typeof target.secretInput === "string") {
|
||||
return target.secretInput;
|
||||
}
|
||||
const pending = resolveConfiguredSecretInputString({
|
||||
const resolved = await resolveConfiguredSecretInputString({
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
value: target.secretInput,
|
||||
path: target.secretConfigPath,
|
||||
}).then((resolved) => resolved.value);
|
||||
secretByTarget.set(target, pending);
|
||||
return pending;
|
||||
});
|
||||
return resolved.value;
|
||||
};
|
||||
|
||||
return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
|
||||
|
||||
Reference in New Issue
Block a user