CP17 · Implemented

Signed webhooks

Subscribe an HTTPS endpoint to committed event topics. Trestle signs every delivery and runs it through the durable retry and dead-letter engine.

Configure a target

Open Integrations, enter a descriptive name, the receiver’s public HTTPS URL and a comma-separated topic list. The signing secret is displayed once; copy it into the receiver’s secret store before leaving the page.

Name: Billing projection
HTTPS endpoint: https://hooks.example.com/trestle
Topics: record.created,record.updated

The administrative API accepts the same model:

curl -X POST "$TRESTLE_URL/admin/v1/webhooks" \
  -H "Cookie: trestle_admin=$ADMIN_SESSION" \
  -H "X-Trestle-CSRF: $CSRF" \
  -H "Content-Type: application/json" \
  --data '{
    "name":"Billing projection",
    "url":"https://hooks.example.com/trestle",
    "topics":["record.created","record.updated"]
  }'

Verify a delivery

Trestle-Delivery: del_...
Trestle-Timestamp: 178...
Trestle-Signature: v1=hex-hmac-sha256

Compute HMAC-SHA256 over timestamp + "." + rawBody, compare signatures in constant time, reject stale timestamps and deduplicate the delivery ID before applying side effects.

const signed = `${timestamp}.${rawBody}`;
const expected = createHmac("sha256", secret).update(signed).digest("hex");
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
  return new Response("invalid signature", {status: 401});
}

Envelope and delivery semantics

{
  "version":"1",
  "id":"del_...",
  "topic":"record.created",
  "collection":"issues",
  "recordId":"rec_...",
  "payload":{}
}

Delivery is at least once. A non-2xx response, timeout or connection failure retries with backoff. Inspect the corresponding job for attempts and bounded diagnostics; repair the receiver before retrying a dead job.

Local development

Private and loopback destinations are deliberately refused. Use a temporary public HTTPS tunnel or a deployed development receiver, never weaken SSRF protections to reach localhost.

Dogfood path

CP21 saves a disabled webhook without relying on live DNS, then retains delivery-time DNS resolution and private-address refusal as security invariants. See the automation exercise.