Events toolkit
@theneuralledger/events is the webhook toolkit: verify deliveries, run a local receiver while developing, and reuse the delivery machinery if you fan events out inside your own systems.
npm install @theneuralledger/eventsThe package has no runtime dependencies, so it is safe to add to a small receiver.
For the signature scheme and framework examples, see verifying webhooks. This page covers the rest of the surface.
Verifying
import { verifyWebhook } from '@theneuralledger/events';
const { deliveryId, keyId, timestamp } = await verifyWebhook({
rawBody: req.body, // Buffer or string, unparsed
headers: req.headers,
keys: { key_abc: process.env.TNL_WEBHOOK_SECRET! },
toleranceSeconds: 300, // clock skew allowance
});Throws on any failure — bad signature, stale timestamp, unknown key id, malformed headers. Treat every throw as 401 and do not inspect the body.
Rejecting replays
import { verifyWebhook } from '@theneuralledger/events';
const seen = new Set<string>(); // use Redis in production
await verifyWebhook({
rawBody: req.body,
headers: req.headers,
keys,
replayStore: {
async has(id) { return seen.has(id); },
async add(id) { seen.add(id); },
},
});An in-process Set is fine for one instance but wrong behind a load balancer — each replica would keep its own view. Back it with Redis, and expire entries after your tolerance window rather than growing forever.
Local receiver for development
Rather than deploying to see whether your handler works:
import { createLocalWebhookReceiver, type LocalReceiverObservation } from '@theneuralledger/events';
const observations: LocalReceiverObservation[] = [];
const server = createLocalWebhookReceiver({
keys: { key_dev: 'dev-secret' },
allowDevelopmentHttp: true, // refuses to start without this
observations, // filled in as deliveries arrive
});
server.listen(4319);
// ... exercise it, then
server.close();It returns a Node Server, so you control the port and lifecycle. The allowDevelopmentHttp flag is deliberate friction: the receiver speaks plain HTTP and must never be exposed as a real endpoint.
It verifies signatures exactly as production does, so a payload that passes here passes there. Point a tunnel at it and use POST /v1/webhooks/subscriptions/{id}/test to drive real deliveries.
The observations array you passed in records what arrived — deliveryId, eventId, eventType, receivedAt — which makes it usable directly in tests:
await sendTestEvent(subscriptionId);
await waitFor(() => observations.length === 1);
expect(observations[0].eventType).toBe('subscription.test');Pass an errors array too if you want to assert on rejected deliveries. You can also force failures with status, either a fixed code or a function of the attempt number, which is how you exercise your own retry handling.
Typed envelopes
import {
WEBHOOK_EVENT_TYPES,
WEBHOOK_SCHEMA_VERSION,
type WebhookEventEnvelope,
type WebhookEventType,
} from '@theneuralledger/events';
function handle(event: WebhookEventEnvelope) {
switch (event.type) {
case 'intelligence.published':
return index(event.resource.id, event.data.summary);
case 'intelligence.retracted':
return remove(event.resource.id);
default:
return; // unknown types are not an error
}
}Do not throw on an unrecognised type. New event types can be added, and a receiver that rejects them will start dead-lettering deliveries it could safely have ignored.
Guarding on WEBHOOK_SCHEMA_VERSION is worthwhile if you persist envelopes:
if (event.schemaVersion !== WEBHOOK_SCHEMA_VERSION) {
logger.warn({ seen: event.schemaVersion }, 'unexpected envelope version');
}Filters
The same filter logic the service applies server-side, so you can predict what a subscription will receive:
import { matchesSubscription, normalizeFilters } from '@theneuralledger/events';
const eventTypes = ['intelligence.published'] as const;
const filters = normalizeFilters({
categories: ['Economic & Macro'],
geographies: ['Japan'],
});
if (matchesSubscription(event, eventTypes, filters)) { /* ... */ }normalizeFilters applies the same casing and trimming the service does, so a filter that matches here matches server-side.
Useful for testing a filter before creating the subscription, and for a second narrowing pass when one endpoint serves several internal consumers.
Delivery machinery
The package also exports the pieces the hosted service is built from — EventDeliveryWorker, FairEventQueue, in-memory stores, and secret protectors. These exist so the service can be tested and so you can reuse the same retry and fairness behaviour for your own internal fan-out.
The in-memory stores are for tests
InMemoryDeliveryStore and InMemoryOutboxStore lose everything on restart. They are for tests and local development. Anything durable needs a real store behind the same interfaces.
You do not need any of this to consume TNL webhooks — verifyWebhook is the whole job.