Skip to content

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.

bash
npm install @theneuralledger/events

The 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

ts
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

ts
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:

ts
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:

ts
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

ts
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:

ts
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:

ts
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.

The Neural Ledger API