{"_id":"webhook-sentinelx","_rev":"2-37c920c1801f307d74c69f67f46b43cd","name":"webhook-sentinelx","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"webhook-sentinelx","version":"0.1.0","keywords":["webhook","stripe","github","slack","braintree","verification","dedup","redis","typescript"],"author":{"name":"Yevhen Mykhailenko"},"license":"MIT","_id":"webhook-sentinelx@0.1.0","maintainers":[{"name":"yevhen_mykhailenko","email":"yvn.mykh@gmail.com"}],"dist":{"shasum":"510138faf9e8431bbb6f1dce4929944d4efbc937","tarball":"https://registry.npmjs.org/webhook-sentinelx/-/webhook-sentinelx-0.1.0.tgz","fileCount":10,"integrity":"sha512-O0DUcsjqLjM23Oir+1WVVuEDOz6KhOCj5saYZ6pxlh84/0za2XZ6HRblqVxIVHgjHs68pX2yXGUI57gk+s5vGA==","signatures":[{"sig":"MEYCIQDiDbY3lQRIuenJqyykX9jXSQfbslXh7rR2G0K23NbV6wIhAOj05maYTb54UO1vQXbQ8h7fUM7oqjeIdJMM46egP5Cb","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":72389},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"gitHead":"190c77c816598128572eff66652979aa02f00740","scripts":{"lint":"eslint .","test":"vitest run","build":"tsup","format":"prettier --write .","lint:fix":"eslint . --fix","typecheck":"tsc -p tsconfig.json --noEmit","format:check":"prettier --check ."},"_npmUser":{"name":"yevhen_mykhailenko","email":"yvn.mykh@gmail.com"},"_npmVersion":"10.9.2","description":"Unified webhook verification and dispatch with normalized events and de-duplication.","directories":{},"sideEffects":false,"_nodeVersion":"22.13.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","eslint":"^9.36.0","vitest":"^2.0.5","globals":"^15.9.0","prettier":"^3.3.3","typescript":"^5.6.2","@types/node":"^20.12.12","eslint-plugin-import":"^2.29.1","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^8.8.1","@typescript-eslint/eslint-plugin":"^8.8.1"},"peerDependencies":{"redis":"*","braintree":"*"},"peerDependenciesMeta":{"redis":{"optional":true},"braintree":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/webhook-sentinelx_0.1.0_1758590374995_0.39426307905118874","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"webhook-sentinelx","version":"0.1.1","description":"Unified webhook verification and dispatch with normalized events and de-duplication.","license":"MIT","author":{"name":"Yevhen Mykhailenko"},"repository":{"type":"git","url":"git+https://github.com/YevhenMykhailenko/webhook-sentinelx.git"},"bugs":{"url":"https://github.com/YevhenMykhailenko/webhook-sentinelx/issues"},"homepage":"https://github.com/YevhenMykhailenko/webhook-sentinelx#readme","type":"module","engines":{"node":">=18"},"sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"build":"tsup","typecheck":"tsc -p tsconfig.json --noEmit","test":"vitest run","lint":"eslint .","lint:fix":"eslint . --fix","format":"prettier --write .","format:check":"prettier --check ."},"keywords":["webhook","stripe","github","slack","braintree","verification","dedup","redis","typescript"],"peerDependencies":{"braintree":"*","redis":"*"},"peerDependenciesMeta":{"braintree":{"optional":true},"redis":{"optional":true}},"devDependencies":{"@types/node":"^20.12.12","@typescript-eslint/eslint-plugin":"^8.8.1","@typescript-eslint/parser":"^8.8.1","eslint":"^9.36.0","eslint-config-prettier":"^9.1.0","eslint-plugin-import":"^2.29.1","globals":"^15.9.0","prettier":"^3.3.3","tsup":"^8.0.1","typescript":"^5.6.2","vitest":"^2.0.5"},"_id":"webhook-sentinelx@0.1.1","gitHead":"1074d640fc2b07901f3acee14b9cf9e1c3520827","_nodeVersion":"22.13.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-rFlTK/USrhr9hN9CGCrSEWf2rbId/12aSrsn0wcwW8b1GEjg4WLSe7SAk2Nh5LmZgvTz/wb7ir9ZaWL8X/EGbQ==","shasum":"b222f0abc0a960b2cfe6955dc02a01dc3f0f8c90","tarball":"https://registry.npmjs.org/webhook-sentinelx/-/webhook-sentinelx-0.1.1.tgz","fileCount":10,"unpackedSize":72698,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIAP0MXaUk2XZqiHnKgJ58sSSMdAJvr/0pmGHWm3UZ4nYAiEAjZ/Yu9cxtlcp4Oo6QO6Kd8GioVKClYOr3J9WTg/63Ms="}]},"_npmUser":{"name":"yevhen_mykhailenko","email":"yvn.mykh@gmail.com"},"directories":{},"maintainers":[{"name":"yevhen_mykhailenko","email":"yvn.mykh@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/webhook-sentinelx_0.1.1_1758590989213_0.5807075532897776"},"_hasShrinkwrap":false}},"time":{"created":"2025-09-23T01:19:34.994Z","modified":"2025-09-23T01:29:49.620Z","0.1.0":"2025-09-23T01:19:35.204Z","0.1.1":"2025-09-23T01:29:49.406Z"},"author":{"name":"Yevhen Mykhailenko"},"license":"MIT","keywords":["webhook","stripe","github","slack","braintree","verification","dedup","redis","typescript"],"description":"Unified webhook verification and dispatch with normalized events and de-duplication.","maintainers":[{"name":"yevhen_mykhailenko","email":"yvn.mykh@gmail.com"}],"readme":"# webhook-sentinelx\n\nUnified webhook verification and dispatch with normalized events and de-duplication. TypeScript-first, ESM+CJS, minimal dependencies.\n\nVerify signatures from multiple providers (Stripe, Braintree, GitHub, Slack), normalize event shape, and prevent duplicates with an in-memory or Redis store.\n\n---\n\n## Features\n\n- Signature verification for Stripe, Braintree, GitHub, Slack. Extensible providers.\n- Normalized event shape: `{ id, source, type, createdAt, payload, raw, requestId? }`.\n- Dedup store options: in-memory by default or Redis as an optional peer dependency.\n- Small dispatcher for ergonomic routing.\n- Framework-friendly: Express, Next.js, or any Node HTTP server using raw body.\n- Typed API and testable design with raw Buffer input.\n\n---\n\n## Installation\n\n```bash\nnpm i webhook-sentinelx\n# optional at runtime only if you use these features\nnpm i braintree redis\n```\n\n`braintree` and `redis` are optional peer dependencies. Install them only if you use the Braintree provider or the Redis store in production.\n\nRequirements: Node 18 or newer.\n\n---\n\n## Quick Start (Express)\n\n```ts\nimport express from \"express\";\nimport { createVerifier, MemoryStore, createDispatcher } from \"webhook-sentinelx\";\n\nconst app = express();\n\n// Important: Stripe and Slack need the raw body\napp.use(\"/webhooks\", express.raw({ type: \"*/*\" }));\n\nconst verify = createVerifier({\n  stripe: { endpointSecret: process.env.STRIPE_WEBHOOK_SECRET! },\n  github: { secret: process.env.GH_WEBHOOK_SECRET! },\n  slack: { signingSecret: process.env.SLACK_SIGNING_SECRET! },\n  // Braintree uses its SDK and x-www-form-urlencoded body\n  braintree: {\n    merchantId: process.env.BT_MERCHANT_ID!,\n    publicKey: process.env.BT_PUBLIC_KEY!,\n    privateKey: process.env.BT_PRIVATE_KEY!,\n    environment: \"sandbox\", // or \"production\"\n  },\n  toleranceSec: 300, // anti replay window\n});\n\nconst store = new MemoryStore({ ttlSec: 600 });\nconst on = createDispatcher();\n\non.on(\"stripe:payment_intent.succeeded\", async (evt) => {\n  // your logic\n});\n\napp.post(\"/webhooks/:source\", async (req, res) => {\n  try {\n    const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body);\n    const evt = await verify(req.params.source as any, req.headers as any, raw);\n\n    if (await store.seen(evt.id)) return res.sendStatus(202); // duplicate\n\n    await on.dispatch(`${evt.source}:${evt.type}`, evt);\n    res.sendStatus(200);\n  } catch (e: any) {\n    if (e.code === \"SIGNATURE_VERIFICATION_FAILED\") return res.status(400).send(\"bad signature\");\n    if (e.code === \"UNSUPPORTED_SOURCE\") return res.status(404).send(\"unknown source\");\n    if (e.code === \"STALE_TIMESTAMP\") return res.status(400).send(\"stale timestamp\");\n    console.error(e);\n    res.sendStatus(500);\n  }\n});\n\napp.listen(3000, () => console.log(\"webhook-sentinelx listening on :3000\"));\n```\n\n---\n\n## Next.js (Pages router)\n\n```ts\n// pages/api/webhooks/[source].ts\nimport type { NextApiRequest, NextApiResponse } from \"next\";\nimport { createVerifier, MemoryStore } from \"webhook-sentinelx\";\n\nexport const config = { api: { bodyParser: false } }; // preserve raw body\n\nconst verify = createVerifier({\n  stripe: { endpointSecret: process.env.STRIPE_WEBHOOK_SECRET! },\n});\nconst store = new MemoryStore({ ttlSec: 600 });\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  if (req.method !== \"POST\") return res.status(405).end();\n\n  const source = req.query.source as any;\n  const chunks: Buffer[] = [];\n  for await (const c of req) chunks.push(Buffer.from(c));\n  const raw = Buffer.concat(chunks);\n\n  try {\n    const evt = await verify(source, req.headers as any, raw);\n    if (await store.seen(evt.id)) return res.status(202).end();\n    // handle evt\n    return res.status(200).end();\n  } catch (e: any) {\n    const code = e.code || \"ERR\";\n    const status =\n      code === \"UNSUPPORTED_SOURCE\" ? 404 : code === \"SIGNATURE_VERIFICATION_FAILED\" ? 400 : 500;\n    return res.status(status).send(code);\n  }\n}\n```\n\nFor App Router or Edge runtime make sure you can access the raw Request body as an ArrayBuffer and pass it as a Buffer to `verify()`.\n\n---\n\n## API\n\n### `createVerifier(config)` returns `(source, headers, rawBody) => NormalizedEvent`\n\n**Config**\n\n```ts\nexport type VerifierConfig = {\n  toleranceSec?: number; // anti replay window, default 300\n  stripe?: { endpointSecret: string };\n  braintree?: {\n    merchantId: string;\n    publicKey: string;\n    privateKey: string;\n    environment?: \"sandbox\" | \"production\";\n  };\n  github?: { secret: string };\n  slack?: { signingSecret: string };\n};\n```\n\n**NormalizedEvent**\n\n```ts\nexport type NormalizedEvent = {\n  id: string;\n  source: \"stripe\" | \"braintree\" | \"github\" | \"slack\";\n  type: string;\n  createdAt: Date;\n  payload: unknown; // original parsed JSON or SDK object for Braintree\n  raw: Buffer; // raw request body\n  requestId?: string;\n};\n```\n\n**Errors** thrown as `WebhookSentinelxdError` with `code`:\n\n- `UNSUPPORTED_SOURCE`\n- `SIGNATURE_VERIFICATION_FAILED`\n- `STALE_TIMESTAMP`\n\n### Providers\n\n- Stripe reads `Stripe-Signature` header and verifies HMAC SHA256 over `t.rawBody`. Requires raw body.\n- GitHub reads `x-hub-signature-256` with `sha256=` prefix. HMAC SHA256 over raw body.\n- Slack reads `x-slack-request-timestamp` and `x-slack-signature`. HMAC SHA256 over `v0:{ts}:{raw}`. Requires raw body.\n- Braintree consumes `x-www-form-urlencoded` with `bt_signature` and `bt_payload` through the official SDK.\n\nRaw body note: For HMAC verification providers do not JSON parse before verification. Pass the original bytes.\n\n### Stores for de-duplication\n\n```ts\nimport { MemoryStore, RedisStore } from \"webhook-sentinelx\";\n\nconst memory = new MemoryStore({ ttlSec: 600 });\nawait memory.seen(\"event-id\"); // false on first call, true afterwards within TTL\n\nconst redis = new RedisStore({ url: process.env.REDIS_URL!, keyPrefix: \"webhook-sentinelx\" });\nawait redis.seen(\"event-id\");\n```\n\n### Dispatcher helper\n\n```ts\nimport { createDispatcher } from \"webhook-sentinelx\";\n\nconst bus = createDispatcher();\nbus.on(\"stripe:payment_intent.succeeded\", (evt) => {\n  /*...*/\n});\nbus.on(\"github:push\", (evt) => {\n  /*...*/\n});\nawait bus.dispatch(\"stripe:payment_intent.succeeded\", evt);\n```\n\n---\n\n## TypeScript and Module Formats\n\nPublished as dual ESM (.mjs) and CJS (.cjs) with typings.\n\nImport examples:\n\n```ts\n// ESM\nimport { createVerifier } from \"webhook-sentinelx\";\n// CJS\nconst { createVerifier } = require(\"webhook-sentinelx\");\n```\n\n---\n\n## Testing locally\n\n```bash\nnpm i -D typescript tsup vitest @types/node\nnpm run test\nnpm run build\n```\n\n---\n\n## Security notes\n\n- Use `toleranceSec` to reject replayed events with stale timestamps.\n- Always use HTTPS for webhook endpoints.\n- Keep provider secrets in a secure manager or environment.\n\n---\n\n## Roadmap\n\n- More providers: Shopify, PayPal non Braintree, Plaid, Twilio, Notion.\n- Pluggable retry or queue adapters such as SQS or Kafka.\n- Delivery receipts and replay helpers.\n\n---\n\n## License\n\nMIT 2025\n","readmeFilename":"README.md","homepage":"https://github.com/YevhenMykhailenko/webhook-sentinelx#readme","repository":{"type":"git","url":"git+https://github.com/YevhenMykhailenko/webhook-sentinelx.git"},"bugs":{"url":"https://github.com/YevhenMykhailenko/webhook-sentinelx/issues"}}