Commercial And Brand
Harbor Callback Receiver Verification
Harbor Node can sign async execution callbacks with an HMAC SHA-256 header pair:
commercial and brandharborcallbackreceiververification
Source: HARBOR_CALLBACK_RECEIVER_VERIFICATION.md
Harbor Callback Receiver Verification
Harbor Node can sign async execution callbacks with an HMAC SHA-256 header pair:
- timestamp header: defaults to
x-harbor-timestamp - signature header: defaults to
x-harbor-signature
The signature is computed over:
${timestamp}.${rawRequestBody}
The transmitted signature format is:
sha256=<hex digest>
Minimal Node.js example
Use the raw request body exactly as Harbor sent it. Do not parse and re-serialize JSON before verification.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const signingSecret = process.env.HARBOR_CALLBACK_SIGNING_SECRET ?? "replace-me";
const signatureHeaderName = "x-harbor-signature";
const timestampHeaderName = "x-harbor-timestamp";
const maxSkewMs = 5 * 60 * 1000;
app.post(
"/harbor/executions",
express.raw({ type: "application/json" }),
(request, response) => {
const rawTimestamp = request.header(timestampHeaderName);
const rawSignature = request.header(signatureHeaderName);
if (!rawTimestamp || !rawSignature?.startsWith("sha256=")) {
response.status(401).json({ ok: false, reason: "missing_signature_headers" });
return;
}
const timestampMs = Date.parse(rawTimestamp);
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
response.status(401).json({ ok: false, reason: "timestamp_out_of_window" });
return;
}
const rawBody = Buffer.isBuffer(request.body) ? request.body : Buffer.from([]);
const signedPayload = Buffer.concat([Buffer.from(`${rawTimestamp}.`, "utf8"), rawBody]);
const expected = createHmac("sha256", signingSecret).update(signedPayload).digest("hex");
const received = rawSignature.slice("sha256=".length);
const expectedBuffer = Buffer.from(expected, "hex");
const receivedBuffer = Buffer.from(received, "hex");
if (
expectedBuffer.length !== receivedBuffer.length
|| !timingSafeEqual(expectedBuffer, receivedBuffer)
) {
response.status(401).json({ ok: false, reason: "signature_mismatch" });
return;
}
const payload = JSON.parse(rawBody.toString("utf8"));
response.status(202).json({
ok: true,
executionId: payload.execution?.executionId,
event: payload.event
});
}
);
Verification notes
- Keep the signing secret local to the receiver.
- Reject stale timestamps to reduce replay risk.
- Compare digests with
timingSafeEqual. - If you change Harbor's header names in node settings, update the receiver to match.
- If you proxy Harbor callbacks through another service, preserve the raw body byte-for-byte until verification finishes.