{"_id":"@021.is/spine-webhooks","_rev":"2-005f56913806ae8452fcb38b204ec182","name":"@021.is/spine-webhooks","dist-tags":{"latest":"0.4.3"},"versions":{"0.4.0":{"name":"@021.is/spine-webhooks","version":"0.4.0","license":"MIT","_id":"@021.is/spine-webhooks@0.4.0","maintainers":[{"name":"edvone","email":"edvard@edvone.dev"}],"homepage":"https://github.com/021is/spine-ts#readme","bugs":{"url":"https://github.com/021is/spine-ts/issues"},"dist":{"shasum":"e1ece28db125af4a46fcbaa4b6faf87e035989d0","tarball":"https://registry.npmjs.org/@021.is/spine-webhooks/-/spine-webhooks-0.4.0.tgz","fileCount":4,"integrity":"sha512-RfKUSk96fmBnTRvh3uGe8cJbfBeh5orU8EEHJRYlxKTCYphAp/VYjJqyI1ZI6Q4nwqHQxaq76M82Q6EqDV1Hxg==","signatures":[{"sig":"MEQCIFGf5mrbi6WK2hJq1C1oUu4Qs6jFIuHbva7mQA5XFc9NAiAxFCjK+SN3W3KeGaPaVb+z6Q4wpUreG9gmMfgoRsajWQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":7640},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"e4434c6f59db8f586e0f05ffdb3b88a96e68e67b","scripts":{"build":"tsup src/index.ts --format esm --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"edvone","email":"edvard@edvone.dev"},"repository":{"url":"git+https://github.com/021is/spine-ts.git","type":"git","directory":"packages/webhooks"},"_npmVersion":"10.9.4","description":"Inbound webhook verifier: HMAC signature + replay/idempotency dedupe. Generalized from DC's Resend/Stripe handlers.","directories":{},"_nodeVersion":"22.22.0","dependencies":{"@021.is/spine-errors":"^0.4.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/spine-webhooks_0.4.0_1779981979443_0.13221343859102164","host":"s3://npm-registry-packages-npm-production"}},"0.4.3":{"name":"@021.is/spine-webhooks","version":"0.4.3","license":"MIT","description":"Inbound webhook verifier: HMAC signature + replay/idempotency dedupe. Generalized from DC's Resend/Stripe handlers.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsup src/index.ts --format esm --dts --clean","typecheck":"tsc --noEmit"},"dependencies":{"@021.is/spine-errors":"^0.4.3"},"repository":{"type":"git","url":"git+https://github.com/021is/spine-ts.git","directory":"packages/webhooks"},"publishConfig":{"access":"public"},"_id":"@021.is/spine-webhooks@0.4.3","gitHead":"b9f9333776fa55686cc2feb05f851aa6000b0e05","bugs":{"url":"https://github.com/021is/spine-ts/issues"},"homepage":"https://github.com/021is/spine-ts#readme","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-LoklmtmiLo6rtIC16h8tpdAN7U9gDgOYSq5f6b1GHibUdc1dPpmnr218QzPj7TygxoCjr+ZotTuP34/eOfm6Kg==","shasum":"57379fb477a0c8692678a62d407031e16fdced85","tarball":"https://registry.npmjs.org/@021.is/spine-webhooks/-/spine-webhooks-0.4.3.tgz","fileCount":5,"unpackedSize":9471,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDYI4beGUFSrOKbMdhWtEGBFK9ICZQj4+wPuueaXNjvDAiEAvDvs5pwUDl0dUnFjRbnWHg2QOZi+udH/tIb4A+yKANA="}]},"_npmUser":{"name":"edvone","email":"edvard@edvone.dev"},"directories":{},"maintainers":[{"name":"edvone","email":"edvard@edvone.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/spine-webhooks_0.4.3_1779995263432_0.019491922475554535"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-28T15:26:19.218Z","modified":"2026-05-28T19:07:43.767Z","0.4.0":"2026-05-28T15:26:19.587Z","0.4.3":"2026-05-28T19:07:43.620Z"},"bugs":{"url":"https://github.com/021is/spine-ts/issues"},"license":"MIT","homepage":"https://github.com/021is/spine-ts#readme","repository":{"type":"git","url":"git+https://github.com/021is/spine-ts.git","directory":"packages/webhooks"},"description":"Inbound webhook verifier: HMAC signature + replay/idempotency dedupe. Generalized from DC's Resend/Stripe handlers.","maintainers":[{"name":"edvone","email":"edvard@edvone.dev"}],"readme":"# @021.is/spine-webhooks\n\nInbound webhook HMAC verifier (4 formats: raw, GitHub `sha256=…`, Stripe `t=…,v1=…` with tolerance window, Resend svix-style `v1,<b64>`) + idempotency-key dedupe store. Timing-safe comparisons throughout.\n\n## Use\n\n```ts\nimport { makeHmacVerifier, makeMemoryIdempotencyStore } from \"@021.is/spine-webhooks\";\n\nconst stripe = makeHmacVerifier({\n  secret: env.STRIPE_WEBHOOK_SECRET,\n  toleranceSec: 5 * 60, // 5min — Stripe's recommended max clock skew\n});\n\nexport const POST = withErrorHandling(async (req: Request) => {\n  const rawBody = await req.text();\n  const signature = req.headers.get(\"stripe-signature\");\n  if (!signature) throw new BadRequestException(\"missing signature\");\n\n  stripe.verify({ payload: rawBody, signature, headerScheme: \"stripe\" });\n\n  const event = JSON.parse(rawBody);\n  const seen = await store.seen(event.id);\n  if (seen) return Response.json(ok({ duplicate: true }));\n\n  await handleStripeEvent(event);\n  return Response.json(ok({ processed: true }));\n});\n```\n\n## Schemes\n\n| | What |\n|---|---|\n| `raw` | Plain HMAC-SHA256 hex of body |\n| `github` | `sha256=<hex>` prefix |\n| `stripe` | `t=<unix>,v1=<sig>` — body is `${ts}.${rawBody}`; tolerance window enforces freshness |\n| `resend` | svix-style `v1,<base64>` |\n\n## Idempotency\n\nWebhooks retry on network failure. Without dedup you charge twice / send twice / publish twice. `store.seen(key, ttlSec)` returns true if the key has been seen within TTL — first call returns false + records; subsequent calls return true.\n\nMemory store for tests + tiny apps. Wire to your Prisma idempotency table for production.\n\n## Timing-safe comparisons\n\nAll signature checks use `crypto.timingSafeEqual` — string compare is timing-attackable.\n","readmeFilename":"README.md"}