PolicyWatcherPublic evidence laboratory
Webhook readiness · local verification

Verify the delivery contract before enabling delivery.

Inspect the exact signed input and verify a deterministic public test vector in this browser. This readiness kit defines receiver interoperability; it does not provide webhook subscriptions or outbound delivery.

Local computation

Verification runs with browser Web Crypto. Field values are not submitted or persisted.

Historical compatibility vector

The public vector checks signature compatibility. Its timestamp is evaluated at the recorded vector time, never by disabling production freshness.

Push delivery not enabled

No endpoint registration, outbound send, retry or delivery receipt is available here.

Protocol workbench

Nothing hidden between raw body and decision.

Change any field to test failure behavior, then restore the canonical vector. All computation remains on this device.

Signing input

Canonical test vector

Public · test only

Public value for this deterministic vector. Do not use it in production.

Positive integer included before the raw body.

Whitespace and property order are part of the signature. Verify before parsing.

Expected form: v1= followed by 64 lowercase hexadecimal characters.

Receiver conformance lab

Eight fixtures. Exact receiver decisions.

Run the published positive and negative cases in this browser. A passing run demonstrates fixture compatibility only; no endpoint or delivery is tested.

Suitev1.0.0
Cases
8 deterministic
Contract
v1.0.0
Execution
Browser · local only

8 local cases are ready to run.

CasePurposeExpectedActualState
01canonical-validAccept the canonical vectorConfirms exact HMAC-SHA256 construction over the recorded timestamp and raw body.validnot_runNot run
02empty-secretReject an empty secretConfirms fail-closed secret validation before digest construction.invalid_secretnot_runNot run
03zero-timestampReject a zero timestampConfirms that the receiver accepts only positive integer Unix timestamps.invalid_timestampnot_runNot run
04stale-timestampReject a stale timestampConfirms enforcement immediately outside the candidate 300-second tolerance.timestamp_outside_tolerancenot_runNot run
05unsupported-versionReject an unsupported signature versionConfirms strict parsing of the lowercase v1 signature prefix.invalid_signature_headernot_runNot run
06uppercase-digestReject an uppercase digestConfirms the canonical lowercase hexadecimal encoding contract.invalid_signature_headernot_runNot run
07body-byte-mutationReject a raw-body byte mutationConfirms that whitespace and exact raw-body bytes remain signature material.signature_mismatchnot_runNot run
08digest-mutationReject a digest mutationConfirms rejection of a well-formed but nonmatching signature value.signature_mismatchnot_runNot run
Evidence summaryNo local run recorded

This suite tests deterministic compatibility with the candidate receiver contract. It does not test endpoint identity, production secret custody, network delivery, retry behavior, replay storage, availability or implementation security outside these cases.

Receiver sequence

Verification has five explicit stages.

The browser workbench covers construction, calculation and comparison. Production receivers must also enforce freshness and replay controls.

  1. 01 · Read
    Preserve the raw body

    Read the exact request bytes before any JSON parsing or reformatting.

  2. 02 · Construct
    Build the signed message

    Join the Unix timestamp, a period and the unchanged raw body.

  3. 03 · Compute
    Calculate HMAC-SHA256

    Use the endpoint secret and encode the digest as lowercase hexadecimal.

  4. 04 · Compare
    Use constant-time comparison

    Compare the receiver digest with the value after the v1 prefix.

  5. 05 · Constrain
    Check freshness and replay

    Apply the timestamp window and reject event IDs already processed.

Receiver examples

Implement the same contract in your runtime.

These examples verify the signature shape and digest. Add the production controls listed below before accepting a delivered event.

Node.jsnode:crypto
import { createHmac, timingSafeEqual } from 'node:crypto';

const timestamp = request.headers.get('PolicyWatcher-Timestamp');
const signature = request.headers.get('PolicyWatcher-Signature');
const eventId = request.headers.get('PolicyWatcher-Event-Id');
const rawBody = Buffer.from(await request.arrayBuffer());

if (!/^\d+$/.test(timestamp ?? '') || !/^v1=[a-f0-9]{64}$/.test(signature ?? '')) {
  throw new Error('Invalid webhook headers');
}

const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (age > 300) throw new Error('Stale webhook timestamp');

const expected = createHmac('sha256', process.env.POLICYWATCHER_WEBHOOK_SECRET)
  .update(`${timestamp}.`, 'utf8')
  .update(rawBody)
  .digest('hex');
const supplied = signature.slice(3);

if (!timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(supplied, 'hex'))) {
  throw new Error('Invalid webhook signature');
}

// Check eventId in a bounded replay store before processing.
Pythonhmac · hashlib
import hashlib
import hmac
import os
import re
import time

timestamp = request.headers["PolicyWatcher-Timestamp"]
signature = request.headers["PolicyWatcher-Signature"]
event_id = request.headers["PolicyWatcher-Event-Id"]
raw_body = request.get_data(cache=False, as_text=False)

if not timestamp.isdigit() or re.fullmatch(r"v1=[a-f0-9]{64}", signature) is None:
    raise ValueError("Invalid webhook headers")
if abs(int(time.time()) - int(timestamp)) > 300:
    raise ValueError("Stale webhook timestamp")

message = timestamp.encode() + b"." + raw_body
expected = hmac.new(
    os.environ["POLICYWATCHER_WEBHOOK_SECRET"].encode(),
    message,
    hashlib.sha256,
).hexdigest()

if not hmac.compare_digest(expected, signature[3:]):
    raise ValueError("Invalid webhook signature")

# Check event_id in a bounded replay store before processing.
Production receiver

Controls still owned by the integrator.

  • Read the exact raw request bytes before JSON parsing or body transformation.
  • Resolve a tenant-owned secret from a managed secret store; never use the public test secret.
  • Reject timestamps outside the configured tolerance; the candidate default is 300 seconds.
  • Compare signatures with a constant-time primitive.
  • Store accepted event IDs or nonces for a bounded replay-protection window.
  • Support overlapping active secrets during controlled key rotation.
  • Record bounded delivery outcomes without logging secrets or raw private payloads.
Available now

Use forward polling for published change events.

The public feed exposes already-published events with an opaque cursor. It does not imply delivery or receipt.