Horizon

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.

app/api/webhooks/nexus/route.ts
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.

.env.local
NEXUS_WEBHOOK_SECRET=replace-with-a-long-random-string

Start the app on port 3000.

npm run dev

Start 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-app
Output
HORIZON: Tunnel connected
  URL          https://my-nexus-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-nexus-app

Your public URL is https://my-nexus-app.hrzn.run. Keep this terminal open.

Add the webhook capability in Nexus

  1. In Nexus Repository, open Settings, then Capabilities.
  2. Select Create Capability.
  3. Select Webhook: Repository to watch one repository. Select Webhook: Global to receive global events.
  4. For a repository webhook, set Repository to the repository to watch.
  5. Set Event Types to the events you want.
  6. Set URL to https://my-nexus-app.hrzn.run/api/webhooks/nexus.
  7. Set Secret Key to the same value as NEXUS_WEBHOOK_SECRET, then save.

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:

Output
  POST    200  /api/webhooks/nexus

Your app terminal prints a line such as:

Output
Received Nexus event: rm:repository:asset CREATED

X-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_SECRET is 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 dev after 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 for Reconnected.
  • Check that URL ends with /api/webhooks/nexus.
  • Check that the Nexus server can reach the internet to call your hrzn.run URL.

Next steps

On this page