/** * Neo Wallet webhook receiver — minimal example (contract: the Webhooks guide). * * NEO_WEBHOOK_SECRETS='{"whk_2026_09":"nwhs_…"}' PORT=8080 \ * node --experimental-strip-types receiver.ts # Node ≥ 22.6; plain `node` on ≥ 23.6 * * What a correct receiver does, in this order: * 1. read the RAW body — never a parsed and re-serialised copy; * 2. verify the HMAC with the secret for `X-Webhook-Key-Id`, and the timestamp window; * 3. deduplicate on `dedupKey` — delivery is at least once and may be out of order; * 4. answer 2xx inside 10 s, then do the work; * 5. treat `GET /api/v1/tenant/operations/{uid}` as the authoritative state, not the event. * * The in-memory dedup set is for the example only; use a unique index in your database. */ import { createServer } from "node:http"; import { verifyWebhook } from "../typescript/neo-wallet-signing.ts"; const secretsByKid: Record = JSON.parse(process.env.NEO_WEBHOOK_SECRETS ?? "{}"); const port = Number(process.env.PORT ?? 8080); const seen = new Set(); interface WebhookEnvelope { id: string; type: string; occurredAt: string; dedupKey: string; data: Record; apiVersion: string; } createServer((req, res) => { if (req.method !== "POST") { res.writeHead(405).end(); return; } const chunks: Buffer[] = []; req.on("data", (chunk: Buffer) => chunks.push(chunk)); req.on("end", () => { const rawBody = Buffer.concat(chunks); // A 401 is a permanent failure for the sender (any 4xx but 408/429 is not retried), so it // is right for a forged delivery and wrong for a secret you forgot to load: check your // configuration if genuine events land here. if (!verifyWebhook(rawBody, req.headers as Record, secretsByKid)) { res.writeHead(401).end(); return; } const event = JSON.parse(rawBody.toString("utf8")) as WebhookEnvelope; const duplicate = seen.has(event.dedupKey); seen.add(event.dedupKey); // Acknowledge first; a slow handler is a timeout, and a timeout is retried. res.writeHead(204).end(); if (duplicate) return; setImmediate(() => handle(event)); }); }).listen(port, () => { console.log( `webhook receiver on :${port}, kids: ${Object.keys(secretsByKid).join(", ") || "(none)"}`, ); }); function handle(event: WebhookEnvelope): void { // Queue real work here. For an `operation` family event, re-read the operation // (GET /api/v1/tenant/operations/{uid}) before acting on its state. console.log(`${event.occurredAt} ${event.type} ${event.dedupKey}`); }