Inngest

Turn a Hypeline change event into an Inngest event with a transform, then run durable functions on it. Verify the Ed25519 content signature inside the function to trust the payload.

Inngest runs durable functions triggered by events. A Hypeline change becomes an Inngest event through a webhook transform, and your functions subscribe by event name. This recipe assumes the mental model from the automation platforms overview: an alert delivers to a webhook destination, and here that destination is an Inngest webhook URL.

Step 1: mint the Inngest webhook URL and register a destination

In the Inngest dashboard, open Manage > Webhooks and create a webhook. Inngest gives you a unique URL like https://inn.gs/e/.... Register it as a Hypeline destination on the alert you want to forward:

bash
curl -X POST https://api.hypeline.io/v1/alerts/{alert_id}/destinations \
  -H "Authorization: Bearer $YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"webhook","url":"https://inn.gs/e/YOUR_INNGEST_KEY","delivery_mode":"instant"}'

Pin delivery_mode: "instant" so each change is one event. A digest destination posts a batch wrapper, which the transform below does not expect.

Step 2: write the transform

Inngest lets you attach a server-side JavaScript transform to the webhook. It receives the parsed body and returns an Inngest event. Name events by their kind so functions can subscribe precisely, and carry the Hypeline event id through as the Inngest event id so Inngest dedups repeated deliveries for you:

javascript
// Inngest webhook transform: (evt, headers, queryParams) => InngestEvent
function transform(evt, headers, queryParams) {
  return {
    // Provider-prefixed names keep your event catalog readable.
    name: evt.alert_type
      ? `hypeline/alert.${evt.alert_type}`
      : "hypeline/change.detected",
    // Carrying the UUIDv7 through as the Inngest id makes redeliveries idempotent.
    id: evt.id,
    data: evt,
    ts: Date.parse(evt.emitted_at),
  };
}

A normal content match becomes hypeline/change.detected. An engine alert notification (for example a watched source going quiet) carries alert_type and becomes hypeline/alert.source_quiet, so you can route health signals to a different function.

Step 3: run a durable function

Subscribe a function to the event name. The event data is the full StreamEvent:

javascript
import { inngest } from "./client";

export const onPageChanged = inngest.createFunction(
  { id: "on-page-changed" },
  { event: "hypeline/change.detected" },
  async ({ event, step }) => {
    const change = event.data; // the StreamEvent

    await step.run("handle", async () => {
      // change.url, change.title, change.diff, change.tags, ...
      return { url: change.url };
    });
  },
);

Verify the content signature inside the function

Inngest's transform sees the already-parsed JSON, so the Standard Webhooks HMAC over the raw bytes cannot be recomputed there. Inngest's own model treats the unguessable webhook URL as the shared secret. Hypeline gives you a stronger check: the event carries an Ed25519 content signature that verifies against a public key and holds even though Inngest re-serialized the JSON. Verify it inside the function before you trust the payload:

javascript
import { createPublicKey, verify } from "node:crypto";
import canonicalize from "canonicalize"; // npm i canonicalize (RFC 8785)

let jwksCache;
async function getJwks() {
  if (!jwksCache) {
    const res = await fetch("https://api.hypeline.io/.well-known/jwks.json");
    jwksCache = await res.json(); // Cache-Control: max-age=300
  }
  return jwksCache;
}

function b64urlToBuf(s) {
  return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
}

export async function verifyEvent(event) {
  const jwks = await getJwks();
  const jwk = jwks.keys.find((k) => k.kid === event.key_id);
  if (!jwk) throw new Error(`no JWK for kid ${event.key_id}`);
  const pub = createPublicKey({ key: jwk, format: "jwk" });

  // Reproduce the signed bytes: drop `signature`, JCS-canonicalize the rest.
  const { signature, ...covered } = event;
  const canonical = Buffer.from(canonicalize(covered), "utf8");

  // algorithm null selects EdDSA for an OKP key.
  return verify(null, canonical, pub, b64urlToBuf(signature));
}

Call verifyEvent(event.data) at the top of the function and stop on a false result. The verifying events page covers key rotation and the exact canonicalization rules.

Notes

  • Dedup is handled by carrying evt.id through as the Inngest event id. A repeated delivery of the same UUIDv7 is deduplicated by Inngest.
  • These recipe steps assume delivery_mode: "instant". If you register a digest destination instead, the body is a batch wrapper with an events array, and the transform must iterate it.