diff --git a/apps/dashboard/src/lib/github-webhook-debug.test.ts b/apps/dashboard/src/lib/github-webhook-debug.test.ts new file mode 100644 index 0000000..d554322 --- /dev/null +++ b/apps/dashboard/src/lib/github-webhook-debug.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { getGitHubWebhookPayloadMetadata } from "./github-webhook-debug"; + +describe("getGitHubWebhookPayloadMetadata", () => { + it("returns compact metadata for object payloads", () => { + expect( + getGitHubWebhookPayloadMetadata({ + action: "opened", + repository: { + name: "rabat", + owner: { login: "adn" }, + }, + sender: { + login: "octocat", + }, + installation: { + id: 77, + }, + pull_request: { + number: 42, + }, + }), + ).toEqual({ + payloadType: "object", + payloadKeys: [ + "action", + "installation", + "pull_request", + "repository", + "sender", + ], + action: "opened", + repository: "adn/rabat", + sender: "octocat", + installationId: 77, + pullNumber: 42, + issueNumber: undefined, + workflowRunId: undefined, + workflowJobId: undefined, + checkRunId: undefined, + checkSuiteId: undefined, + }); + }); + + it("returns array metadata without logging item contents", () => { + expect(getGitHubWebhookPayloadMetadata([{ id: 1 }, { id: 2 }])).toEqual({ + payloadType: "array", + itemCount: 2, + }); + }); + + it("returns primitive metadata without leaking values", () => { + expect(getGitHubWebhookPayloadMetadata("raw-body")).toEqual({ + payloadType: "string", + }); + }); +}); diff --git a/apps/dashboard/src/lib/github-webhook-debug.ts b/apps/dashboard/src/lib/github-webhook-debug.ts new file mode 100644 index 0000000..24d7c13 --- /dev/null +++ b/apps/dashboard/src/lib/github-webhook-debug.ts @@ -0,0 +1,70 @@ +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function getString(value: unknown) { + return typeof value === "string" ? value : undefined; +} + +function getNumber(value: unknown) { + return typeof value === "number" ? value : undefined; +} + +export function getGitHubWebhookPayloadMetadata(payload: unknown) { + if (Array.isArray(payload)) { + return { + payloadType: "array", + itemCount: payload.length, + }; + } + + if (!isRecord(payload)) { + return { + payloadType: payload === null ? "null" : typeof payload, + }; + } + + const repository = isRecord(payload.repository) + ? payload.repository + : undefined; + const owner = + repository && isRecord(repository.owner) ? repository.owner : undefined; + const installation = isRecord(payload.installation) + ? payload.installation + : undefined; + const sender = isRecord(payload.sender) ? payload.sender : undefined; + const pullRequest = isRecord(payload.pull_request) + ? payload.pull_request + : undefined; + const issue = isRecord(payload.issue) ? payload.issue : undefined; + const workflowRun = isRecord(payload.workflow_run) + ? payload.workflow_run + : undefined; + const workflowJob = isRecord(payload.workflow_job) + ? payload.workflow_job + : undefined; + const checkRun = isRecord(payload.check_run) ? payload.check_run : undefined; + const checkSuite = isRecord(payload.check_suite) + ? payload.check_suite + : undefined; + const repositoryName = getString(repository?.name); + const repositoryOwner = getString(owner?.login); + + return { + payloadType: "object", + payloadKeys: Object.keys(payload).sort(), + action: getString(payload.action), + repository: + repositoryOwner && repositoryName + ? `${repositoryOwner}/${repositoryName}` + : undefined, + sender: getString(sender?.login), + installationId: getNumber(installation?.id), + pullNumber: getNumber(pullRequest?.number), + issueNumber: getNumber(issue?.number), + workflowRunId: getNumber(workflowRun?.id) ?? getNumber(workflowJob?.run_id), + workflowJobId: getNumber(workflowJob?.id), + checkRunId: getNumber(checkRun?.id), + checkSuiteId: getNumber(checkSuite?.id), + }; +} diff --git a/apps/dashboard/src/routes/api/webhooks/github.ts b/apps/dashboard/src/routes/api/webhooks/github.ts index f6fd47a..e196a2c 100644 --- a/apps/dashboard/src/routes/api/webhooks/github.ts +++ b/apps/dashboard/src/routes/api/webhooks/github.ts @@ -6,6 +6,7 @@ import { } from "#/lib/github-app.server"; import { markGitHubRevalidationSignals } from "#/lib/github-cache"; import { getGitHubWebhookRevalidationSignalKeys } from "#/lib/github-revalidation"; +import { getGitHubWebhookPayloadMetadata } from "#/lib/github-webhook-debug"; import { PRIVATE_ROUTE_HEADERS } from "#/lib/seo"; export const Route = createFileRoute("/api/webhooks/github")({ @@ -32,6 +33,7 @@ export const Route = createFileRoute("/api/webhooks/github")({ debug("github-webhook", "received webhook request", { deliveryId, event, + bodyLength: requestBody.length, hasSignature: Boolean(signature), userAgent: request.headers.get("user-agent"), }); @@ -68,7 +70,7 @@ export const Route = createFileRoute("/api/webhooks/github")({ debug("github-webhook", "rejected webhook due to invalid json", { deliveryId, event, - requestBody, + bodyLength: requestBody.length, }); return new Response("Invalid JSON payload.", { status: 400, @@ -78,7 +80,7 @@ export const Route = createFileRoute("/api/webhooks/github")({ debug("github-webhook", "parsed webhook payload", { deliveryId, event, - payload, + ...getGitHubWebhookPayloadMetadata(payload), }); const signalKeys = getGitHubWebhookRevalidationSignalKeys(