cfsa-antigravity 1.0.0
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/bin/cli.mjs +324 -0
- package/package.json +34 -0
- package/template/.agent/instructions/commands.md +48 -0
- package/template/.agent/instructions/patterns.md +61 -0
- package/template/.agent/instructions/structure.md +29 -0
- package/template/.agent/instructions/tech-stack.md +43 -0
- package/template/.agent/instructions/workflow.md +41 -0
- package/template/.agent/kit-sync.md +15 -0
- package/template/.agent/rules/boundary-not-placeholder.md +146 -0
- package/template/.agent/rules/completion-checklist.md +48 -0
- package/template/.agent/rules/decision-classification.md +103 -0
- package/template/.agent/rules/extensibility.md +47 -0
- package/template/.agent/rules/question-vs-command.md +81 -0
- package/template/.agent/rules/security-first.md +43 -0
- package/template/.agent/rules/specificity-standards.md +54 -0
- package/template/.agent/rules/tdd-contract-first.md +57 -0
- package/template/.agent/rules/vertical-slices.md +42 -0
- package/template/.agent/skill-library/MANIFEST.md +480 -0
- package/template/.agent/skill-library/README.md +38 -0
- package/template/.agent/skill-library/meta/brand-guidelines/SKILL.md +73 -0
- package/template/.agent/skill-library/meta/claude-code/README.md +9 -0
- package/template/.agent/skill-library/meta/claude-code/agent-development/SKILL.md +415 -0
- package/template/.agent/skill-library/meta/claude-code/hook-development/SKILL.md +712 -0
- package/template/.agent/skill-library/meta/claude-code/plugin-structure/SKILL.md +476 -0
- package/template/.agent/skill-library/meta/git-advanced/SKILL.md +972 -0
- package/template/.agent/skill-library/meta/mcp-builder/SKILL.md +236 -0
- package/template/.agent/skill-library/meta/product-marketing-context/SKILL.md +241 -0
- package/template/.agent/skill-library/meta/regex-patterns/SKILL.md +751 -0
- package/template/.agent/skill-library/meta/tmux-processes/SKILL.md +210 -0
- package/template/.agent/skill-library/meta/using-tmux-for-interactive-commands/SKILL.md +178 -0
- package/template/.agent/skill-library/stack/3d/threejs-pro/SKILL.md +300 -0
- package/template/.agent/skill-library/stack/ai/ai-sdk/SKILL.md +77 -0
- package/template/.agent/skill-library/stack/ai/langchain/SKILL.md +530 -0
- package/template/.agent/skill-library/stack/ai/ollama/SKILL.md +321 -0
- package/template/.agent/skill-library/stack/ai/openai-sdk/SKILL.md +549 -0
- package/template/.agent/skill-library/stack/analytics/google-analytics/SKILL.md +153 -0
- package/template/.agent/skill-library/stack/api/graphql/SKILL.md +1061 -0
- package/template/.agent/skill-library/stack/api/trpc/SKILL.md +576 -0
- package/template/.agent/skill-library/stack/auth/authjs/SKILL.md +569 -0
- package/template/.agent/skill-library/stack/auth/clerk/SKILL.md +590 -0
- package/template/.agent/skill-library/stack/auth/firebase-auth/SKILL.md +734 -0
- package/template/.agent/skill-library/stack/cms/payload-cms/SKILL.md +573 -0
- package/template/.agent/skill-library/stack/cms/shopify/SKILL.md +1193 -0
- package/template/.agent/skill-library/stack/cms/wordpress/SKILL.md +1104 -0
- package/template/.agent/skill-library/stack/css/sass-scss/SKILL.md +1121 -0
- package/template/.agent/skill-library/stack/css/tailwind-css-patterns/SKILL.md +863 -0
- package/template/.agent/skill-library/stack/css/tailwind-design-system/SKILL.md +490 -0
- package/template/.agent/skill-library/stack/css/vanilla-css/SKILL.md +1078 -0
- package/template/.agent/skill-library/stack/databases/clickhouse/SKILL.md +311 -0
- package/template/.agent/skill-library/stack/databases/influxdb/SKILL.md +280 -0
- package/template/.agent/skill-library/stack/databases/lancedb/SKILL.md +415 -0
- package/template/.agent/skill-library/stack/databases/mongodb/SKILL.md +1169 -0
- package/template/.agent/skill-library/stack/databases/neo4j/SKILL.md +839 -0
- package/template/.agent/skill-library/stack/databases/pgvector/SKILL.md +241 -0
- package/template/.agent/skill-library/stack/databases/pinecone/SKILL.md +212 -0
- package/template/.agent/skill-library/stack/databases/postgresql/SKILL.md +658 -0
- package/template/.agent/skill-library/stack/databases/qdrant/SKILL.md +312 -0
- package/template/.agent/skill-library/stack/databases/redis/SKILL.md +1079 -0
- package/template/.agent/skill-library/stack/databases/spacetimedb/SKILL.md +532 -0
- package/template/.agent/skill-library/stack/databases/sqlite/SKILL.md +1132 -0
- package/template/.agent/skill-library/stack/databases/supabase/SKILL.md +640 -0
- package/template/.agent/skill-library/stack/databases/surrealdb-expert/SKILL.md +945 -0
- package/template/.agent/skill-library/stack/databases/timescaledb/SKILL.md +745 -0
- package/template/.agent/skill-library/stack/databases/weaviate/SKILL.md +218 -0
- package/template/.agent/skill-library/stack/devops/github-actions/SKILL.md +554 -0
- package/template/.agent/skill-library/stack/devops/kubernetes/SKILL.md +950 -0
- package/template/.agent/skill-library/stack/devops/nginx/SKILL.md +841 -0
- package/template/.agent/skill-library/stack/devops/terraform/SKILL.md +860 -0
- package/template/.agent/skill-library/stack/email/resend/SKILL.md +391 -0
- package/template/.agent/skill-library/stack/engines/godot/SKILL.md +488 -0
- package/template/.agent/skill-library/stack/extensions/chrome-extension/SKILL.md +375 -0
- package/template/.agent/skill-library/stack/extensions/vscode-extension/SKILL.md +453 -0
- package/template/.agent/skill-library/stack/frameworks/astro-framework/SKILL.md +162 -0
- package/template/.agent/skill-library/stack/frameworks/electron/SKILL.md +1286 -0
- package/template/.agent/skill-library/stack/frameworks/fastapi/SKILL.md +650 -0
- package/template/.agent/skill-library/stack/frameworks/hono/SKILL.md +90 -0
- package/template/.agent/skill-library/stack/frameworks/nestjs/SKILL.md +878 -0
- package/template/.agent/skill-library/stack/frameworks/nextjs/SKILL.md +635 -0
- package/template/.agent/skill-library/stack/frameworks/nuxt/SKILL.md +564 -0
- package/template/.agent/skill-library/stack/frameworks/sveltekit/SKILL.md +614 -0
- package/template/.agent/skill-library/stack/frameworks/tauri/SKILL.md +920 -0
- package/template/.agent/skill-library/stack/gamedev/godot/SKILL.md +1032 -0
- package/template/.agent/skill-library/stack/gamedev/unity/SKILL.md +1175 -0
- package/template/.agent/skill-library/stack/hosting/aws/SKILL.md +467 -0
- package/template/.agent/skill-library/stack/hosting/cloudflare/SKILL.md +201 -0
- package/template/.agent/skill-library/stack/hosting/docker-expert/SKILL.md +409 -0
- package/template/.agent/skill-library/stack/hosting/vercel/SKILL.md +484 -0
- package/template/.agent/skill-library/stack/languages/bash-scripting/SKILL.md +773 -0
- package/template/.agent/skill-library/stack/languages/c-cpp/SKILL.md +712 -0
- package/template/.agent/skill-library/stack/languages/gdscript/SKILL.md +789 -0
- package/template/.agent/skill-library/stack/languages/go/SKILL.md +664 -0
- package/template/.agent/skill-library/stack/languages/java/SKILL.md +778 -0
- package/template/.agent/skill-library/stack/languages/kotlin/SKILL.md +665 -0
- package/template/.agent/skill-library/stack/languages/python/SKILL.md +678 -0
- package/template/.agent/skill-library/stack/languages/rust/SKILL.md +673 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/SKILL.md +141 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/advanced-generics.md +90 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/branded-types.md +57 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/builder-pattern.md +71 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/common-pitfalls.md +135 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/conditional-types.md +27 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/decorators.md +98 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/discriminated-unions.md +62 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/mapped-types.md +53 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/performance-best-practices.md +104 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/template-literal-types.md +49 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/testing-types.md +112 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/type-guards.md +70 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/type-inference.md +101 -0
- package/template/.agent/skill-library/stack/languages/typescript-advanced-patterns/references/utility-types.md +98 -0
- package/template/.agent/skill-library/stack/languages/vanilla-javascript/SKILL.md +803 -0
- package/template/.agent/skill-library/stack/messaging/kafka/SKILL.md +235 -0
- package/template/.agent/skill-library/stack/mobile/expo-react-native/SKILL.md +665 -0
- package/template/.agent/skill-library/stack/mobile/flutter/SKILL.md +316 -0
- package/template/.agent/skill-library/stack/mobile/react-native/SKILL.md +337 -0
- package/template/.agent/skill-library/stack/monitoring/posthog/SKILL.md +396 -0
- package/template/.agent/skill-library/stack/monitoring/sentry/SKILL.md +509 -0
- package/template/.agent/skill-library/stack/observability/datadog/SKILL.md +179 -0
- package/template/.agent/skill-library/stack/observability/distributed-tracing/SKILL.md +140 -0
- package/template/.agent/skill-library/stack/observability/logging-best-practices/SKILL.md +168 -0
- package/template/.agent/skill-library/stack/observability/opentelemetry/SKILL.md +164 -0
- package/template/.agent/skill-library/stack/observability/prometheus-grafana/SKILL.md +246 -0
- package/template/.agent/skill-library/stack/observability/python-observability/SKILL.md +158 -0
- package/template/.agent/skill-library/stack/orm/drizzle-orm/SKILL.md +613 -0
- package/template/.agent/skill-library/stack/orm/prisma/SKILL.md +744 -0
- package/template/.agent/skill-library/stack/payments/lemonsqueezy/SKILL.md +393 -0
- package/template/.agent/skill-library/stack/payments/stripe-integration/SKILL.md +457 -0
- package/template/.agent/skill-library/stack/queue/bullmq/SKILL.md +385 -0
- package/template/.agent/skill-library/stack/queue/inngest/SKILL.md +438 -0
- package/template/.agent/skill-library/stack/realtime/socketio/SKILL.md +595 -0
- package/template/.agent/skill-library/stack/search/elasticsearch/SKILL.md +248 -0
- package/template/.agent/skill-library/stack/search/meilisearch/SKILL.md +385 -0
- package/template/.agent/skill-library/stack/security/crypto-patterns/SKILL.md +437 -0
- package/template/.agent/skill-library/stack/security/csp-cors-headers/SKILL.md +588 -0
- package/template/.agent/skill-library/stack/security/dependency-auditing/SKILL.md +560 -0
- package/template/.agent/skill-library/stack/security/input-sanitization/SKILL.md +430 -0
- package/template/.agent/skill-library/stack/security/owasp-web-security/SKILL.md +421 -0
- package/template/.agent/skill-library/stack/state/tanstack-query/SKILL.md +637 -0
- package/template/.agent/skill-library/stack/state/zustand/SKILL.md +483 -0
- package/template/.agent/skill-library/stack/storage/aws-s3/SKILL.md +415 -0
- package/template/.agent/skill-library/stack/testing/playwright/SKILL.md +641 -0
- package/template/.agent/skill-library/stack/testing/storybook/SKILL.md +923 -0
- package/template/.agent/skill-library/stack/testing/testing-library/SKILL.md +872 -0
- package/template/.agent/skill-library/stack/testing/vitest/SKILL.md +714 -0
- package/template/.agent/skill-library/stack/ui/react-best-practices/SKILL.md +877 -0
- package/template/.agent/skill-library/stack/ui/react-composition-patterns/SKILL.md +1107 -0
- package/template/.agent/skill-library/stack/ui/react-flow/SKILL.md +425 -0
- package/template/.agent/skill-library/stack/ui/shadcn-ui/SKILL.md +703 -0
- package/template/.agent/skill-library/surface/api/api-caching/SKILL.md +458 -0
- package/template/.agent/skill-library/surface/api/api-documentation-openapi/SKILL.md +697 -0
- package/template/.agent/skill-library/surface/api/api-error-handling/SKILL.md +478 -0
- package/template/.agent/skill-library/surface/api/api-security-checklist/SKILL.md +147 -0
- package/template/.agent/skill-library/surface/api/api-versioning/SKILL.md +420 -0
- package/template/.agent/skill-library/surface/api/email-best-practices/SKILL.md +59 -0
- package/template/.agent/skill-library/surface/api/rate-limiting-abuse-protection/SKILL.md +147 -0
- package/template/.agent/skill-library/surface/api/rest-api-design/SKILL.md +478 -0
- package/template/.agent/skill-library/surface/api/webhook-design/SKILL.md +752 -0
- package/template/.agent/skill-library/surface/cli/cli-configuration-management/SKILL.md +445 -0
- package/template/.agent/skill-library/surface/cli/cli-error-diagnostics/SKILL.md +515 -0
- package/template/.agent/skill-library/surface/cli/cli-shell-integration/SKILL.md +479 -0
- package/template/.agent/skill-library/surface/cli/cli-ux-design/SKILL.md +477 -0
- package/template/.agent/skill-library/surface/desktop/desktop-app-distribution/SKILL.md +416 -0
- package/template/.agent/skill-library/surface/desktop/desktop-security-sandboxing/SKILL.md +407 -0
- package/template/.agent/skill-library/surface/desktop/desktop-ux-conventions/SKILL.md +361 -0
- package/template/.agent/skill-library/surface/desktop/native-os-integration/SKILL.md +563 -0
- package/template/.agent/skill-library/surface/extension/browser-extension-patterns/SKILL.md +482 -0
- package/template/.agent/skill-library/surface/extension/plugin-architecture-design/SKILL.md +632 -0
- package/template/.agent/skill-library/surface/extension/vscode-extension-development/SKILL.md +728 -0
- package/template/.agent/skill-library/surface/mobile/app-store-submission/SKILL.md +304 -0
- package/template/.agent/skill-library/surface/mobile/mobile-offline-sync/SKILL.md +443 -0
- package/template/.agent/skill-library/surface/mobile/mobile-responsive-patterns/SKILL.md +432 -0
- package/template/.agent/skill-library/surface/mobile/push-notifications/SKILL.md +495 -0
- package/template/.agent/skill-library/surface/web/accessibility-compliance/SKILL.md +827 -0
- package/template/.agent/skill-library/surface/web/ai-seo/SKILL.md +398 -0
- package/template/.agent/skill-library/surface/web/ai-seo/references/content-patterns.md +285 -0
- package/template/.agent/skill-library/surface/web/ai-seo/references/platform-ranking-factors.md +152 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/SKILL.md +309 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/references/event-library.md +260 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/references/ga4-implementation.md +300 -0
- package/template/.agent/skill-library/surface/web/analytics-tracking/references/gtm-implementation.md +390 -0
- package/template/.agent/skill-library/surface/web/authentication-ui-flows/SKILL.md +530 -0
- package/template/.agent/skill-library/surface/web/dark-mode-theming/SKILL.md +516 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/SKILL.md +105 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/charts.csv +26 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/colors.csv +97 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/landing.csv +31 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/styles.csv +59 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/typography.csv +58 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/data/ux-guidelines.csv +100 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/scripts/core.py +258 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/scripts/design_system.py +1067 -0
- package/template/.agent/skill-library/surface/web/design-reference-data/scripts/search.py +106 -0
- package/template/.agent/skill-library/surface/web/form-handling-validation/SKILL.md +675 -0
- package/template/.agent/skill-library/surface/web/frontend-design/SKILL.md +1393 -0
- package/template/.agent/skill-library/surface/web/frontend-design/templates/cppn-hero.tsx +299 -0
- package/template/.agent/skill-library/surface/web/frontend-design/templates/wave-hero.tsx +875 -0
- package/template/.agent/skill-library/surface/web/frontend-verification/SKILL.md +111 -0
- package/template/.agent/skill-library/surface/web/frontend-verification/scripts/ux_audit.py +739 -0
- package/template/.agent/skill-library/surface/web/i18n-localization/SKILL.md +154 -0
- package/template/.agent/skill-library/surface/web/offline-first-pwa/SKILL.md +657 -0
- package/template/.agent/skill-library/surface/web/page-cro/SKILL.md +182 -0
- package/template/.agent/skill-library/surface/web/page-cro/references/experiments.md +248 -0
- package/template/.agent/skill-library/surface/web/programmatic-seo/SKILL.md +238 -0
- package/template/.agent/skill-library/surface/web/programmatic-seo/references/playbooks.md +308 -0
- package/template/.agent/skill-library/surface/web/schema-markup/SKILL.md +179 -0
- package/template/.agent/skill-library/surface/web/schema-markup/references/schema-examples.md +398 -0
- package/template/.agent/skill-library/surface/web/seo-audit/SKILL.md +394 -0
- package/template/.agent/skill-library/surface/web/seo-audit/references/ai-writing-detection.md +200 -0
- package/template/.agent/skill-library/surface/web/web-performance-optimization/SKILL.md +646 -0
- package/template/.agent/skill-library/surface/web/web-scraping/SKILL.md +58 -0
- package/template/.agent/skills/accessibility/SKILL.md +522 -0
- package/template/.agent/skills/accessibility/references/WCAG.md +162 -0
- package/template/.agent/skills/adversarial-review/SKILL.md +90 -0
- package/template/.agent/skills/antigravity-workflows/SKILL.md +81 -0
- package/template/.agent/skills/antigravity-workflows/resources/implementation-playbook.md +36 -0
- package/template/.agent/skills/api-design-principles/SKILL.md +37 -0
- package/template/.agent/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/template/.agent/skills/api-design-principles/assets/rest-api-template.py +182 -0
- package/template/.agent/skills/api-design-principles/references/graphql-schema-design.md +583 -0
- package/template/.agent/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/template/.agent/skills/api-design-principles/resources/implementation-playbook.md +513 -0
- package/template/.agent/skills/api-versioning/SKILL.md +420 -0
- package/template/.agent/skills/architecture-mapping/SKILL.md +219 -0
- package/template/.agent/skills/bootstrap-agents/SKILL.md +259 -0
- package/template/.agent/skills/brainstorming/SKILL.md +236 -0
- package/template/.agent/skills/brand-guidelines/SKILL.md +44 -0
- package/template/.agent/skills/clean-code/SKILL.md +94 -0
- package/template/.agent/skills/code-review-pro/SKILL.md +152 -0
- package/template/.agent/skills/concise-planning/SKILL.md +68 -0
- package/template/.agent/skills/cross-layer-consistency/SKILL.md +117 -0
- package/template/.agent/skills/database-schema-design/SKILL.md +429 -0
- package/template/.agent/skills/deployment-procedures/SKILL.md +241 -0
- package/template/.agent/skills/design-anti-cliche/SKILL.md +159 -0
- package/template/.agent/skills/design-direction/SKILL.md +45 -0
- package/template/.agent/skills/error-handling-patterns/SKILL.md +721 -0
- package/template/.agent/skills/find-skills/SKILL.md +145 -0
- package/template/.agent/skills/git-advanced/SKILL.md +972 -0
- package/template/.agent/skills/git-workflow/SKILL.md +420 -0
- package/template/.agent/skills/idea-extraction/SKILL.md +271 -0
- package/template/.agent/skills/logging-best-practices/SKILL.md +851 -0
- package/template/.agent/skills/migration-management/SKILL.md +384 -0
- package/template/.agent/skills/minimalist-surgical-development/SKILL.md +69 -0
- package/template/.agent/skills/parallel-agents/SKILL.md +165 -0
- package/template/.agent/skills/parallel-debugging/SKILL.md +135 -0
- package/template/.agent/skills/parallel-feature-development/SKILL.md +166 -0
- package/template/.agent/skills/performance-budgeting/SKILL.md +144 -0
- package/template/.agent/skills/pipeline-rubrics/SKILL.md +51 -0
- package/template/.agent/skills/pipeline-rubrics/references/architecture-rubric.md +19 -0
- package/template/.agent/skills/pipeline-rubrics/references/be-rubric.md +21 -0
- package/template/.agent/skills/pipeline-rubrics/references/fe-rubric.md +20 -0
- package/template/.agent/skills/pipeline-rubrics/references/ia-rubric.md +19 -0
- package/template/.agent/skills/pipeline-rubrics/references/scoring.md +28 -0
- package/template/.agent/skills/pipeline-rubrics/references/vision-rubric.md +11 -0
- package/template/.agent/skills/prd-templates/SKILL.md +88 -0
- package/template/.agent/skills/prd-templates/references/architecture-design-template.md +88 -0
- package/template/.agent/skills/prd-templates/references/be-spec-template.md +101 -0
- package/template/.agent/skills/prd-templates/references/data-placement-template.md +74 -0
- package/template/.agent/skills/prd-templates/references/decomposition-templates.md +211 -0
- package/template/.agent/skills/prd-templates/references/design-system-decisions.md +198 -0
- package/template/.agent/skills/prd-templates/references/engineering-standards-template.md +124 -0
- package/template/.agent/skills/prd-templates/references/fe-classification-procedures.md +47 -0
- package/template/.agent/skills/prd-templates/references/fe-spec-template.md +84 -0
- package/template/.agent/skills/prd-templates/references/infrastructure-report-template.md +71 -0
- package/template/.agent/skills/prd-templates/references/operational-templates.md +116 -0
- package/template/.agent/skills/prd-templates/references/placeholder-guard-template.md +21 -0
- package/template/.agent/skills/prd-templates/references/surface-model.md +61 -0
- package/template/.agent/skills/prd-templates/references/vision-template.md +66 -0
- package/template/.agent/skills/prompt-engineer/README.md +659 -0
- package/template/.agent/skills/prompt-engineer/SKILL.md +249 -0
- package/template/.agent/skills/regex-patterns/SKILL.md +751 -0
- package/template/.agent/skills/resolve-ambiguity/SKILL.md +278 -0
- package/template/.agent/skills/rest-api-design/SKILL.md +478 -0
- package/template/.agent/skills/security-scanning-security-hardening/SKILL.md +231 -0
- package/template/.agent/skills/session-continuity/SKILL.md +730 -0
- package/template/.agent/skills/session-continuity/protocols/01-session-resumption.md +38 -0
- package/template/.agent/skills/session-continuity/protocols/02-progress-generation.md +85 -0
- package/template/.agent/skills/session-continuity/protocols/03-progress-update.md +70 -0
- package/template/.agent/skills/session-continuity/protocols/04-pattern-extraction.md +60 -0
- package/template/.agent/skills/session-continuity/protocols/05-session-close.md +37 -0
- package/template/.agent/skills/session-continuity/protocols/06-decision-analysis.md +84 -0
- package/template/.agent/skills/session-continuity/protocols/07-spec-pipeline-generation.md +48 -0
- package/template/.agent/skills/session-continuity/protocols/08-spec-pipeline-update.md +43 -0
- package/template/.agent/skills/session-continuity/protocols/09-parallel-claim.md +122 -0
- package/template/.agent/skills/session-continuity/protocols/10-placeholder-verification-gate.md +104 -0
- package/template/.agent/skills/session-continuity/protocols/ambiguity-gates.md +48 -0
- package/template/.agent/skills/skill-creator/LICENSE.txt +202 -0
- package/template/.agent/skills/skill-creator/README.md +270 -0
- package/template/.agent/skills/skill-creator/SKILL.md +590 -0
- package/template/.agent/skills/skill-creator/references/output-patterns.md +82 -0
- package/template/.agent/skills/skill-creator/references/workflows.md +28 -0
- package/template/.agent/skills/skill-creator/scripts/init_skill.py +303 -0
- package/template/.agent/skills/skill-creator/scripts/package_skill.py +110 -0
- package/template/.agent/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/template/.agent/skills/spec-writing/SKILL.md +110 -0
- package/template/.agent/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/template/.agent/skills/systematic-debugging/SKILL.md +297 -0
- package/template/.agent/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/template/.agent/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/template/.agent/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/template/.agent/skills/systematic-debugging/find-polluter.sh +63 -0
- package/template/.agent/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/template/.agent/skills/systematic-debugging/test-academic.md +14 -0
- package/template/.agent/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/template/.agent/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/template/.agent/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/template/.agent/skills/tdd-workflow/SKILL.md +409 -0
- package/template/.agent/skills/tech-stack-catalog/SKILL.md +49 -0
- package/template/.agent/skills/tech-stack-catalog/references/constraint-questions.md +21 -0
- package/template/.agent/skills/tech-stack-catalog/references/dev-tooling-decisions.md +37 -0
- package/template/.agent/skills/tech-stack-catalog/references/surface-decision-tables.md +69 -0
- package/template/.agent/skills/technical-writer/SKILL.md +242 -0
- package/template/.agent/skills/testing-strategist/SKILL.md +932 -0
- package/template/.agent/skills/verification-before-completion/SKILL.md +145 -0
- package/template/.agent/skills/workflow-automation/SKILL.md +73 -0
- package/template/.agent/workflows/audit-ambiguity-execute.md +165 -0
- package/template/.agent/workflows/audit-ambiguity-rubrics.md +83 -0
- package/template/.agent/workflows/audit-ambiguity.md +64 -0
- package/template/.agent/workflows/bootstrap-agents-fill.md +201 -0
- package/template/.agent/workflows/bootstrap-agents-provision.md +197 -0
- package/template/.agent/workflows/bootstrap-agents.md +66 -0
- package/template/.agent/workflows/create-prd-architecture.md +119 -0
- package/template/.agent/workflows/create-prd-compile.md +138 -0
- package/template/.agent/workflows/create-prd-design-system.md +135 -0
- package/template/.agent/workflows/create-prd-security.md +113 -0
- package/template/.agent/workflows/create-prd-stack.md +91 -0
- package/template/.agent/workflows/create-prd.md +168 -0
- package/template/.agent/workflows/decompose-architecture-structure.md +82 -0
- package/template/.agent/workflows/decompose-architecture-validate.md +119 -0
- package/template/.agent/workflows/decompose-architecture.md +111 -0
- package/template/.agent/workflows/evolve-contract.md +98 -0
- package/template/.agent/workflows/evolve-feature-cascade.md +140 -0
- package/template/.agent/workflows/evolve-feature-classify.md +116 -0
- package/template/.agent/workflows/evolve-feature.md +56 -0
- package/template/.agent/workflows/ideate-discover.md +144 -0
- package/template/.agent/workflows/ideate-extract.md +129 -0
- package/template/.agent/workflows/ideate-validate.md +117 -0
- package/template/.agent/workflows/ideate.md +113 -0
- package/template/.agent/workflows/implement-slice-setup.md +113 -0
- package/template/.agent/workflows/implement-slice-tdd.md +198 -0
- package/template/.agent/workflows/implement-slice.md +50 -0
- package/template/.agent/workflows/plan-phase.md +202 -0
- package/template/.agent/workflows/propagate-decision-apply.md +135 -0
- package/template/.agent/workflows/propagate-decision-scan.md +147 -0
- package/template/.agent/workflows/propagate-decision.md +56 -0
- package/template/.agent/workflows/remediate-pipeline-assess.md +138 -0
- package/template/.agent/workflows/remediate-pipeline-execute.md +135 -0
- package/template/.agent/workflows/remediate-pipeline.md +55 -0
- package/template/.agent/workflows/resolve-ambiguity.md +82 -0
- package/template/.agent/workflows/sync-kit.md +209 -0
- package/template/.agent/workflows/update-architecture-map.md +74 -0
- package/template/.agent/workflows/validate-phase.md +219 -0
- package/template/.agent/workflows/verify-infrastructure.md +207 -0
- package/template/.agent/workflows/write-architecture-spec-deepen.md +139 -0
- package/template/.agent/workflows/write-architecture-spec-design.md +202 -0
- package/template/.agent/workflows/write-architecture-spec.md +63 -0
- package/template/.agent/workflows/write-be-spec-classify.md +165 -0
- package/template/.agent/workflows/write-be-spec-write.md +98 -0
- package/template/.agent/workflows/write-be-spec.md +76 -0
- package/template/.agent/workflows/write-fe-spec-classify.md +170 -0
- package/template/.agent/workflows/write-fe-spec-write.md +94 -0
- package/template/.agent/workflows/write-fe-spec.md +71 -0
- package/template/AGENTS.md +176 -0
- package/template/GEMINI.md +177 -0
- package/template/docs/README.md +187 -0
- package/template/docs/audits/.gitkeep +0 -0
- package/template/docs/audits/README.md +10 -0
- package/template/docs/plans/.gitkeep +0 -0
- package/template/docs/plans/README.md +21 -0
- package/template/docs/plans/be/.gitkeep +0 -0
- package/template/docs/plans/be/README.md +11 -0
- package/template/docs/plans/fe/.gitkeep +0 -0
- package/template/docs/plans/fe/README.md +11 -0
- package/template/docs/plans/ia/.gitkeep +0 -0
- package/template/docs/plans/ia/README.md +17 -0
- package/template/docs/plans/ia/deep-dives/.gitkeep +0 -0
- package/template/docs/plans/ia/deep-dives/README.md +5 -0
- package/template/docs/plans/phases/.gitkeep +0 -0
- package/template/docs/plans/phases/README.md +11 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webhook-design
|
|
3
|
+
description: "Design reliable webhook systems with HMAC payload signing, retry policies, idempotency keys, dead letter queues, registration APIs, payload versioning, and local testing patterns. Use when building webhook delivery, webhook consumers, or event notification systems."
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Webhook Design & Reliability
|
|
8
|
+
|
|
9
|
+
Webhooks are HTTP callbacks that notify external systems when events occur. Unlike APIs where the consumer polls, webhooks push data to the consumer. This inversion creates unique reliability challenges.
|
|
10
|
+
|
|
11
|
+
## Core Principles
|
|
12
|
+
|
|
13
|
+
1. **At-least-once delivery** --- assume webhooks may be delivered more than once
|
|
14
|
+
2. **Payload signing** --- consumers must verify the webhook came from you
|
|
15
|
+
3. **Retry with backoff** --- transient failures must not lose events
|
|
16
|
+
4. **Idempotency** --- consumers must handle duplicate deliveries safely
|
|
17
|
+
5. **Versioned payloads** --- changing the shape without warning breaks consumers
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Payload Signing (HMAC-SHA256)
|
|
22
|
+
|
|
23
|
+
Every webhook request must include a signature so consumers can verify authenticity and integrity.
|
|
24
|
+
|
|
25
|
+
### Generating Signatures (Producer)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
29
|
+
|
|
30
|
+
function signPayload(payload: string, secret: string): string {
|
|
31
|
+
return createHmac('sha256', secret)
|
|
32
|
+
.update(payload, 'utf8')
|
|
33
|
+
.digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function deliverWebhook(
|
|
37
|
+
url: string,
|
|
38
|
+
event: WebhookEvent,
|
|
39
|
+
secret: string
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
const payload = JSON.stringify(event);
|
|
42
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
43
|
+
const signedContent = `${timestamp}.${payload}`;
|
|
44
|
+
const signature = signPayload(signedContent, secret);
|
|
45
|
+
|
|
46
|
+
return fetch(url, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'X-Webhook-ID': event.id,
|
|
51
|
+
'X-Webhook-Timestamp': String(timestamp),
|
|
52
|
+
'X-Webhook-Signature': `v1=${signature}`,
|
|
53
|
+
},
|
|
54
|
+
body: payload,
|
|
55
|
+
signal: AbortSignal.timeout(30_000), // 30 second timeout
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Verifying Signatures (Consumer)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
function verifyWebhookSignature(
|
|
64
|
+
payload: string,
|
|
65
|
+
timestamp: string,
|
|
66
|
+
signature: string,
|
|
67
|
+
secret: string
|
|
68
|
+
): boolean {
|
|
69
|
+
// Reject if timestamp is too old (prevent replay attacks)
|
|
70
|
+
const now = Math.floor(Date.now() / 1000);
|
|
71
|
+
const webhookTime = parseInt(timestamp, 10);
|
|
72
|
+
if (Math.abs(now - webhookTime) > 300) { // 5 minute tolerance
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const expectedSignature = `v1=${signPayload(`${timestamp}.${payload}`, secret)}`;
|
|
77
|
+
|
|
78
|
+
// Timing-safe comparison to prevent timing attacks
|
|
79
|
+
const expected = Buffer.from(expectedSignature, 'utf8');
|
|
80
|
+
const received = Buffer.from(signature, 'utf8');
|
|
81
|
+
|
|
82
|
+
if (expected.length !== received.length) return false;
|
|
83
|
+
return timingSafeEqual(expected, received);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Usage in webhook handler
|
|
87
|
+
export async function POST({ request }: APIContext) {
|
|
88
|
+
const payload = await request.text();
|
|
89
|
+
const timestamp = request.headers.get('X-Webhook-Timestamp') ?? '';
|
|
90
|
+
const signature = request.headers.get('X-Webhook-Signature') ?? '';
|
|
91
|
+
|
|
92
|
+
if (!verifyWebhookSignature(payload, timestamp, signature, WEBHOOK_SECRET)) {
|
|
93
|
+
return new Response('Invalid signature', { status: 401 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const event = JSON.parse(payload);
|
|
97
|
+
await processWebhookEvent(event);
|
|
98
|
+
|
|
99
|
+
return new Response('OK', { status: 200 });
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Retry Policies with Exponential Backoff
|
|
106
|
+
|
|
107
|
+
### Retry Schedule
|
|
108
|
+
|
|
109
|
+
| Attempt | Delay | Total Elapsed |
|
|
110
|
+
|---------|-------|---------------|
|
|
111
|
+
| 1 | Immediate | 0 |
|
|
112
|
+
| 2 | 30 seconds | 30s |
|
|
113
|
+
| 3 | 2 minutes | 2.5 min |
|
|
114
|
+
| 4 | 15 minutes | 17.5 min |
|
|
115
|
+
| 5 | 1 hour | 1 hr 17 min |
|
|
116
|
+
| 6 | 4 hours | 5 hr 17 min |
|
|
117
|
+
| 7 | 8 hours | 13 hr 17 min |
|
|
118
|
+
| 8 | 24 hours | 37 hr 17 min |
|
|
119
|
+
|
|
120
|
+
### Implementation
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
interface RetryPolicy {
|
|
124
|
+
maxAttempts: number;
|
|
125
|
+
baseDelay: number; // milliseconds
|
|
126
|
+
maxDelay: number; // milliseconds
|
|
127
|
+
backoffMultiplier: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const DEFAULT_RETRY_POLICY: RetryPolicy = {
|
|
131
|
+
maxAttempts: 8,
|
|
132
|
+
baseDelay: 30_000, // 30 seconds
|
|
133
|
+
maxDelay: 86_400_000, // 24 hours
|
|
134
|
+
backoffMultiplier: 4,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function calculateDelay(attempt: number, policy: RetryPolicy): number {
|
|
138
|
+
const delay = policy.baseDelay * Math.pow(policy.backoffMultiplier, attempt - 1);
|
|
139
|
+
// Add jitter (0-25% random variation) to prevent thundering herd
|
|
140
|
+
const jitter = delay * 0.25 * Math.random();
|
|
141
|
+
return Math.min(delay + jitter, policy.maxDelay);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function deliverWithRetry(
|
|
145
|
+
webhook: WebhookDelivery,
|
|
146
|
+
policy: RetryPolicy = DEFAULT_RETRY_POLICY
|
|
147
|
+
): Promise<DeliveryResult> {
|
|
148
|
+
for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await deliverWebhook(webhook.url, webhook.event, webhook.secret);
|
|
151
|
+
|
|
152
|
+
if (response.ok) {
|
|
153
|
+
return { status: 'delivered', attempt, statusCode: response.status };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4xx errors (except 429) are permanent failures --- do not retry
|
|
157
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
158
|
+
return { status: 'failed_permanent', attempt, statusCode: response.status };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 429 or 5xx: retry
|
|
162
|
+
if (attempt < policy.maxAttempts) {
|
|
163
|
+
const delay = response.status === 429
|
|
164
|
+
? parseInt(response.headers.get('Retry-After') ?? '60', 10) * 1000
|
|
165
|
+
: calculateDelay(attempt, policy);
|
|
166
|
+
await scheduleRetry(webhook, delay, attempt + 1);
|
|
167
|
+
return { status: 'retrying', attempt, nextRetryIn: delay };
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// Network error: retry
|
|
171
|
+
if (attempt < policy.maxAttempts) {
|
|
172
|
+
const delay = calculateDelay(attempt, policy);
|
|
173
|
+
await scheduleRetry(webhook, delay, attempt + 1);
|
|
174
|
+
return { status: 'retrying', attempt, nextRetryIn: delay };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// All retries exhausted
|
|
180
|
+
await moveToDeadLetterQueue(webhook);
|
|
181
|
+
return { status: 'dead_lettered', attempt: policy.maxAttempts };
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Idempotency Keys
|
|
188
|
+
|
|
189
|
+
Every webhook event must have a unique ID. Consumers use this to deduplicate.
|
|
190
|
+
|
|
191
|
+
### Producer Side
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface WebhookEvent {
|
|
195
|
+
id: string; // Unique event ID (idempotency key)
|
|
196
|
+
type: string; // Event type
|
|
197
|
+
createdAt: string; // ISO 8601 timestamp
|
|
198
|
+
data: unknown; // Event payload
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createWebhookEvent(type: string, data: unknown): WebhookEvent {
|
|
202
|
+
return {
|
|
203
|
+
id: crypto.randomUUID(),
|
|
204
|
+
type,
|
|
205
|
+
createdAt: new Date().toISOString(),
|
|
206
|
+
data,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Consumer Side
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// Deduplicate using processed event IDs
|
|
215
|
+
async function processWebhookEvent(event: WebhookEvent): Promise<void> {
|
|
216
|
+
// Check if already processed
|
|
217
|
+
const alreadyProcessed = await redis.sismember('processed-webhooks', event.id);
|
|
218
|
+
if (alreadyProcessed) {
|
|
219
|
+
console.log(`Webhook ${event.id} already processed, skipping`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Process the event
|
|
224
|
+
await handleEvent(event);
|
|
225
|
+
|
|
226
|
+
// Mark as processed (expire after 7 days to prevent unbounded growth)
|
|
227
|
+
await redis.sadd('processed-webhooks', event.id);
|
|
228
|
+
await redis.expire('processed-webhooks', 7 * 24 * 60 * 60);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Dead Letter Queues
|
|
235
|
+
|
|
236
|
+
Events that fail all retry attempts go to a dead letter queue for manual inspection and replay.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
interface DeadLetter {
|
|
240
|
+
id: string;
|
|
241
|
+
webhookId: string;
|
|
242
|
+
event: WebhookEvent;
|
|
243
|
+
endpoint: string;
|
|
244
|
+
lastAttempt: string;
|
|
245
|
+
attemptCount: number;
|
|
246
|
+
lastError: string;
|
|
247
|
+
lastStatusCode: number | null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function moveToDeadLetterQueue(delivery: WebhookDelivery): Promise<void> {
|
|
251
|
+
const deadLetter: DeadLetter = {
|
|
252
|
+
id: crypto.randomUUID(),
|
|
253
|
+
webhookId: delivery.webhookId,
|
|
254
|
+
event: delivery.event,
|
|
255
|
+
endpoint: delivery.url,
|
|
256
|
+
lastAttempt: new Date().toISOString(),
|
|
257
|
+
attemptCount: delivery.attemptCount,
|
|
258
|
+
lastError: delivery.lastError ?? 'Unknown error',
|
|
259
|
+
lastStatusCode: delivery.lastStatusCode ?? null,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
await db.create('dead_letter_queue', deadLetter);
|
|
263
|
+
|
|
264
|
+
// Notify operators
|
|
265
|
+
await alertOps({
|
|
266
|
+
channel: 'webhook-failures',
|
|
267
|
+
message: `Webhook to ${delivery.url} failed after ${delivery.attemptCount} attempts. Event: ${delivery.event.type}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Admin: Replay Dead Letters
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// API endpoint to replay a dead letter
|
|
276
|
+
export async function POST({ params }: APIContext) {
|
|
277
|
+
const deadLetter = await db.get<DeadLetter>('dead_letter_queue', params.id);
|
|
278
|
+
if (!deadLetter) return new Response(null, { status: 404 });
|
|
279
|
+
|
|
280
|
+
// Re-queue for delivery
|
|
281
|
+
await enqueueWebhookDelivery({
|
|
282
|
+
webhookId: deadLetter.webhookId,
|
|
283
|
+
event: deadLetter.event,
|
|
284
|
+
url: deadLetter.endpoint,
|
|
285
|
+
attemptCount: 0, // Reset attempts
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Remove from DLQ
|
|
289
|
+
await db.delete('dead_letter_queue', params.id);
|
|
290
|
+
|
|
291
|
+
return new Response(JSON.stringify({ status: 'replayed' }), { status: 200 });
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Webhook Registration/Management API
|
|
298
|
+
|
|
299
|
+
### Endpoints
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
paths:
|
|
303
|
+
/webhooks:
|
|
304
|
+
get:
|
|
305
|
+
summary: List registered webhooks
|
|
306
|
+
responses:
|
|
307
|
+
'200':
|
|
308
|
+
content:
|
|
309
|
+
application/json:
|
|
310
|
+
schema:
|
|
311
|
+
type: array
|
|
312
|
+
items:
|
|
313
|
+
$ref: '#/components/schemas/Webhook'
|
|
314
|
+
|
|
315
|
+
post:
|
|
316
|
+
summary: Register a new webhook
|
|
317
|
+
requestBody:
|
|
318
|
+
content:
|
|
319
|
+
application/json:
|
|
320
|
+
schema:
|
|
321
|
+
$ref: '#/components/schemas/CreateWebhookRequest'
|
|
322
|
+
responses:
|
|
323
|
+
'201':
|
|
324
|
+
description: Webhook created (secret returned ONCE)
|
|
325
|
+
content:
|
|
326
|
+
application/json:
|
|
327
|
+
schema:
|
|
328
|
+
$ref: '#/components/schemas/WebhookWithSecret'
|
|
329
|
+
|
|
330
|
+
/webhooks/{id}:
|
|
331
|
+
patch:
|
|
332
|
+
summary: Update webhook (URL, events, status)
|
|
333
|
+
delete:
|
|
334
|
+
summary: Delete webhook
|
|
335
|
+
|
|
336
|
+
/webhooks/{id}/rotate-secret:
|
|
337
|
+
post:
|
|
338
|
+
summary: Rotate webhook signing secret
|
|
339
|
+
|
|
340
|
+
/webhooks/{id}/test:
|
|
341
|
+
post:
|
|
342
|
+
summary: Send a test event to the webhook URL
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Schemas
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { z } from 'zod';
|
|
349
|
+
|
|
350
|
+
export const CreateWebhookSchema = z.object({
|
|
351
|
+
url: z.string().url('Must be a valid HTTPS URL').startsWith('https://', 'Webhooks require HTTPS'),
|
|
352
|
+
events: z.array(z.string()).min(1, 'Subscribe to at least one event type'),
|
|
353
|
+
description: z.string().max(200).optional(),
|
|
354
|
+
active: z.boolean().default(true),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
export const WebhookSchema = z.object({
|
|
358
|
+
id: z.string().uuid(),
|
|
359
|
+
url: z.string().url(),
|
|
360
|
+
events: z.array(z.string()),
|
|
361
|
+
description: z.string().nullable(),
|
|
362
|
+
active: z.boolean(),
|
|
363
|
+
createdAt: z.string().datetime(),
|
|
364
|
+
// Secret is NEVER returned after creation
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
export const WebhookWithSecretSchema = WebhookSchema.extend({
|
|
368
|
+
secret: z.string().describe('Signing secret. Shown only once at creation time.'),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
export type CreateWebhook = z.infer<typeof CreateWebhookSchema>;
|
|
372
|
+
export type Webhook = z.infer<typeof WebhookSchema>;
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Secret Rotation
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
export async function POST({ params }: APIContext) {
|
|
379
|
+
const webhook = await db.get<Webhook>('webhooks', params.id);
|
|
380
|
+
if (!webhook) return new Response(null, { status: 404 });
|
|
381
|
+
|
|
382
|
+
const newSecret = crypto.randomBytes(32).toString('hex');
|
|
383
|
+
|
|
384
|
+
// Store both old and new secret for a transition period
|
|
385
|
+
await db.update('webhooks', params.id, {
|
|
386
|
+
secret: newSecret,
|
|
387
|
+
previousSecret: webhook.secret,
|
|
388
|
+
previousSecretExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return new Response(JSON.stringify({
|
|
392
|
+
secret: newSecret,
|
|
393
|
+
note: 'The previous secret will remain valid for 24 hours to allow a smooth transition.',
|
|
394
|
+
}), { status: 200 });
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Payload Versioning
|
|
401
|
+
|
|
402
|
+
Include a version in every webhook payload. When the payload shape changes, bump the version and let consumers migrate.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
interface WebhookEvent<T = unknown> {
|
|
406
|
+
id: string;
|
|
407
|
+
type: string;
|
|
408
|
+
version: string; // e.g., '2024-01-15' (date-based versioning)
|
|
409
|
+
createdAt: string;
|
|
410
|
+
data: T;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Example: model.updated event, version 2024-01-15
|
|
414
|
+
const event: WebhookEvent = {
|
|
415
|
+
id: 'evt_a1b2c3',
|
|
416
|
+
type: 'model.updated',
|
|
417
|
+
version: '2024-01-15',
|
|
418
|
+
createdAt: '2026-02-15T10:30:00Z',
|
|
419
|
+
data: {
|
|
420
|
+
modelId: 'gpt-4o',
|
|
421
|
+
changes: {
|
|
422
|
+
status: { from: 'preview', to: 'active' },
|
|
423
|
+
pricing: { from: { input: 5.0 }, to: { input: 2.5 } },
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Per-Endpoint Version Selection
|
|
430
|
+
|
|
431
|
+
Allow consumers to specify which payload version they expect:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const CreateWebhookSchemaV2 = CreateWebhookSchema.extend({
|
|
435
|
+
apiVersion: z.string().optional().describe('Payload version (e.g., 2024-01-15). Defaults to latest.'),
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Timeout Handling
|
|
442
|
+
|
|
443
|
+
### Producer Timeouts
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
async function deliverWebhook(url: string, event: WebhookEvent, secret: string): Promise<Response> {
|
|
447
|
+
const controller = new AbortController();
|
|
448
|
+
const timeout = setTimeout(() => controller.abort(), 30_000); // 30 second timeout
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const response = await fetch(url, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
headers: { /* ... */ },
|
|
454
|
+
body: JSON.stringify(event),
|
|
455
|
+
signal: controller.signal,
|
|
456
|
+
});
|
|
457
|
+
return response;
|
|
458
|
+
} finally {
|
|
459
|
+
clearTimeout(timeout);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Consumer Best Practice: Acknowledge Immediately
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// Consumer: respond 200 immediately, process asynchronously
|
|
468
|
+
export async function POST({ request }: APIContext) {
|
|
469
|
+
const event = await request.json();
|
|
470
|
+
|
|
471
|
+
// Verify signature (fast)
|
|
472
|
+
if (!verifySignature(request)) {
|
|
473
|
+
return new Response('Invalid signature', { status: 401 });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Queue for async processing (fast)
|
|
477
|
+
await queue.enqueue('process-webhook', event);
|
|
478
|
+
|
|
479
|
+
// Respond immediately --- do NOT do heavy processing here
|
|
480
|
+
return new Response('Accepted', { status: 202 });
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Event Types and Filtering
|
|
487
|
+
|
|
488
|
+
### Event Type Taxonomy
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
resource.action
|
|
492
|
+
|
|
493
|
+
model.created
|
|
494
|
+
model.updated
|
|
495
|
+
model.deleted
|
|
496
|
+
model.status_changed
|
|
497
|
+
|
|
498
|
+
user.created
|
|
499
|
+
user.updated
|
|
500
|
+
user.deleted
|
|
501
|
+
|
|
502
|
+
completion.started
|
|
503
|
+
completion.completed
|
|
504
|
+
completion.failed
|
|
505
|
+
|
|
506
|
+
billing.invoice_created
|
|
507
|
+
billing.payment_succeeded
|
|
508
|
+
billing.payment_failed
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Wildcard Subscriptions
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// Subscribe to all model events
|
|
515
|
+
const webhook = await createWebhook({
|
|
516
|
+
url: 'https://consumer.example.com/webhooks',
|
|
517
|
+
events: ['model.*'], // Wildcard: all model events
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Filter during delivery
|
|
521
|
+
function shouldDeliver(webhook: Webhook, eventType: string): boolean {
|
|
522
|
+
return webhook.events.some((pattern) => {
|
|
523
|
+
if (pattern === '*') return true;
|
|
524
|
+
if (pattern.endsWith('.*')) {
|
|
525
|
+
const prefix = pattern.slice(0, -2);
|
|
526
|
+
return eventType.startsWith(`${prefix}.`);
|
|
527
|
+
}
|
|
528
|
+
return pattern === eventType;
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Rate Limiting Webhook Delivery
|
|
536
|
+
|
|
537
|
+
Protect consumers from being overwhelmed during bulk operations.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
interface DeliveryRateLimit {
|
|
541
|
+
maxPerSecond: number;
|
|
542
|
+
maxConcurrent: number;
|
|
543
|
+
burstSize: number;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const DEFAULT_DELIVERY_RATE: DeliveryRateLimit = {
|
|
547
|
+
maxPerSecond: 50,
|
|
548
|
+
maxConcurrent: 10,
|
|
549
|
+
burstSize: 100,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Per-endpoint rate limiting
|
|
553
|
+
class WebhookDeliveryQueue {
|
|
554
|
+
private queues = new Map<string, PQueue>();
|
|
555
|
+
|
|
556
|
+
getQueue(endpointUrl: string): PQueue {
|
|
557
|
+
if (!this.queues.has(endpointUrl)) {
|
|
558
|
+
this.queues.set(endpointUrl, new PQueue({
|
|
559
|
+
concurrency: DEFAULT_DELIVERY_RATE.maxConcurrent,
|
|
560
|
+
intervalCap: DEFAULT_DELIVERY_RATE.maxPerSecond,
|
|
561
|
+
interval: 1000,
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
return this.queues.get(endpointUrl)!;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async enqueue(delivery: WebhookDelivery): Promise<void> {
|
|
568
|
+
const queue = this.getQueue(delivery.url);
|
|
569
|
+
await queue.add(() => deliverWithRetry(delivery));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Replay and Debugging Tools
|
|
577
|
+
|
|
578
|
+
### Event Log API
|
|
579
|
+
|
|
580
|
+
```yaml
|
|
581
|
+
paths:
|
|
582
|
+
/webhooks/{id}/deliveries:
|
|
583
|
+
get:
|
|
584
|
+
summary: List delivery attempts for a webhook
|
|
585
|
+
parameters:
|
|
586
|
+
- name: status
|
|
587
|
+
in: query
|
|
588
|
+
schema:
|
|
589
|
+
enum: [delivered, failed, retrying, dead_lettered]
|
|
590
|
+
- name: eventType
|
|
591
|
+
in: query
|
|
592
|
+
schema:
|
|
593
|
+
type: string
|
|
594
|
+
responses:
|
|
595
|
+
'200':
|
|
596
|
+
content:
|
|
597
|
+
application/json:
|
|
598
|
+
schema:
|
|
599
|
+
type: array
|
|
600
|
+
items:
|
|
601
|
+
type: object
|
|
602
|
+
properties:
|
|
603
|
+
id: { type: string }
|
|
604
|
+
eventId: { type: string }
|
|
605
|
+
eventType: { type: string }
|
|
606
|
+
status: { type: string }
|
|
607
|
+
statusCode: { type: integer }
|
|
608
|
+
attemptCount: { type: integer }
|
|
609
|
+
createdAt: { type: string, format: date-time }
|
|
610
|
+
deliveredAt: { type: string, format: date-time, nullable: true }
|
|
611
|
+
|
|
612
|
+
/webhooks/{id}/deliveries/{deliveryId}/replay:
|
|
613
|
+
post:
|
|
614
|
+
summary: Replay a specific delivery
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Test Event
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
export async function POST({ params }: APIContext) {
|
|
621
|
+
const webhook = await db.get<Webhook>('webhooks', params.id);
|
|
622
|
+
if (!webhook) return new Response(null, { status: 404 });
|
|
623
|
+
|
|
624
|
+
const testEvent: WebhookEvent = {
|
|
625
|
+
id: `test_${crypto.randomUUID()}`,
|
|
626
|
+
type: 'webhook.test',
|
|
627
|
+
version: '2024-01-15',
|
|
628
|
+
createdAt: new Date().toISOString(),
|
|
629
|
+
data: {
|
|
630
|
+
message: 'This is a test webhook delivery.',
|
|
631
|
+
webhookId: webhook.id,
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const result = await deliverWebhook(webhook.url, testEvent, webhook.secret);
|
|
636
|
+
|
|
637
|
+
return new Response(JSON.stringify({
|
|
638
|
+
delivered: result.ok,
|
|
639
|
+
statusCode: result.status,
|
|
640
|
+
responseBody: await result.text().catch(() => null),
|
|
641
|
+
}), { status: 200 });
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Testing Webhooks Locally
|
|
648
|
+
|
|
649
|
+
### ngrok
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
# Expose local port 3000 to the internet
|
|
653
|
+
ngrok http 3000
|
|
654
|
+
|
|
655
|
+
# Use the generated URL as your webhook endpoint:
|
|
656
|
+
# https://abc123.ngrok.io/api/webhooks/handler
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### smee.io
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
# Create a channel at https://smee.io
|
|
663
|
+
npx smee -u https://smee.io/abc123 -t http://localhost:3000/api/webhooks/handler
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Local Testing Without External Tools
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// In tests: mock the webhook consumer
|
|
670
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
671
|
+
|
|
672
|
+
describe('Webhook Delivery', () => {
|
|
673
|
+
it('delivers event with correct signature', async () => {
|
|
674
|
+
const receivedRequests: Request[] = [];
|
|
675
|
+
|
|
676
|
+
// Mock consumer server
|
|
677
|
+
const server = Bun.serve({
|
|
678
|
+
port: 9999,
|
|
679
|
+
fetch(request) {
|
|
680
|
+
receivedRequests.push(request);
|
|
681
|
+
return new Response('OK', { status: 200 });
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const event = createWebhookEvent('model.created', { modelId: 'test-1' });
|
|
686
|
+
await deliverWebhook('http://localhost:9999/webhook', event, 'test-secret');
|
|
687
|
+
|
|
688
|
+
expect(receivedRequests).toHaveLength(1);
|
|
689
|
+
const req = receivedRequests[0];
|
|
690
|
+
expect(req.headers.get('X-Webhook-Signature')).toBeTruthy();
|
|
691
|
+
expect(req.headers.get('X-Webhook-Timestamp')).toBeTruthy();
|
|
692
|
+
|
|
693
|
+
// Verify signature is correct
|
|
694
|
+
const body = await req.text();
|
|
695
|
+
const timestamp = req.headers.get('X-Webhook-Timestamp')!;
|
|
696
|
+
const signature = req.headers.get('X-Webhook-Signature')!;
|
|
697
|
+
expect(verifyWebhookSignature(body, timestamp, signature, 'test-secret')).toBe(true);
|
|
698
|
+
|
|
699
|
+
server.stop();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('retries on 500 response', async () => {
|
|
703
|
+
let attemptCount = 0;
|
|
704
|
+
|
|
705
|
+
const server = Bun.serve({
|
|
706
|
+
port: 9999,
|
|
707
|
+
fetch() {
|
|
708
|
+
attemptCount++;
|
|
709
|
+
if (attemptCount < 3) return new Response('Error', { status: 500 });
|
|
710
|
+
return new Response('OK', { status: 200 });
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const event = createWebhookEvent('model.created', { modelId: 'test-1' });
|
|
715
|
+
const result = await deliverWithRetry({
|
|
716
|
+
url: 'http://localhost:9999/webhook',
|
|
717
|
+
event,
|
|
718
|
+
secret: 'test-secret',
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
expect(result.status).toBe('delivered');
|
|
722
|
+
expect(result.attempt).toBe(3);
|
|
723
|
+
|
|
724
|
+
server.stop();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Anti-Patterns
|
|
732
|
+
|
|
733
|
+
| Anti-Pattern | Correct Approach |
|
|
734
|
+
|-------------|------------------|
|
|
735
|
+
| No payload signing | HMAC-SHA256 with per-webhook secret |
|
|
736
|
+
| No retries | Exponential backoff with jitter, up to 8 attempts |
|
|
737
|
+
| No idempotency key | Include unique event ID, consumers deduplicate |
|
|
738
|
+
| Secret in payload | Secret used only for signing, never transmitted |
|
|
739
|
+
| HTTP (not HTTPS) | Require HTTPS for webhook URLs |
|
|
740
|
+
| Synchronous processing in consumer | Respond 200/202 immediately, process async |
|
|
741
|
+
| No dead letter queue | Failed events must not disappear |
|
|
742
|
+
| Returning secret after creation | Show secret only once, allow rotation |
|
|
743
|
+
| No event type filtering | Let consumers subscribe to specific event types |
|
|
744
|
+
| Unbounded payload size | Set a maximum payload size (e.g., 256KB) |
|
|
745
|
+
|
|
746
|
+
## References
|
|
747
|
+
|
|
748
|
+
- [Standard Webhooks Specification](https://www.standardwebhooks.com/)
|
|
749
|
+
- [Stripe Webhook Best Practices](https://stripe.com/docs/webhooks/best-practices)
|
|
750
|
+
- [GitHub Webhook Documentation](https://docs.github.com/en/webhooks)
|
|
751
|
+
- [Svix Webhook Service](https://www.svix.com/)
|
|
752
|
+
- [ngrok](https://ngrok.com/)
|