Inbound

Custom verifiers

Verifier is an open contract — implement it for any sender the built-ins don't cover, or opt out entirely with Noop().

View as Markdown

Verifier is an open contract, not a fixed set. For any sender the built-in provider verifiers don't cover, anything implementing the contract works in a verify slot — a shared-token check, a call out to a verification service, or another provider's HMAC scheme:

lib/postel.ts
import { Postel, SignatureInvalid, type Verifier } from "@postel/core";
import { config } from "./config.js";

function PartnerToken(expected: string): Verifier {
  return {
    async verify(rawBody, headers) {
      if (headers["x-partner-token"] !== expected) {
        throw new SignatureInvalid("partner token mismatch");
      }
      const text = typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody);
      return { event: JSON.parse(text), matchedSecretIndex: 0 };
    },
  };
}

export const postel = Postel({
  inbound: {
    partner: { verify: PartnerToken(config.partnerToken) },
  },
});

A verifier returns a VerifyResult ({ event, matchedSecretIndex }) on success and throws on failure. Throw a PostelError subclass — SignatureInvalid, MalformedHeader, … — so the framework gate maps it to the right HTTP status; any other error bubbles as a 5xx. Custom verifiers compose in arrays alongside the built-ins exactly like Secret/Keyset do — tried in order, first match wins, and matchedVerifierIndex reports which one matched. Two errors short-circuit the array instead of falling through to the next verifier: TimestampTooOld (timestamp validity is independent of which key signed) and ConfigurationError (a config bug is not evidence about the signature — it rethrows immediately, never folded into SignatureInvalid):

verify: [Secret(config.legacySecret), PartnerToken(config.partnerToken)]

Skipping verification with Noop()

If your receiver sits behind a trusted boundary — a private network, or a gateway that already authenticated the caller — and you accept the trade-off, Noop() skips the signature check, the timestamp window, and the signing-header requirement entirely:

lib/postel.ts
import { Postel, Noop } from "@postel/core";

export const postel = Postel({
  inbound: {
    internal: { verify: Noop() },
  },
});

Noop() still parses the Standard Webhooks envelope, so event.type, event.data, and a source schema behave exactly as they do for a verified source — a body that isn't a JSON object with a string type is still rejected. What it does not do is authenticate the sender.

Security. Noop() accepts unauthenticated requests: anyone who can reach the endpoint can deliver an event. Reach for it only when something in front of the receiver already establishes trust.

What's next

On this page