{"_id":"@boring-stack-pkg/eslint-plugin-stripe-webhooks","_rev":"2-d2e21a411229637b57604584d4291047","name":"@boring-stack-pkg/eslint-plugin-stripe-webhooks","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.1":{"name":"@boring-stack-pkg/eslint-plugin-stripe-webhooks","version":"0.1.1","keywords":["eslint","eslintplugin","typescript","stripe","webhook","webhooks","security"],"author":"","license":"MIT","_id":"@boring-stack-pkg/eslint-plugin-stripe-webhooks@0.1.1","maintainers":[{"name":"agjs","email":"hi@aleksandar.xyz"}],"homepage":"https://github.com/AI-Starter-Templates/eslint-plugins#readme","bugs":{"url":"https://github.com/AI-Starter-Templates/eslint-plugins/issues"},"dist":{"shasum":"054a9b48de04eedff44342b9d3c679e9b3dbfe5a","tarball":"https://registry.npmjs.org/@boring-stack-pkg/eslint-plugin-stripe-webhooks/-/eslint-plugin-stripe-webhooks-0.1.1.tgz","fileCount":25,"integrity":"sha512-OAdyysgXMcPDQJdlfq5q7Mp+B5mOP8/1Z0277E6ZMz93jjpamJIu2dBD0vtwEtLWGPnNI4MLvh1ARNW0/d55AA==","signatures":[{"sig":"MEQCIFWyY7dPy7oan94Y1ozvPqIgJvt36ygi2diEvfI10qFXAiBWX++k/9XhoJIgCyhmX4JejQD25garArlBeAvYxZ8+lw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@boring-stack-pkg%2feslint-plugin-stripe-webhooks@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":141052},"main":"./dist/index.cjs","type":"module","_from":"file:boring-stack-pkg-eslint-plugin-stripe-webhooks-0.1.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":"^20.0.0 || ^22.0.0 || >=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --clean","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"agjs","email":"hi@aleksandar.xyz"},"_resolved":"/tmp/d131716b391a17e93ae777717005718b/boring-stack-pkg-eslint-plugin-stripe-webhooks-0.1.1.tgz","_integrity":"sha512-OAdyysgXMcPDQJdlfq5q7Mp+B5mOP8/1Z0277E6ZMz93jjpamJIu2dBD0vtwEtLWGPnNI4MLvh1ARNW0/d55AA==","repository":{"url":"git+https://github.com/AI-Starter-Templates/eslint-plugins.git","type":"git","directory":"eslint-plugin-stripe-webhooks"},"_npmVersion":"10.9.7","description":"ESLint plugin enforcing security and correctness rules for Stripe webhooks.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.2","dependencies":{"@typescript-eslint/utils":"8.0.0"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","eslint":"9.0.0","vitest":"2.0.0","@eslint/js":"9.0.0","typescript":"6.0.3","@types/node":"22.0.0","@typescript-eslint/parser":"8.0.0","@typescript-eslint/rule-tester":"8.0.0"},"peerDependencies":{"eslint":"8.57.0 || ^9.0.0","typescript":">=5.0.0","@typescript-eslint/parser":">=8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/eslint-plugin-stripe-webhooks_0.1.1_1779219599934_0.3469380168439995","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@boring-stack-pkg/eslint-plugin-stripe-webhooks","version":"0.1.2","description":"ESLint plugin enforcing security and correctness rules for Stripe webhooks.","type":"module","license":"MIT","author":"","repository":{"type":"git","url":"git+https://github.com/boringstack-xyz/eslint-plugins.git","directory":"eslint-plugin-stripe-webhooks"},"publishConfig":{"access":"public"},"sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"keywords":["eslint","eslintplugin","typescript","stripe","webhook","webhooks","security"],"peerDependencies":{"@typescript-eslint/parser":">=8.0.0","eslint":"8.57.0 || ^9.0.0","typescript":">=5.0.0"},"dependencies":{"@typescript-eslint/utils":"8.0.0"},"devDependencies":{"@eslint/js":"9.0.0","@types/node":"22.0.0","@typescript-eslint/parser":"8.0.0","@typescript-eslint/rule-tester":"8.0.0","eslint":"9.0.0","tsup":"8.0.0","typescript":"6.0.3","vitest":"2.0.0"},"engines":{"node":"^20.0.0 || ^22.0.0 || >=24.0.0"},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --clean","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest"},"_id":"@boring-stack-pkg/eslint-plugin-stripe-webhooks@0.1.2","bugs":{"url":"https://github.com/boringstack-xyz/eslint-plugins/issues"},"homepage":"https://github.com/boringstack-xyz/eslint-plugins#readme","_integrity":"sha512-C+/ro87xxLy9qhEgnZt4L0A8jZH5NSIIxrnYu0OlZMGgfQMOWBaMi8M/D3KVsrJ9GAASufWOnIP4iVFobYyrhQ==","_resolved":"/tmp/372f2dce8060d0dfeffa94a726035ff3/boring-stack-pkg-eslint-plugin-stripe-webhooks-0.1.2.tgz","_from":"file:boring-stack-pkg-eslint-plugin-stripe-webhooks-0.1.2.tgz","_nodeVersion":"22.22.3","_npmVersion":"11.15.0","dist":{"integrity":"sha512-C+/ro87xxLy9qhEgnZt4L0A8jZH5NSIIxrnYu0OlZMGgfQMOWBaMi8M/D3KVsrJ9GAASufWOnIP4iVFobYyrhQ==","shasum":"a6cfa9bf670bba9bfd4c8a0f4fbd59c3a75d02da","tarball":"https://registry.npmjs.org/@boring-stack-pkg/eslint-plugin-stripe-webhooks/-/eslint-plugin-stripe-webhooks-0.1.2.tgz","fileCount":25,"unpackedSize":141817,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEbtynJGC6L2GrsmO7Dn2AOuiKYH1mhIjTigzpcvjQgGAiAcl0t/Yc+d4yCQPm686xngIJGAL1tjubTpC5J0FETniw=="}]},"_npmUser":{"name":"agjs","email":"hi@aleksandar.xyz"},"directories":{},"maintainers":[{"name":"agjs","email":"hi@aleksandar.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/eslint-plugin-stripe-webhooks_0.1.2_1779695646852_0.7973682244554401"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T19:39:59.793Z","modified":"2026-05-25T07:54:07.156Z","0.1.1":"2026-05-19T19:40:00.147Z","0.1.2":"2026-05-25T07:54:06.985Z"},"bugs":{"url":"https://github.com/boringstack-xyz/eslint-plugins/issues"},"license":"MIT","homepage":"https://github.com/boringstack-xyz/eslint-plugins#readme","keywords":["eslint","eslintplugin","typescript","stripe","webhook","webhooks","security"],"repository":{"type":"git","url":"git+https://github.com/boringstack-xyz/eslint-plugins.git","directory":"eslint-plugin-stripe-webhooks"},"description":"ESLint plugin enforcing security and correctness rules for Stripe webhooks.","maintainers":[{"name":"agjs","email":"hi@aleksandar.xyz"}],"readme":"# eslint-plugin-stripe-webhooks\n\n[![npm](https://img.shields.io/npm/v/@boring-stack-pkg/eslint-plugin-stripe-webhooks?logo=npm)](https://www.npmjs.com/package/@boring-stack-pkg/eslint-plugin-stripe-webhooks) [![source](https://img.shields.io/badge/source-github-blue?logo=github)](https://github.com/boringstack-xyz/eslint-plugins/tree/main/eslint-plugin-stripe-webhooks)\n\nESLint plugin enforcing security and correctness rules for Stripe webhook handlers.\n\n## Why\n\nStripe webhooks have two infamous failure modes:\n\n1. **Unverified payloads.** Reading or parsing the request body before `*.constructEvent(...)` succeeds means an attacker can deliver a webhook-shaped payload to your endpoint and your handler will fire database writes / queue jobs / emails on it. The signature check never gets a chance to reject it.\n2. **Non-idempotent and type-blind handling.** Stripe redelivers events on transient failures — a handler without a dedupe check on `event.id` will double-charge, double-email, double-publish. A handler that doesn't branch on `event.type` will run identical logic for every event kind.\n\nThese six rules pin down the patterns that prevent both.\n\n## Install\n\n```sh\npnpm add -D @boring-stack-pkg/eslint-plugin-stripe-webhooks @typescript-eslint/parser\n```\n\n## Usage (flat config)\n\n```js\n// eslint.config.mjs\nimport tsParser from \"@typescript-eslint/parser\";\nimport stripeWebhooks from \"@boring-stack-pkg/eslint-plugin-stripe-webhooks\";\n\nexport default [\n  {\n    files: [\"**/*.{ts,tsx}\"],\n    languageOptions: {\n      parser: tsParser,\n      parserOptions: { ecmaVersion: \"latest\", sourceType: \"module\" },\n    },\n    plugins: { \"stripe-webhooks\": stripeWebhooks },\n    rules: stripeWebhooks.configs.recommended.rules,\n  },\n];\n```\n\nThe recommended preset enables all six rules at `\"error\"`. Override per-rule via the standard ESLint mechanism.\n\n## Rules\n\n| Rule                                                                                     | Tier                | Description                                                                                                                            |\n| ---------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |\n| [`handler-must-verify-signature`](docs/rules/handler-must-verify-signature.md)           | **TIER 1 SECURITY** | Disallow reading or forwarding the webhook payload before `*.constructEvent(...)` succeeds.                                            |\n| [`no-parsed-body-before-verification`](docs/rules/no-parsed-body-before-verification.md) | Security            | Disallow parsed-body APIs (`request.json()`, `JSON.parse(body)`, `req.body`, `express.json()`) before verification.                    |\n| [`require-stripe-signature-header`](docs/rules/require-stripe-signature-header.md)       | Security            | Require the signature passed into `constructEvent(...)` to come from the Stripe-signature header; forbid hard-coded `whsec_*` secrets. |\n| [`handler-must-handle-event-type`](docs/rules/handler-must-handle-event-type.md)         | Correctness         | Stripe event handlers must branch on `event.type`.                                                                                     |\n| [`handler-must-be-idempotent`](docs/rules/handler-must-be-idempotent.md)                 | Correctness         | Webhook handlers performing side effects must consult `event.id` for dedupe.                                                           |\n| [`service-must-construct-event`](docs/rules/service-must-construct-event.md)             | Convention          | Stripe-aware classes with a `webhook`-named method must also have a verifier method calling `constructEvent`.                          |\n\n## Examples\n\n### handler-must-verify-signature\n\n```ts\n// ❌\nexport async function POST(request: Request) {\n  const body = await request.json();\n  const event = stripe.webhooks.constructEvent(body, sig, k);\n}\n\n// ✅\nexport async function POST(request: Request) {\n  const body = await request.text();\n  const sig = request.headers.get(\"stripe-signature\");\n  const event = stripe.webhooks.constructEvent(\n    body,\n    sig,\n    process.env.WEBHOOK_SECRET!,\n  );\n}\n```\n\n### handler-must-be-idempotent\n\n```ts\n// ❌\nexport async function handle(event: Stripe.Event) {\n  if (event.type === \"payment_intent.succeeded\") {\n    await db.payments.insert({\n      /* ... */\n    });\n  }\n}\n\n// ✅\nexport async function handle(event: Stripe.Event) {\n  if (await alreadyProcessed(event.id)) return;\n  if (event.type === \"payment_intent.succeeded\") {\n    await db.payments.insert({\n      /* ... */\n    });\n  }\n}\n```\n\nFor complete per-rule docs and ❌/✅ snippets, see [`docs/rules/`](docs/rules/) and the runnable [`examples/`](examples/) (Next.js App Router, Express, Elysia/Hono, class-based services).\n\n## Security philosophy\n\nThese rules are **best-effort static analysis**. They catch the common mistakes — reading `req.body` before `constructEvent`, hard-coded `whsec_*` secrets, missing `event.type` branching — but they cannot prove a webhook handler is correct. Specifically, no rule here is sufficient on its own. Pair them with:\n\n- End-to-end tests against the [Stripe CLI](https://docs.stripe.com/cli) webhook simulator.\n- A TIER 1 security review of every webhook entry point at PR time.\n- Centralized verification (see `service-must-construct-event`) so the verification step lives in one place.\n\n## Limitations of static analysis\n\n- **No cross-function dataflow.** A body parameter passed into a helper in another file isn't tracked.\n- **No type-aware inference.** `Stripe.Event` parameter detection relies on the literal type name + a Stripe import being present in the same file. Generic wrapper types (`MyEvent<Stripe.Event>`) aren't recognized.\n- **Idempotency check detection is heuristic.** Helper functions abstracting the dedupe logic must be named per `allowedCheckFunctionPatterns`. DB unique-constraint-based dedupe isn't visible.\n- **Verification by middleware** that this rule doesn't recognize will look like \"no verification\" to the rule.\n\nWhen in doubt, prefer the explicit, in-handler `constructEvent` call over a clever abstraction — it's the pattern the rules optimize for.\n\n## Development\n\n```sh\npnpm install\npnpm test\npnpm typecheck\npnpm build\n```\n\n## Release\n\nTag a `v*` version locally and push the tag. `.github/workflows/release.yml` runs `pnpm publish --access public --no-git-checks` with `NPM_TOKEN`.\n\n## License\n\nMIT.\n","readmeFilename":"README.md"}