diff --git a/apps/webhooks-worker/README.md b/apps/webhooks-worker/README.md index e52e2422..3224a05d 100644 --- a/apps/webhooks-worker/README.md +++ b/apps/webhooks-worker/README.md @@ -137,7 +137,9 @@ The handler therefore resolves that Composio-project-global ID through the datab primary key; ownership and toolkit assignment are immutable after insertion, and terminal status changes atomically reconcile the user's active default. -Production binds one immutable `CHEATCODE_RELEASE_SHA`, exposed by `/health`. +Production binds one immutable `CHEATCODE_RELEASE_SHA`, exposed by `/health` +only to the gateway's service-binding probe (`https://webhooks.internal/health`); +on the public webhook host the route answers as not found. HTTP, cron, idempotency, deletion, and workflow continuation paths use their normal durable ownership and idempotency contracts; database migrations retain their separate target, role, lock, and schema validation. diff --git a/apps/webhooks-worker/src/index.ts b/apps/webhooks-worker/src/index.ts index 4b3451ac..56d5296e 100644 --- a/apps/webhooks-worker/src/index.ts +++ b/apps/webhooks-worker/src/index.ts @@ -165,14 +165,20 @@ webhooksApp.use( }), ); -webhooksApp.get("/health", (c) => - c.json({ +webhooksApp.get("/health", (c) => { + // Service-binding-only surface: the gateway probes https://webhooks.internal/health + // for release aggregation. The public webhook host must answer like the route does + // not exist so release metadata never leaks on an unauthenticated endpoint. + if (new URL(c.req.url).hostname !== "webhooks.internal") { + return c.notFound(); + } + return c.json({ ok: true, releaseSha: c.env.CHEATCODE_RELEASE_SHA ?? "development", versionId: c.env.CF_VERSION_METADATA?.id ?? null, worker: "webhooks", - }), -); + }); +}); webhooksApp.post("/clerk", async (c) => { const signingSecret = await clerkWebhookSigningSecret(c.env);