Skip to content

Commit b25b97d

Browse files
authored
fix(webhooks): make health route service-binding-only (#104)
## Summary Closes the last finding from the public-surface sweep that followed #103: `webhooks.trycheatcode.com/health` answered publicly (200, unauthenticated, unrate-limited) with `releaseSha`/`versionId`, while its only real consumer — the gateway's release aggregation — reaches it through the WEBHOOKS service binding as `https://webhooks.internal/health`. The route now gates on the internal hostname and returns Hono's plain `notFound()` on any other host, indistinguishable from a route that does not exist. Same rule as #103: every public route must have a named external consumer. The four provider webhook POST endpoints (signature-verified) are untouched. ## Deploy safety Gateway is unchanged; its binding fetch already carries the `webhooks.internal` hostname, so the gate passes for old and new gateway alike — no deploy-order coupling, no skew window. Roll-forward as usual. ## Verification - Gates: lint, typecheck, full `turbo build --force` (19 packages incl. web), knip, dependency-cruiser (844 modules, 0 violations), skills bundle — all green - Post-deploy: public `GET webhooks.trycheatcode.com/health` → 404; gateway `/health/release` still 200 with the webhooks leg (proves the internal path intact)
1 parent 39d1a7c commit b25b97d

2 files changed

Lines changed: 13 additions & 5 deletions

File tree

apps/webhooks-worker/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ The handler therefore resolves that Composio-project-global ID through the datab
137137
primary key; ownership and toolkit assignment are immutable after insertion,
138138
and terminal status changes atomically reconcile the user's active default.
139139

140-
Production binds one immutable `CHEATCODE_RELEASE_SHA`, exposed by `/health`.
140+
Production binds one immutable `CHEATCODE_RELEASE_SHA`, exposed by `/health`
141+
only to the gateway's service-binding probe (`https://webhooks.internal/health`);
142+
on the public webhook host the route answers as not found.
141143
HTTP, cron, idempotency, deletion, and workflow continuation paths use their
142144
normal durable ownership and idempotency contracts; database migrations retain
143145
their separate target, role, lock, and schema validation.

apps/webhooks-worker/src/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,20 @@ webhooksApp.use(
165165
}),
166166
);
167167

168-
webhooksApp.get("/health", (c) =>
169-
c.json({
168+
webhooksApp.get("/health", (c) => {
169+
// Service-binding-only surface: the gateway probes https://webhooks.internal/health
170+
// for release aggregation. The public webhook host must answer like the route does
171+
// not exist so release metadata never leaks on an unauthenticated endpoint.
172+
if (new URL(c.req.url).hostname !== "webhooks.internal") {
173+
return c.notFound();
174+
}
175+
return c.json({
170176
ok: true,
171177
releaseSha: c.env.CHEATCODE_RELEASE_SHA ?? "development",
172178
versionId: c.env.CF_VERSION_METADATA?.id ?? null,
173179
worker: "webhooks",
174-
}),
175-
);
180+
});
181+
});
176182

177183
webhooksApp.post("/clerk", async (c) => {
178184
const signingSecret = await clerkWebhookSigningSecret(c.env);

0 commit comments

Comments
 (0)