Test Sonatype Nexus Repository webhooks locally
Receive Sonatype Nexus Repository webhook events on localhost with a Horizon tunnel, and verify the X-Nexus-Webhook-Signature header.
Receive Nexus Repository events on your laptop while you build, with a URL Nexus can reach.
Horizon has no Nexus integration. Nexus sends webhooks to a URL, and Horizon provides a public one. Your Nexus server must be able to reach that URL.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - A Next.js app that uses the App Router and runs on port 3000
- A Nexus Repository instance where you are an administrator with permission to create capabilities
Start your app
When you set a secret key on a webhook capability, Nexus signs each delivery. It sends the X-Nexus-Webhook-Signature header. The header holds an HMAC (a keyed hash) of the JSON body, using SHA-1, as a hex digest. Nexus hashes the JSON body without whitespace, and its Node.js example hashes JSON.stringify(req.body). The handler below does the same, then compares with crypto.timingSafeEqual.
Nexus ships no Node SDK helper for this.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(request: Request) {
const secret = process.env.NEXUS_WEBHOOK_SECRET;
if (!secret) {
return new Response("Missing NEXUS_WEBHOOK_SECRET", { status: 500 });
}
const body = await request.json();
const received = request.headers.get("x-nexus-webhook-signature") ?? "";
const expected = createHmac("sha1", secret)
.update(JSON.stringify(body))
.digest("hex");
const receivedBuffer = Buffer.from(received);
const expectedBuffer = Buffer.from(expected);
const isValid =
receivedBuffer.length === expectedBuffer.length &&
timingSafeEqual(receivedBuffer, expectedBuffer);
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
const eventType = request.headers.get("x-nexus-webhook-id");
console.log(`Received Nexus event: ${eventType} ${body.action}`);
return new Response("ok", { status: 200 });
}Pick a secret and store it in an environment variable. Use a random, high-entropy string.
NEXUS_WEBHOOK_SECRET=replace-with-a-long-random-stringStart the app on port 3000.
npm run devStart a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your Nexus capability would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-nexus-appHORIZON: Tunnel connected
URL https://my-nexus-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-nexus-appYour public URL is https://my-nexus-app.hrzn.run. Keep this terminal open.
Add the webhook capability in Nexus
- In Nexus Repository, open Settings, then Capabilities.
- Select Create Capability.
- Select Webhook: Repository to watch one repository. Select Webhook: Global to receive global events.
- For a repository webhook, set Repository to the repository to watch.
- Set Event Types to the events you want.
- Set URL to
https://my-nexus-app.hrzn.run/api/webhooks/nexus. - Set Secret Key to the same value as
NEXUS_WEBHOOK_SECRET, then save.
The Secret Key is optional. Without it, Nexus sends no X-Nexus-Webhook-Signature header and the handler above rejects every request.
Trigger an event
Nexus documents no test button and no resend. Cause a real event instead. Add or change an asset in the repository you watch. A Nexus asset event carries an action such as CREATED.
Check it works
In the terminal that runs hrzn, you see one line for the delivery:
POST 200 /api/webhooks/nexusYour app terminal prints a line such as:
Received Nexus event: rm:repository:asset CREATEDX-Nexus-Webhook-Id holds the event type, for example rm:repository:asset. X-Nexus-Webhook-Delivery is a unique UUID for the event.
If the line shows [401], see Troubleshooting.
Troubleshooting
The signature doesn't match
- Check that
NEXUS_WEBHOOK_SECRETis identical to the Secret Key in Nexus, with no extra spaces or newline. - Nexus hashes the JSON body without whitespace. The handler stringifies the parsed body to match. If you hash the raw text and it differs, return to the stringified body.
- The algorithm is SHA-1, not SHA-256.
- Restart
npm run devafter you edit.env.local.
Audit events never arrive
If you select the audit event type on a global webhook, you must also enable the separate Audit capability.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-nexus-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.
Nothing reaches your app
- Check that the Horizon terminal is still running. If its last line is
Connection lost. Reconnecting…, wait forReconnected. - Check that URL ends with
/api/webhooks/nexus. - Check that the Nexus server can reach the internet to call your
hrzn.runURL.
Next steps
- Read Sonatype's guide to working with HMAC payloads.
- See the example headers and payloads.
Test Sentry webhooks locally
Receive Sentry integration platform webhooks on localhost with a Horizon tunnel, and verify the Sentry-Hook-Signature header.
Test HCP Terraform webhooks locally
Receive HCP Terraform (Terraform Cloud) notification webhooks on localhost with a Horizon tunnel, and verify the X-TFE-Notification-Signature header.