@skill-graph/cli 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/LICENSE +200 -0
- package/NOTICE +62 -0
- package/README.md +398 -0
- package/SKILL_GRAPH.md +443 -0
- package/bin/skill-graph.js +374 -0
- package/docs/ADOPTION.md +117 -0
- package/docs/CONFORMANCE.md +66 -0
- package/docs/PRIMER.md +384 -0
- package/docs/QUICKSTART-30MIN.md +333 -0
- package/docs/ROUTING-METRICS.md +120 -0
- package/docs/SKILL-MD-FORMAT-COMPATIBILITY.md +127 -0
- package/docs/SKILL_AUDIT_CHECKLIST.md +199 -0
- package/docs/SKILL_AUDIT_LOOP.md +195 -0
- package/docs/SKILL_METADATA_PROTOCOL.md +609 -0
- package/docs/_archived/marketplace-publication-priority-2026-05-18.md +239 -0
- package/docs/adr/0001-predicate-set.md +69 -0
- package/docs/adr/0002-json-ld-context.md +82 -0
- package/docs/adr/0003-ontoclean-rigidity-tags.md +65 -0
- package/docs/adr/0004-persistent-identifiers.md +74 -0
- package/docs/adr/0005-freshness-consolidation.md +70 -0
- package/docs/adr/0006-revise-predicate-rename.md +105 -0
- package/docs/adr/0007-audit-loop-cadence.md +99 -0
- package/docs/adr/0008-skill-surface-split-and-curation-policy.md +93 -0
- package/docs/category-consumers.md +168 -0
- package/docs/concept-map.md +194 -0
- package/docs/diagrams/drift-states.mmd +21 -0
- package/docs/diagrams/manifest-pipeline.mmd +25 -0
- package/docs/diagrams/routing-harness.mmd +41 -0
- package/docs/diagrams/starter-graph.mmd +53 -0
- package/docs/field-decision-guide.md +315 -0
- package/docs/field-rationale.md +211 -0
- package/docs/field-reference.generated.md +624 -0
- package/docs/field-reference.md +1426 -0
- package/docs/glossary.md +190 -0
- package/docs/head-noun-glossary.md +63 -0
- package/docs/images/audit-phases.png +0 -0
- package/docs/images/drift-states.png +0 -0
- package/docs/images/graded-mode.png +0 -0
- package/docs/images/manifest-pipeline.png +0 -0
- package/docs/images/routing-harness.png +0 -0
- package/docs/images/skill-anatomy.png +0 -0
- package/docs/images/starter-graph.png +0 -0
- package/docs/images/system-model.png +0 -0
- package/docs/integrations/github-actions.md +155 -0
- package/docs/manifest-field-mapping.md +443 -0
- package/docs/marketplace-publication-queue.generated.md +240 -0
- package/docs/marketplace-release-agent-prompt.md +82 -0
- package/docs/marketplace-skill-candidate-list.md +272 -0
- package/docs/marketplace-syndication.md +222 -0
- package/docs/migration-sample-review.md +155 -0
- package/docs/migrations/v4-to-v5.md +168 -0
- package/docs/migrations/v5-to-v6.md +221 -0
- package/docs/name-exceptions.yaml +37 -0
- package/docs/plans/marketplace-p1-public-migration-plan.md +41 -0
- package/docs/plans/multi-root-workspace.md +148 -0
- package/docs/plans/scripts-roadmap.md +107 -0
- package/docs/plans/v4-schema-bump.md +160 -0
- package/docs/plans/wave-2-extraction.md +122 -0
- package/docs/positioning-vs-marketplaces.md +175 -0
- package/docs/proposals/skill-audit-loop-positioning.md +160 -0
- package/docs/quality-doctrine.md +138 -0
- package/docs/recommended-skills.md +150 -0
- package/docs/research/skill-comprehension-eval-research.md +1830 -0
- package/docs/research/skill-retrieval-evidence.md +66 -0
- package/docs/skill-metadata-protocol.md +471 -0
- package/docs/skills-sh-maintainer-cleanup-request.md +80 -0
- package/examples/audits/a11y/findings.md +52 -0
- package/examples/audits/a11y/scorecard.md +21 -0
- package/examples/audits/a11y/verdict.md +44 -0
- package/examples/audits/debugging/findings.md +59 -0
- package/examples/audits/debugging/scorecard.md +22 -0
- package/examples/audits/debugging/verdict.md +33 -0
- package/examples/audits/documentation/findings.md +59 -0
- package/examples/audits/documentation/scorecard.md +22 -0
- package/examples/audits/documentation/verdict.md +33 -0
- package/examples/evals/a11y.json +140 -0
- package/examples/evals/api-design.json +52 -0
- package/examples/evals/code-review.json +52 -0
- package/examples/evals/data-modeling.json +52 -0
- package/examples/evals/database-migration.json +52 -0
- package/examples/evals/debugging.json +118 -0
- package/examples/evals/dependency-architecture.json +52 -0
- package/examples/evals/design-system-architecture.json +52 -0
- package/examples/evals/error-tracking.json +52 -0
- package/examples/evals/event-contract-design.json +52 -0
- package/examples/evals/form-ux-architecture.json +52 -0
- package/examples/evals/framework-fit-analysis.json +52 -0
- package/examples/evals/graph-audit.json +139 -0
- package/examples/evals/information-architecture.json +52 -0
- package/examples/evals/interaction-feedback.json +52 -0
- package/examples/evals/interaction-patterns.json +52 -0
- package/examples/evals/layout-composition.json +52 -0
- package/examples/evals/lint-overlay.json +117 -0
- package/examples/evals/microcopy.json +52 -0
- package/examples/evals/observability-modeling.json +52 -0
- package/examples/evals/pattern-recognition.json +96 -0
- package/examples/evals/performance-engineering.json +52 -0
- package/examples/evals/refactor.json +128 -0
- package/examples/evals/semiotics.json +52 -0
- package/examples/evals/skill-infrastructure.json +96 -0
- package/examples/evals/skill-router.json +140 -0
- package/examples/evals/skill-router.routing.json +113 -0
- package/examples/evals/system-interface-contracts.json +52 -0
- package/examples/evals/task-analysis.json +52 -0
- package/examples/evals/testing-strategy.json +118 -0
- package/examples/evals/type-safety.json +249 -0
- package/examples/evals/visual-design-foundations.json +52 -0
- package/examples/evals/webhook-integration.json +52 -0
- package/examples/exports/a11y.skill-md.md +80 -0
- package/examples/exports/debugging.skill-md.md +80 -0
- package/examples/exports/refactor.skill-md.md +78 -0
- package/examples/exports/testing-strategy.skill-md.md +81 -0
- package/examples/projects/markdown-static-site/README.md +115 -0
- package/examples/projects/markdown-static-site/skills/content-source-router/SKILL.md +131 -0
- package/examples/projects/markdown-static-site/skills/image-optimization-pipeline-config/SKILL.md +132 -0
- package/examples/projects/markdown-static-site/skills/link-rot-detection/SKILL.md +103 -0
- package/examples/projects/markdown-static-site/skills/markdown-post-frontmatter-validation/SKILL.md +133 -0
- package/examples/projects/markdown-static-site/skills/migrate-posts-to-v2-frontmatter/SKILL.md +140 -0
- package/examples/projects/saas-stripe-postgres/README.md +208 -0
- package/examples/projects/saas-stripe-postgres/db/migrations/0004_canonicalize_orders.sql +37 -0
- package/examples/projects/saas-stripe-postgres/db/schema.sql +112 -0
- package/examples/projects/saas-stripe-postgres/skills/migrate-orders-to-canonical-schema/SKILL.md +149 -0
- package/examples/projects/saas-stripe-postgres/skills/nextjs-server-action-validation/SKILL.md +154 -0
- package/examples/projects/saas-stripe-postgres/skills/payment-provider-router/SKILL.md +153 -0
- package/examples/projects/saas-stripe-postgres/skills/postgres-rls-pattern/SKILL.md +163 -0
- package/examples/projects/saas-stripe-postgres/skills/stripe-webhook-signature-verification/SKILL.md +137 -0
- package/examples/protocol/skill-metadata-template.md +301 -0
- package/examples/protocol/skills.manifest.sample.json +13245 -0
- package/examples/skill-metadata-template.md +317 -0
- package/examples/skills.manifest.sample.json +13519 -0
- package/examples/tests/v3-1-skos-fixture/SKILL.md +93 -0
- package/marketplace/README.md +17 -0
- package/marketplace/skills/a11y/SKILL.md +66 -0
- package/marketplace/skills/acid-fundamentals/SKILL.md +106 -0
- package/marketplace/skills/agent-engineering/SKILL.md +386 -0
- package/marketplace/skills/agent-eval-design/SKILL.md +55 -0
- package/marketplace/skills/ai-native-development/SKILL.md +294 -0
- package/marketplace/skills/api-design/SKILL.md +60 -0
- package/marketplace/skills/architecture-decision-records/SKILL.md +55 -0
- package/marketplace/skills/background-jobs/SKILL.md +265 -0
- package/marketplace/skills/bounded-context-mapping/SKILL.md +55 -0
- package/marketplace/skills/cap-theorem-tradeoffs/SKILL.md +127 -0
- package/marketplace/skills/client-server-boundary/SKILL.md +187 -0
- package/marketplace/skills/code-review/SKILL.md +120 -0
- package/marketplace/skills/color-system-design/SKILL.md +43 -0
- package/marketplace/skills/component-architecture/SKILL.md +126 -0
- package/marketplace/skills/compression/SKILL.md +112 -0
- package/marketplace/skills/conceptual-modeling/SKILL.md +181 -0
- package/marketplace/skills/connection-pooling/SKILL.md +105 -0
- package/marketplace/skills/constraint-awareness/SKILL.md +287 -0
- package/marketplace/skills/content-monitor/SKILL.md +209 -0
- package/marketplace/skills/context-engineering/SKILL.md +320 -0
- package/marketplace/skills/context-graph/SKILL.md +174 -0
- package/marketplace/skills/context-management/SKILL.md +174 -0
- package/marketplace/skills/context-window/SKILL.md +239 -0
- package/marketplace/skills/contract-testing/SKILL.md +120 -0
- package/marketplace/skills/cron-scheduling/SKILL.md +223 -0
- package/marketplace/skills/dark-mode-implementation/SKILL.md +47 -0
- package/marketplace/skills/data-modeling/SKILL.md +59 -0
- package/marketplace/skills/data-modeling-fundamentals/SKILL.md +117 -0
- package/marketplace/skills/database-migration/SKILL.md +429 -0
- package/marketplace/skills/debugging/SKILL.md +67 -0
- package/marketplace/skills/dependency-architecture/SKILL.md +58 -0
- package/marketplace/skills/design-module-composition/SKILL.md +43 -0
- package/marketplace/skills/design-system-architecture/SKILL.md +61 -0
- package/marketplace/skills/design-thinking/SKILL.md +44 -0
- package/marketplace/skills/diagnosis/SKILL.md +296 -0
- package/marketplace/skills/diff-analysis/SKILL.md +188 -0
- package/marketplace/skills/e2e-test-design/SKILL.md +113 -0
- package/marketplace/skills/entity-relationship-modeling/SKILL.md +218 -0
- package/marketplace/skills/epistemic-grounding/SKILL.md +112 -0
- package/marketplace/skills/error-boundary/SKILL.md +235 -0
- package/marketplace/skills/error-tracking/SKILL.md +261 -0
- package/marketplace/skills/eval-driven-development/SKILL.md +147 -0
- package/marketplace/skills/evaluation/SKILL.md +113 -0
- package/marketplace/skills/event-contract-design/SKILL.md +60 -0
- package/marketplace/skills/event-storming/SKILL.md +56 -0
- package/marketplace/skills/form-ux-architecture/SKILL.md +60 -0
- package/marketplace/skills/framework-fit-analysis/SKILL.md +59 -0
- package/marketplace/skills/frontend-architecture/SKILL.md +43 -0
- package/marketplace/skills/generative-ui/SKILL.md +118 -0
- package/marketplace/skills/graph-audit/SKILL.md +81 -0
- package/marketplace/skills/guardrails/SKILL.md +118 -0
- package/marketplace/skills/hooks-patterns/SKILL.md +185 -0
- package/marketplace/skills/http-semantics/SKILL.md +136 -0
- package/marketplace/skills/ideation/SKILL.md +41 -0
- package/marketplace/skills/indexing-strategy/SKILL.md +108 -0
- package/marketplace/skills/information-architecture/SKILL.md +59 -0
- package/marketplace/skills/integration-test-design/SKILL.md +111 -0
- package/marketplace/skills/intent-recognition/SKILL.md +136 -0
- package/marketplace/skills/interaction-feedback/SKILL.md +59 -0
- package/marketplace/skills/interaction-patterns/SKILL.md +59 -0
- package/marketplace/skills/journey-mapping/SKILL.md +41 -0
- package/marketplace/skills/keywords/SKILL.md +213 -0
- package/marketplace/skills/knowledge-modeling/SKILL.md +232 -0
- package/marketplace/skills/layout-composition/SKILL.md +59 -0
- package/marketplace/skills/linguistics/SKILL.md +429 -0
- package/marketplace/skills/lint-overlay/SKILL.md +76 -0
- package/marketplace/skills/mental-models/SKILL.md +126 -0
- package/marketplace/skills/merge-queue/SKILL.md +94 -0
- package/marketplace/skills/methodology/SKILL.md +317 -0
- package/marketplace/skills/microcopy/SKILL.md +232 -0
- package/marketplace/skills/middleware-patterns/SKILL.md +363 -0
- package/marketplace/skills/mobile-responsive-ux/SKILL.md +287 -0
- package/marketplace/skills/mutation-testing/SKILL.md +112 -0
- package/marketplace/skills/naming-conventions/SKILL.md +112 -0
- package/marketplace/skills/observability-modeling/SKILL.md +59 -0
- package/marketplace/skills/ontology-modeling/SKILL.md +67 -0
- package/marketplace/skills/owasp-security/SKILL.md +153 -0
- package/marketplace/skills/pattern-recognition/SKILL.md +472 -0
- package/marketplace/skills/performance-budgets/SKILL.md +185 -0
- package/marketplace/skills/performance-engineering/SKILL.md +58 -0
- package/marketplace/skills/performance-testing/SKILL.md +125 -0
- package/marketplace/skills/printify/SKILL.md +42 -0
- package/marketplace/skills/prioritization/SKILL.md +118 -0
- package/marketplace/skills/problem-framing/SKILL.md +41 -0
- package/marketplace/skills/problem-locating-solving/SKILL.md +203 -0
- package/marketplace/skills/project-knowledge-extraction/SKILL.md +54 -0
- package/marketplace/skills/prompt-craft/SKILL.md +134 -0
- package/marketplace/skills/prompt-injection-defense/SKILL.md +132 -0
- package/marketplace/skills/property-based-testing/SKILL.md +100 -0
- package/marketplace/skills/prototyping/SKILL.md +43 -0
- package/marketplace/skills/query-optimization/SKILL.md +144 -0
- package/marketplace/skills/real-time-updates/SKILL.md +324 -0
- package/marketplace/skills/ref-patterns/SKILL.md +284 -0
- package/marketplace/skills/refactor/SKILL.md +65 -0
- package/marketplace/skills/rendering-models/SKILL.md +142 -0
- package/marketplace/skills/replication-patterns/SKILL.md +110 -0
- package/marketplace/skills/research-synthesis/SKILL.md +41 -0
- package/marketplace/skills/route-handler-design/SKILL.md +347 -0
- package/marketplace/skills/schema-evolution/SKILL.md +140 -0
- package/marketplace/skills/security-fundamentals/SKILL.md +139 -0
- package/marketplace/skills/semantic-center/SKILL.md +194 -0
- package/marketplace/skills/semantic-relations/SKILL.md +250 -0
- package/marketplace/skills/semantics/SKILL.md +366 -0
- package/marketplace/skills/semiotics/SKILL.md +230 -0
- package/marketplace/skills/seo-strategy/SKILL.md +260 -0
- package/marketplace/skills/server-actions-design/SKILL.md +243 -0
- package/marketplace/skills/server-components-design/SKILL.md +190 -0
- package/marketplace/skills/sharding-strategy/SKILL.md +123 -0
- package/marketplace/skills/shopify/SKILL.md +42 -0
- package/marketplace/skills/skill-infrastructure/SKILL.md +320 -0
- package/marketplace/skills/skill-router/SKILL.md +71 -0
- package/marketplace/skills/skill-scaffold/SKILL.md +105 -0
- package/marketplace/skills/snapshot-testing/SKILL.md +120 -0
- package/marketplace/skills/spec-driven-development/SKILL.md +148 -0
- package/marketplace/skills/state-machine-modeling/SKILL.md +56 -0
- package/marketplace/skills/state-management/SKILL.md +134 -0
- package/marketplace/skills/streaming-architecture/SKILL.md +194 -0
- package/marketplace/skills/summarization/SKILL.md +156 -0
- package/marketplace/skills/suspense-patterns/SKILL.md +265 -0
- package/marketplace/skills/system-interface-contracts/SKILL.md +59 -0
- package/marketplace/skills/task-analysis/SKILL.md +201 -0
- package/marketplace/skills/taxonomy-design/SKILL.md +66 -0
- package/marketplace/skills/test-coverage-strategy/SKILL.md +108 -0
- package/marketplace/skills/test-doubles-design/SKILL.md +98 -0
- package/marketplace/skills/test-driven-development/SKILL.md +96 -0
- package/marketplace/skills/testing-strategy/SKILL.md +67 -0
- package/marketplace/skills/theme-system-design/SKILL.md +43 -0
- package/marketplace/skills/tool-call-flow/SKILL.md +229 -0
- package/marketplace/skills/tool-call-strategy/SKILL.md +292 -0
- package/marketplace/skills/transaction-isolation/SKILL.md +98 -0
- package/marketplace/skills/type-safety/SKILL.md +177 -0
- package/marketplace/skills/typography-system/SKILL.md +43 -0
- package/marketplace/skills/usability-testing/SKILL.md +43 -0
- package/marketplace/skills/user-research/SKILL.md +43 -0
- package/marketplace/skills/vercel-composition-patterns/SKILL.md +157 -0
- package/marketplace/skills/version-control/SKILL.md +233 -0
- package/marketplace/skills/visual-design-foundations/SKILL.md +59 -0
- package/marketplace/skills/visual-hierarchy/SKILL.md +43 -0
- package/marketplace/skills/webhook-integration/SKILL.md +331 -0
- package/marketplace/skills/writing-humanizer/SKILL.md +380 -0
- package/package.json +67 -0
- package/schemas/manifest.schema.json +811 -0
- package/schemas/manifest.v2.schema.json +164 -0
- package/schemas/manifest.v3.schema.json +758 -0
- package/schemas/manifest.v4.schema.json +755 -0
- package/schemas/manifest.v5.schema.json +755 -0
- package/schemas/manifest.v6.schema.json +811 -0
- package/schemas/skill.context.jsonld +279 -0
- package/schemas/skill.schema.json +919 -0
- package/schemas/skill.v2.schema.json +201 -0
- package/schemas/skill.v3.schema.json +827 -0
- package/schemas/skill.v4.schema.json +822 -0
- package/schemas/skill.v5.schema.json +830 -0
- package/schemas/skill.v6.schema.json +946 -0
- package/schemas/vocabulary/keywords.json +180 -0
- package/schemas/vocabulary/workspace_tags.json +23 -0
- package/scripts/__tests__/migrate-skill-v2-to-v3.test.js +161 -0
- package/scripts/__tests__/migrate-skill-v3-to-v4.test.js +158 -0
- package/scripts/__tests__/test-export-parser-drift.js +149 -0
- package/scripts/__tests__/test-marketplace-export.js +114 -0
- package/scripts/__tests__/test-router-paths.js +82 -0
- package/scripts/__tests__/test-stability-promotion.js +244 -0
- package/scripts/__tests__/test-v3-1-alias-contract.js +109 -0
- package/scripts/__tests__/test-v3-1-skos-runtime.js +116 -0
- package/scripts/backfill-schema-version.js +198 -0
- package/scripts/build-field-reference.js +160 -0
- package/scripts/build-retrieval-baseline.js +511 -0
- package/scripts/check-markdown-links.js +211 -0
- package/scripts/check-protocol-consistency.js +979 -0
- package/scripts/export-marketplace-skills.js +610 -0
- package/scripts/export-skill.js +374 -0
- package/scripts/generate-manifest.js +787 -0
- package/scripts/lib/alias-contract.js +83 -0
- package/scripts/lib/audit-prompt-builder.js +771 -0
- package/scripts/lib/mock-grader.js +134 -0
- package/scripts/lib/parse-frontmatter.js +429 -0
- package/scripts/lib/roots.js +119 -0
- package/scripts/lint/check-archetype-sections.js +185 -0
- package/scripts/lint/check-category-enum.js +83 -0
- package/scripts/lint/check-routing-eval.js +146 -0
- package/scripts/lint/check-routing-quality.js +211 -0
- package/scripts/lint/check-stability-promotion.js +220 -0
- package/scripts/lint/format-code-frame.js +206 -0
- package/scripts/marketplace-install.js +125 -0
- package/scripts/migrate-category-to-enum.js +169 -0
- package/scripts/migrate-skill-v2-to-v3.js +424 -0
- package/scripts/migrate-skill-v3-to-v4.js +200 -0
- package/scripts/migrate-skill-v5-to-v6.js +304 -0
- package/scripts/restructure-by-category.js +85 -0
- package/scripts/seed-publication-classification.js +282 -0
- package/scripts/skill-audit.js +893 -0
- package/scripts/skill-graph-drift.js +483 -0
- package/scripts/skill-graph-route.js +766 -0
- package/scripts/skill-graph-routing-eval.js +393 -0
- package/scripts/skill-lint.js +1317 -0
- package/scripts/skill-overlap.js +213 -0
- package/scripts/verify-skill-md-export.js +201 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webhook-integration
|
|
3
|
+
description: "Use when implementing or reviewing an inbound webhook handler for any third-party provider - verifying signatures, deduplicating retries, choosing the right HTTP status code for retry vs no-retry, persisting raw payloads before canonical mapping, and quarantining unverifiable events. Covers signature schemes, idempotency patterns, provider retry contracts, raw-then-canonical pipelines, quarantine, secret rotation, and PII-capture timing. Do NOT use for outbound webhook publishing (use `event-contract-design`), general background-job orchestration, or chasing a webhook handler that has already failed in production (use `debugging`)."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: "Provider-agnostic. Examples reference HMAC-SHA256 (the dominant scheme), SDK-style verification helpers (Stripe-style, where the provider ships a library that takes raw body + header + secret), and round-trip verification APIs (PayPal-style, where the receiver POSTs the event back to the provider for validation). Substitute each provider's specific header names, hashing algorithm, and retry-status-code contract from their docs."
|
|
6
|
+
allowed-tools: Read Grep Bash Edit
|
|
7
|
+
metadata:
|
|
8
|
+
metadata: "{\"schema_version\":6,\"version\":\"1.0.0\",\"type\":\"capability\",\"category\":\"engineering\",\"domain\":\"integrations/webhooks\",\"scope\":\"portable\",\"owner\":\"skill-graph-maintainer\",\"freshness\":\"2026-05-06\",\"drift_check\":\"{\\\\\\\"last_verified\\\\\\\":\\\\\\\"2026-05-06\\\\\\\"}\",\"eval_artifacts\":\"present\",\"eval_state\":\"unverified\",\"routing_eval\":\"absent\",\"stability\":\"experimental\",\"keywords\":\"[\\\\\\\"webhook handler\\\\\\\",\\\\\\\"webhook signature\\\\\\\",\\\\\\\"webhook signature verification\\\\\\\",\\\\\\\"HMAC webhook\\\\\\\",\\\\\\\"timing-safe comparison\\\\\\\",\\\\\\\"duplicate webhook delivery\\\\\\\",\\\\\\\"webhook idempotency\\\\\\\",\\\\\\\"idempotency key\\\\\\\",\\\\\\\"webhook retry\\\\\\\",\\\\\\\"retry contract\\\\\\\",\\\\\\\"webhook 200 vs 500\\\\\\\",\\\\\\\"replay attack webhook\\\\\\\",\\\\\\\"webhook quarantine\\\\\\\",\\\\\\\"raw payload archive\\\\\\\",\\\\\\\"webhook canonical mapping\\\\\\\",\\\\\\\"PII capture window\\\\\\\",\\\\\\\"webhook secret rotation\\\\\\\",\\\\\\\"inbound event handler\\\\\\\"]\",\"examples\":\"[\\\\\\\"implement signature verification for a new third-party webhook handler\\\\\\\",\\\\\\\"the same webhook event is being processed twice — fix the idempotency\\\\\\\",\\\\\\\"should I return 200 or 500 when a webhook handler hits a database error?\\\\\\\",\\\\\\\"the provider keeps retrying a webhook we already accepted — what's wrong with our 200 path?\\\\\\\",\\\\\\\"design a quarantine path for webhooks that fail signature verification\\\\\\\",\\\\\\\"extract a stable idempotency key from this provider's webhook payload\\\\\\\",\\\\\\\"reject all webhook deliveries with an invalid HMAC, log them for audit\\\\\\\",\\\\\\\"the provider deletes customer data 30 days after order — how do I capture PII safely on first delivery?\\\\\\\"]\",\"anti_examples\":\"[\\\\\\\"design our outbound webhook product (we want to deliver events to customers)\\\\\\\",\\\\\\\"the production webhook is failing — find the root cause\\\\\\\",\\\\\\\"explain our webhook patterns in the contributor docs\\\\\\\",\\\\\\\"review this AI-generated webhook handler for correctness\\\\\\\",\\\\\\\"refactor the webhook handler helpers for clarity\\\\\\\",\\\\\\\"decide whether this webhook needs an integration test\\\\\\\",\\\\\\\"design the secret-rotation policy for our integration credentials\\\\\\\"]\",\"relations\":\"{\\\\\\\"boundary\\\\\\\":[{\\\\\\\"skill\\\\\\\":\\\\\\\"event-contract-design\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"event-contract-design owns outbound event and webhook contracts; webhook-integration owns inbound third-party handler mechanics\\\\\\\"},{\\\\\\\"skill\\\\\\\":\\\\\\\"debugging\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"debugging chases an observed handler failure with reproduction; webhook-integration plans the safe handler shape before deployment\\\\\\\"},{\\\\\\\"skill\\\\\\\":\\\\\\\"refactor\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"refactor is behavior-preserving cleanup; webhook-integration is the contract-enforcement layer that decides what behavior the handler must preserve\\\\\\\"},{\\\\\\\"skill\\\\\\\":\\\\\\\"owasp-security\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"owasp-security owns the secret-storage and rotation policy; webhook-integration owns the per-request signature-verification mechanics that consume those secrets\\\\\\\"},{\\\\\\\"skill\\\\\\\":\\\\\\\"testing-strategy\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"testing-strategy decides what deserves a regression test; webhook-integration defines the failure modes (replay, signature mismatch, duplicate delivery) those tests target\\\\\\\"}],\\\\\\\"related\\\\\\\":[\\\\\\\"testing-strategy\\\\\\\",\\\\\\\"debugging\\\\\\\",\\\\\\\"owasp-security\\\\\\\",\\\\\\\"code-review\\\\\\\",\\\\\\\"event-contract-design\\\\\\\"],\\\\\\\"verify_with\\\\\\\":[\\\\\\\"testing-strategy\\\\\\\",\\\\\\\"code-review\\\\\\\"]}\",\"portability\":\"{\\\\\\\"readiness\\\\\\\":\\\\\\\"scripted\\\\\\\",\\\\\\\"targets\\\\\\\":[\\\\\\\"skill-md\\\\\\\"]}\",\"lifecycle\":\"{\\\\\\\"stale_after_days\\\\\\\":90,\\\\\\\"review_cadence\\\\\\\":\\\\\\\"quarterly\\\\\\\"}\",\"skill_graph_source_repo\":\"https://github.com/jacob-balslev/skill-graph\",\"skill_graph_protocol\":\"Skill Metadata Protocol v5\",\"skill_graph_project\":\"Skill Graph\",\"skill_graph_canonical_skill\":\"skills/webhook-integration/SKILL.md\"}"
|
|
9
|
+
skill_graph_source_repo: "https://github.com/jacob-balslev/skill-graph"
|
|
10
|
+
skill_graph_protocol: Skill Metadata Protocol v4
|
|
11
|
+
skill_graph_project: Skill Graph
|
|
12
|
+
skill_graph_canonical_skill: skills/webhook-integration/SKILL.md
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Webhook Integration
|
|
16
|
+
|
|
17
|
+
## Coverage
|
|
18
|
+
|
|
19
|
+
- The three signature-scheme families: shared-secret HMAC (dominant), provider-SDK verification helpers, and round-trip verification APIs — what each looks like, when each is used, and how to verify correctly
|
|
20
|
+
- The four idempotency patterns: processed-flag, payload-hash UPSERT, event-ID deduplication, composite-key UPSERT — and the selection rule for picking the right one for a given provider
|
|
21
|
+
- The retry-contract reading discipline: every provider has a documented (or undocumented) policy for how it interprets 4xx vs 5xx; the wrong status code on a duplicate or transient error silently loses events
|
|
22
|
+
- The raw-then-canonical pipeline: persist the raw payload before any mapping, so downstream bugs in canonical conversion never lose original data
|
|
23
|
+
- Quarantine for unverifiable events: where to put webhooks that fail signature verification or schema validation, and how to keep them auditable without polluting the main pipeline
|
|
24
|
+
- PII-capture timing: when a provider has a deletion window (GDPR-driven, often 30 days), the first webhook is the only reliable opportunity to persist customer data
|
|
25
|
+
- Webhook handler skeleton: the standard ordering of verify → parse → dedupe → process → respond, and why each step must precede the next
|
|
26
|
+
- Secret rotation hooks: the handler-side support for accepting both old and new secrets during a rotation window, without owning the rotation policy itself
|
|
27
|
+
|
|
28
|
+
## Philosophy
|
|
29
|
+
|
|
30
|
+
Webhooks are the primary real-time data ingestion channel for any application that integrates with third-party providers. When a webhook handler is wrong, the failure mode is *silent data loss* — events arrive, the handler returns the wrong status code, the provider stops retrying, and the data is gone with no error in your logs. There is no "exception thrown" symptom. The discipline is to treat the handler as a contract negotiation: the provider's retry policy is law, the signature scheme is law, and the handler's job is to honour both *exactly*, not approximately.
|
|
31
|
+
|
|
32
|
+
The dominant misconception is that REST conventions ("4xx for client error, 5xx for server error") apply uniformly. They do not. Several major providers interpret *any* 4xx as "your endpoint is permanently broken, stop retrying" — including a `409 Conflict` returned for a duplicate delivery, which is the natural REST-ish response. The result: returning a "semantically correct" 409 for a duplicate Stripe-style or HMAC-style webhook ends the retry loop on the *first* duplicate and silently drops every subsequent attempt. The right response for a duplicate is `200 OK` (with a body indicating it was already processed) — telling the provider "we have it, you can stop." For a transient database error, the right response is `500` — telling the provider "retry me." Neither is the REST default.
|
|
33
|
+
|
|
34
|
+
The second hard problem is timing. Most providers retry on a backoff schedule (one minute, ten minutes, an hour, a day), and most have a deadline beyond which they give up. If your handler does heavy work synchronously (calling third-party APIs, generating PDFs, sending email), the handler can time out, the provider sees a 504 from your gateway, and you start drifting through the retry budget without ever processing the event correctly. The discipline: verify, dedupe, persist the raw payload, return 200 fast, and dispatch the heavy work to a background queue that can fail and retry on its own schedule.
|
|
35
|
+
|
|
36
|
+
The third trap is forgetting that signature verification *must* run on the raw bytes of the request body, not on a parsed-and-re-serialized version. The moment you parse JSON and re-serialize it, you introduce whitespace and key-ordering changes that make the HMAC mismatch. Capture the raw body first, verify, then parse — never the other way around.
|
|
37
|
+
|
|
38
|
+
## Webhook Handler Skeleton
|
|
39
|
+
|
|
40
|
+
Every webhook handler should follow this exact ordering. Steps cannot be reordered without breaking either correctness or the retry contract.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
export async function POST(req: Request) {
|
|
44
|
+
// 1. Read the RAW body — bytes as delivered, before any parsing
|
|
45
|
+
const rawBody = await req.text();
|
|
46
|
+
|
|
47
|
+
// 2. Verify signature against the raw body
|
|
48
|
+
const signature = req.headers.get('x-provider-signature') ?? '';
|
|
49
|
+
if (!verifyProviderSignature(rawBody, signature, providerSecret)) {
|
|
50
|
+
// Quarantine for audit, then return permanent failure
|
|
51
|
+
await quarantineUnverifiableEvent(rawBody, signature, 'signature_mismatch');
|
|
52
|
+
return jsonResponse(401, { error: 'invalid_signature' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Parse the body now that it has been verified
|
|
56
|
+
let event;
|
|
57
|
+
try {
|
|
58
|
+
event = JSON.parse(rawBody);
|
|
59
|
+
} catch {
|
|
60
|
+
return jsonResponse(400, { error: 'malformed_json' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Idempotency check — extract a stable key, dedupe
|
|
64
|
+
const idempotencyKey = extractIdempotencyKey(req, event);
|
|
65
|
+
if (await isAlreadyProcessed(idempotencyKey)) {
|
|
66
|
+
return jsonResponse(200, { ok: true, skipped: true, reason: 'already_processed' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 5. Persist the raw payload BEFORE any canonical mapping
|
|
70
|
+
await persistRawEvent({ idempotencyKey, body: rawBody, receivedAt: new Date() });
|
|
71
|
+
|
|
72
|
+
// 6. Dispatch heavy work to a background queue; do not block the response
|
|
73
|
+
await enqueueEventProcessing({ idempotencyKey, event });
|
|
74
|
+
|
|
75
|
+
// 7. Mark processed and return 200 fast
|
|
76
|
+
await markProcessed(idempotencyKey);
|
|
77
|
+
return jsonResponse(200, { ok: true, received: true });
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Every step in this skeleton is load-bearing. Skipping signature verification opens the endpoint to forged events; parsing before verifying breaks signature correctness; processing synchronously blocks the response and risks timeouts; not persisting the raw body first means a bug in canonical mapping silently destroys the original data.
|
|
82
|
+
|
|
83
|
+
## Signature Schemes
|
|
84
|
+
|
|
85
|
+
There are three families. Identify which one your provider uses before writing a single line of handler code; verifying the wrong way fails open in subtle ways.
|
|
86
|
+
|
|
87
|
+
### Family 1: Shared-Secret HMAC (most common)
|
|
88
|
+
|
|
89
|
+
The provider sends a header containing an HMAC of the raw body, computed with a secret you both know. You recompute the HMAC and compare in constant time.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
93
|
+
|
|
94
|
+
function verifyHmacSha256(rawBody: string, signatureHeader: string, secret: string): boolean {
|
|
95
|
+
if (!signatureHeader || !secret) return false;
|
|
96
|
+
|
|
97
|
+
// Some providers prefix the hex signature with "sha256=" — strip if present
|
|
98
|
+
const expectedHex = signatureHeader.replace(/^sha256=/i, '');
|
|
99
|
+
|
|
100
|
+
const computedHex = createHmac('sha256', secret)
|
|
101
|
+
.update(rawBody, 'utf8')
|
|
102
|
+
.digest('hex');
|
|
103
|
+
|
|
104
|
+
// Buffers must be equal length for timingSafeEqual; bail early if not
|
|
105
|
+
if (expectedHex.length !== computedHex.length) return false;
|
|
106
|
+
|
|
107
|
+
return timingSafeEqual(
|
|
108
|
+
Buffer.from(expectedHex, 'hex'),
|
|
109
|
+
Buffer.from(computedHex, 'hex')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Critical details:
|
|
115
|
+
|
|
116
|
+
- **Constant-time comparison.** A naive `===` leaks the signature length and prefix via response timing, enabling a forge-by-timing attack. Always use `timingSafeEqual` (Node) or its equivalent.
|
|
117
|
+
- **Hash algorithm.** Most providers use HMAC-SHA256. A few still use HMAC-SHA1 (legacy GitHub) or HMAC-SHA512. Match the algorithm in the docs exactly — using SHA-1 against an SHA-256 signature looks like a mismatch.
|
|
118
|
+
- **Encoding.** Some providers send hex; others send base64. Decode according to the docs, not a guess.
|
|
119
|
+
- **Header prefix.** GitHub uses `sha256=<hex>`; Shopify uses bare base64; Slack uses `v0=<hex>`. Read the docs and strip exactly what the provider sends.
|
|
120
|
+
- **Body encoding.** The HMAC is over the *bytes* of the request body. If your framework auto-parses JSON before you can grab the raw body, the verification will fail because re-serialized JSON differs from the original bytes.
|
|
121
|
+
|
|
122
|
+
### Family 2: Provider SDK Verification (Stripe-style)
|
|
123
|
+
|
|
124
|
+
Some providers ship a verification function in their SDK that takes the raw body, the header, and the secret, and returns either a parsed event object or throws. Use the SDK call directly — do not reimplement.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Pseudocode shape — substitute the actual SDK import for your provider
|
|
128
|
+
import { providerSdk } from 'provider-sdk';
|
|
129
|
+
|
|
130
|
+
let event;
|
|
131
|
+
try {
|
|
132
|
+
event = providerSdk.webhooks.constructEvent(rawBody, signatureHeader, secret);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
await quarantineUnverifiableEvent(rawBody, signatureHeader, 'sdk_construct_failed');
|
|
135
|
+
return jsonResponse(401, { error: 'invalid_signature' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// `event` is now both verified AND parsed — proceed to idempotency
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The SDK form is the safer default when available because it encapsulates timestamp tolerance (rejecting replays older than N minutes), version checks, and the exact algorithm + encoding combination.
|
|
142
|
+
|
|
143
|
+
### Family 3: Round-Trip Verification API (PayPal-style)
|
|
144
|
+
|
|
145
|
+
A few providers do not give you a secret. Instead, on receiving a webhook, you POST a subset of the headers and the body back to a verification endpoint, and they return a yes/no.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const verification = await fetch(`${providerBaseUrl}/v1/notifications/verify-webhook-signature`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { Authorization: `Bearer ${oauthToken}`, 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
auth_algo: req.headers.get('auth-algo'),
|
|
153
|
+
cert_url: req.headers.get('cert-url'),
|
|
154
|
+
transmission_id: req.headers.get('transmission-id'),
|
|
155
|
+
transmission_sig: req.headers.get('transmission-sig'),
|
|
156
|
+
transmission_time: req.headers.get('transmission-time'),
|
|
157
|
+
webhook_id: providerWebhookId,
|
|
158
|
+
webhook_event: JSON.parse(rawBody),
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const { verification_status } = await verification.json();
|
|
163
|
+
if (verification_status !== 'SUCCESS') {
|
|
164
|
+
await quarantineUnverifiableEvent(rawBody, '<round-trip>', 'verification_api_rejected');
|
|
165
|
+
return jsonResponse(401, { error: 'invalid_signature' });
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Round-trip verification adds a network hop and an external dependency to the hot path. If the provider's verification API is degraded, your webhook endpoint is degraded too. Plan accordingly: a short timeout on the verification call, a quarantine path for verification timeouts, and a circuit breaker if the verification endpoint is consistently slow.
|
|
170
|
+
|
|
171
|
+
## Idempotency Patterns
|
|
172
|
+
|
|
173
|
+
Duplicate delivery is *normal*, not an error. Every provider retries on transient failures, and you will see the same event twice, ten times, or hundreds of times during a network blip. The handler must dedupe.
|
|
174
|
+
|
|
175
|
+
Pick one of these four patterns based on what the provider's payload gives you:
|
|
176
|
+
|
|
177
|
+
| Pattern | When to use | How |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| **Processed-flag** | Provider gives you a stable event ID and you want to record per-event metadata | Insert event with `processed_at = NULL`; on second delivery, check `processed_at` is non-null and return 200 skipped |
|
|
180
|
+
| **Payload-hash UPSERT** | Provider gives you no stable ID, but the same logical event always serializes identically | Compute SHA-256 of canonical payload; UPSERT with `ON CONFLICT (hash) DO NOTHING`; affected-rows = 0 means duplicate |
|
|
181
|
+
| **Event-ID dedup** | Provider gives a UUID per delivery; you only need to know it was seen | Track in a small `seen_event_ids (id, received_at)` table with a TTL of (provider's retry window + buffer) |
|
|
182
|
+
| **Composite-key UPSERT** | Same logical entity arrives across multiple providers | UPSERT on `(source_provider, external_id)` so each provider has its own idempotent slot |
|
|
183
|
+
|
|
184
|
+
The processed-flag and event-ID-dedup patterns are the cleanest for new handlers. Reach for payload-hash only when the provider gives you no ID at all (rare on modern APIs).
|
|
185
|
+
|
|
186
|
+
A worked example of processed-flag for a provider that ships a stable `event.id`:
|
|
187
|
+
|
|
188
|
+
```sql
|
|
189
|
+
-- Idempotent insert: returns true if this is the first time we have seen this event
|
|
190
|
+
INSERT INTO inbound_events (provider, event_id, raw_payload, received_at)
|
|
191
|
+
VALUES ($1, $2, $3, NOW())
|
|
192
|
+
ON CONFLICT (provider, event_id) DO NOTHING
|
|
193
|
+
RETURNING id;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If `RETURNING id` produces no row, the event was already inserted and you can return `200 { skipped: true }` immediately without re-running any side effects.
|
|
197
|
+
|
|
198
|
+
## Retry Contract Per Provider
|
|
199
|
+
|
|
200
|
+
This is the part agents get wrong most often. There is no universal answer; every provider has a documented retry policy and you must read each one and write it down before implementing the handler.
|
|
201
|
+
|
|
202
|
+
The questions to answer for each provider:
|
|
203
|
+
|
|
204
|
+
| Question | Why it matters |
|
|
205
|
+
|---|---|
|
|
206
|
+
| Which response codes trigger a retry? | Determines the right code for transient errors — usually `5xx`, sometimes `429`, occasionally specific codes only |
|
|
207
|
+
| Which response codes are treated as permanent failure? | Tells you the code for duplicates and bad payloads — usually `4xx`, but the *threshold* matters: some providers retry on `408` and `429` while treating `400-407` as permanent |
|
|
208
|
+
| What is the retry schedule? | Tells you how long the deduplication window must remain valid (a provider that retries for 24 hours requires 24 hours of seen-ID retention plus a buffer) |
|
|
209
|
+
| What is the request timeout? | Tells you how fast the handler must return; usually 5-30 seconds |
|
|
210
|
+
| Does the provider sign retried deliveries with the original timestamp or a fresh one? | Some signature schemes embed a timestamp; if retries use a fresh timestamp, replay protection at the receiver must be tolerant |
|
|
211
|
+
|
|
212
|
+
A worked example of writing this down for a hypothetical provider:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
Provider: example-vendor
|
|
216
|
+
Retry on: 500, 502, 503, 504, 408, 429
|
|
217
|
+
Permanent on: 400, 401, 403, 404, 409, 410, 422
|
|
218
|
+
Schedule: 2m, 10m, 1h, 6h, 24h (5 retries over 32 hours)
|
|
219
|
+
Timeout: 15 seconds
|
|
220
|
+
Signature time: fresh per retry (timestamp tolerance: 5 minutes)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
With this in hand, the handler's response strategy is mechanical:
|
|
224
|
+
|
|
225
|
+
| Handler condition | Response |
|
|
226
|
+
|---|---|
|
|
227
|
+
| Signature mismatch | `401` (permanent — replaying a bad signature gains nothing) |
|
|
228
|
+
| Malformed JSON | `400` (permanent — payload will not parse on retry) |
|
|
229
|
+
| Already processed (duplicate) | `200` with `{ skipped: true }` — *not* `409`, which would be permanent |
|
|
230
|
+
| Transient DB error during processing | `500` — provider retries |
|
|
231
|
+
| Persisted, dispatched to background queue | `200` with `{ received: true }` — provider stops, queue owns success/failure |
|
|
232
|
+
|
|
233
|
+
Returning `409` for a duplicate against this provider would silently lose every retry of that event after the first.
|
|
234
|
+
|
|
235
|
+
## Raw-Then-Canonical Pipeline
|
|
236
|
+
|
|
237
|
+
Persist the raw payload before any conversion to your application's canonical schema. Two reasons:
|
|
238
|
+
|
|
239
|
+
1. **Bugs in canonical mapping are common.** A new field, a renamed field, or a type-coercion edge case can lose data during conversion. If you persisted only the canonical version, the original is gone and you cannot reprocess. With the raw version stored, you can re-run the canonical mapper after fixing the bug and recover the data.
|
|
240
|
+
2. **Audit trail.** Disputes ("we never sent that order!") are resolved by replaying the raw bytes the provider sent, signed and timestamped. A canonical-only store loses that.
|
|
241
|
+
|
|
242
|
+
```sql
|
|
243
|
+
CREATE TABLE inbound_events (
|
|
244
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
245
|
+
provider TEXT NOT NULL,
|
|
246
|
+
event_id TEXT NOT NULL,
|
|
247
|
+
raw_payload JSONB NOT NULL,
|
|
248
|
+
raw_signature TEXT,
|
|
249
|
+
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
250
|
+
processed_at TIMESTAMPTZ,
|
|
251
|
+
UNIQUE (provider, event_id)
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Canonical mapping runs *after* the raw insert succeeds, often asynchronously in a background job. If the canonical mapper fails, the raw row remains and can be replayed.
|
|
256
|
+
|
|
257
|
+
## Quarantine for Unverifiable Events
|
|
258
|
+
|
|
259
|
+
Webhooks that fail signature verification or schema validation should not silently drop. Persist them to a quarantine table with the failure reason and the original headers — both for audit (was someone trying to forge events?) and for forensics (did we change a secret without coordinating?).
|
|
260
|
+
|
|
261
|
+
```sql
|
|
262
|
+
CREATE TABLE quarantined_events (
|
|
263
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
264
|
+
provider TEXT NOT NULL,
|
|
265
|
+
raw_payload TEXT,
|
|
266
|
+
raw_headers JSONB,
|
|
267
|
+
failure_reason TEXT NOT NULL, -- 'signature_mismatch' | 'malformed_json' | 'sdk_construct_failed' | ...
|
|
268
|
+
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
269
|
+
);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Quarantine inserts must be best-effort and never throw — if the quarantine table is unavailable, log to whatever logging your application uses and continue returning 401/400 to the provider. Do not let a quarantine failure cascade into a 500, which would trigger a retry of an event that should be permanently rejected.
|
|
273
|
+
|
|
274
|
+
## PII-Capture Timing
|
|
275
|
+
|
|
276
|
+
If a provider has a deletion window (GDPR-driven, often 30 days), the *first* webhook delivery is the only reliable opportunity to persist customer data. After the deletion window, calls to the provider's API will return redacted records.
|
|
277
|
+
|
|
278
|
+
The discipline:
|
|
279
|
+
|
|
280
|
+
- Capture all PII fields the provider includes in the webhook payload at the moment of receipt — email, phone, address, name — into your own storage, *not* a deferred "we'll fetch it later" step.
|
|
281
|
+
- The capture lives in the same transaction as the raw-event insert; if the insert succeeds, the PII is durably stored.
|
|
282
|
+
- Treat the PII as your application's data from that moment on; subsequent provider API calls cannot replace it.
|
|
283
|
+
|
|
284
|
+
This is one of the few cases where a synchronous side-effect inside the webhook handler is correct: deferring it to a background job is exactly the bug that causes data loss.
|
|
285
|
+
|
|
286
|
+
## Secret Rotation
|
|
287
|
+
|
|
288
|
+
Handlers should accept either the *current* or the *previous* secret during a rotation window. The handler does not own the rotation policy (that belongs to the security model), but it must support it:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
function verifyWithRotation(rawBody: string, sig: string, current: string, previous?: string): boolean {
|
|
292
|
+
if (verifyHmacSha256(rawBody, sig, current)) return true;
|
|
293
|
+
if (previous && verifyHmacSha256(rawBody, sig, previous)) return true;
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The previous secret stays valid for the provider's maximum retry window after rotation, then is removed. Without this, every rotation drops every event still in the retry queue from the old secret.
|
|
299
|
+
|
|
300
|
+
## Evals
|
|
301
|
+
|
|
302
|
+
This skill ships a comprehension-eval artifact at [`examples/evals/webhook-integration.json`](https://github.com/jacob-balslev/skill-graph/blob/main/examples/evals/webhook-integration.json). The checklist below is the authoring gate for inbound webhook-handler decisions; the eval file is the grader surface.
|
|
303
|
+
|
|
304
|
+
## Verification
|
|
305
|
+
|
|
306
|
+
- [ ] Signature verification runs on the *raw* request body, before any JSON parsing
|
|
307
|
+
- [ ] Comparison uses constant-time equality (`timingSafeEqual` or equivalent), not `===`
|
|
308
|
+
- [ ] The signature scheme matches the provider's documented algorithm, encoding, and header format exactly
|
|
309
|
+
- [ ] An idempotency key is extracted from each event and checked before any side effects run
|
|
310
|
+
- [ ] The retry-status-code table for each provider is documented in code comments or a per-provider README, not inferred
|
|
311
|
+
- [ ] Duplicate deliveries return `200 { skipped: true }`, never `409` or any other 4xx
|
|
312
|
+
- [ ] Transient errors (DB unavailable, downstream API down) return `5xx` so the provider retries
|
|
313
|
+
- [ ] Permanent errors (signature mismatch, malformed JSON, schema violation) return `4xx` and are quarantined
|
|
314
|
+
- [ ] The raw payload is persisted before canonical mapping; the canonical mapping runs in a background job
|
|
315
|
+
- [ ] Heavy processing is dispatched to a queue, not run synchronously in the handler
|
|
316
|
+
- [ ] Quarantine inserts cannot themselves throw and cascade into a 500
|
|
317
|
+
- [ ] If the provider has a deletion window, PII fields are captured synchronously on first delivery
|
|
318
|
+
- [ ] Secret rotation is supported by accepting both current and previous secrets during the rotation window
|
|
319
|
+
- [ ] At least one regression test simulates duplicate delivery; at least one simulates a forged signature
|
|
320
|
+
|
|
321
|
+
## Do NOT Use When
|
|
322
|
+
|
|
323
|
+
| Use instead | When |
|
|
324
|
+
|---|---|
|
|
325
|
+
| `documentation` | Writing the integration-conventions page for the contributor docs, or explaining your outbound webhook product to consumers |
|
|
326
|
+
| `event-contract-design` | Designing outbound webhook or async event schemas, envelopes, topics, compatibility, and consumer-facing fixtures |
|
|
327
|
+
| `debugging` | Chasing a webhook handler that is failing in production and reproducing the failure |
|
|
328
|
+
| `refactor` | Reorganizing webhook helper code or consolidating duplicated handler logic |
|
|
329
|
+
| `testing-strategy` | Deciding whether a particular webhook handler needs a regression test |
|
|
330
|
+
| `code-review` | Reviewing an AI-generated webhook handler for correctness |
|
|
331
|
+
| `owasp-security` | Designing the secret-storage and rotation policy itself, or auditing your application's webhook attack surface |
|