# Harbor Webhooks

Harbor (the open source container registry) sends project webhooks as an HTTP `POST` when
artifacts are pushed, pulled, deleted or scanned, when a quota is hit, and on replication and
tag retention. Payloads are not signed. The only authentication is the optional **Auth Header**
you enter on the webhook, which Harbor sends verbatim as the `Authorization` header.

Use the HTTP notify type and paste the Auth Header value exactly as set in Harbor, including any "Bearer " or "Basic " prefix.

## When to Use This Skill

* How do I receive Harbor webhooks?
* How do I verify the Harbor webhook Auth Header (`Authorization`)?
* Does Harbor sign webhook payloads? (No: shared credential header only)
* Why does my Harbor webhook endpoint return 401?
* How do I handle PUSH_ARTIFACT and SCANNING_COMPLETED events?
* How do I handle Harbor's CloudEvents payload format?

## Verification (core)

There is no signature. Compare the **whole `Authorization` header** to the value you typed in
Harbor's Auth Header field, scheme prefix included (for example `Bearer <token>` or
`Basic <base64>`). Don't strip, parse or base64-decode the prefix:

```javascript
const crypto = require('crypto');

// Check lengths first: timingSafeEqual throws on a length mismatch.
function safeEqual(a, b) {
  const ba = Buffer.from(String(a), 'utf8');
  const bb = Buffer.from(String(b), 'utf8');
  return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
}

function verifyHarborRequest(headers, expected = process.env.HARBOR_AUTHORIZATION) {
  if (!expected) return false;                 // never accept when unconfigured
  const received = headers['authorization'];   // full value, e.g. "Bearer 5f1c..."
  return Boolean(received) && safeEqual(received, expected);
}

```

Return `401` when this fails, `400` for a body that isn't a JSON object, and `200` once handled
(any 2xx counts as delivered). Read the body raw (`express.raw({ type: '*/*' })`) and parse it
only after the check. In Python use `hmac.compare_digest` on **bytes** (encode both sides; a
non-ASCII `str` raises). Python and Next.js equivalents are in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/harbor-webhooks/references/verification.md).

> **A static shared credential authenticates the sender, not the body, and gives no replay
> protection** (no signature, no timestamp). Use HTTPS and rotate the Auth Header in Harbor if it
> leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/harbor-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/harbor-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/harbor-webhooks/examples/fastapi/).

## Event Types

The **Default** format is `{ type, occur_at, operator, event_data }` (`occur_at` is Unix seconds).
The **CloudEvents** format is a CloudEvents 1.0 envelope (`specversion`, `id`, `source`, `type`,
`time`, `data`, `operator`). Pick the format per webhook in Harbor; handle both by dispatching
on `type`:

| Default `type` | CloudEvents `type` | Fired when |
| --- | --- | --- |
| `PUSH_ARTIFACT` | `harbor.artifact.pushed` | An artifact is pushed |
| `PULL_ARTIFACT` | `harbor.artifact.pulled` | An artifact is pulled |
| `DELETE_ARTIFACT` | `harbor.artifact.deleted` | An artifact is deleted |
| `SCANNING_COMPLETED` | `harbor.scan.completed` | A scan completes |
| `SCANNING_STOPPED` | `harbor.scan.stopped` | A scan is stopped |
| `SCANNING_FAILED` | `harbor.scan.failed` | A scan fails |
| `QUOTA_EXCEED` | `harbor.quota.exceeded` | The project quota is exceeded |
| `QUOTA_WARNING` | `harbor.quota.warned` | The project quota nears its threshold |
| `REPLICATION` | `harbor.replication.status.changed` | A replication status changes |
| `TAG_RETENTION` | `harbor.tag_retention.finished` | A tag retention run finishes |

## Environment Variables

```bash
HARBOR_AUTHORIZATION=Bearer 5f1c0f3e8b7a4d2c9e6a1b3d   # exactly what you typed in Harbor's Auth Header field

```

## Local Development

```bash
npx hookdeck-cli listen 3000 harbor --path /webhooks/harbor

```

No account required. Use the printed URL as the Endpoint URL of the Harbor webhook.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/harbor-webhooks/references/overview.md) - Event types, both payload formats, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/harbor-webhooks/references/setup.md) - Adding a webhook to a Harbor project and setting the Auth Header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/harbor-webhooks/references/verification.md) - Header check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/harbor-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests