feat(api): wire WEBHOOK_SIGNING_SECRET into outbound webhook signing - #174
Open
kathy-ai-art wants to merge 2 commits into
Open
feat(api): wire WEBHOOK_SIGNING_SECRET into outbound webhook signing#174kathy-ai-art wants to merge 2 commits into
kathy-ai-art wants to merge 2 commits into
Conversation
All outbound webhook senders (Webhook Tester, contract-event replay, monitor alerts) now share a timestamped HMAC-SHA256 wire format: X-SaviTools-Signature: sha256=<hex> plus X-SaviTools-Timestamp, where the hex covers `<timestamp>.<body>` with the exact body bytes sent. WEBHOOK_SIGNING_SECRET is the global fallback secret when no per-request secret is given, closing the documented-but-unused gap; GET /webhooks/signing reports whether signing is enabled. Adds a shared sign/verify utility with full test coverage and verification docs. Closes Savitura#148
|
@kathy-ai-art Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #148
Summary
— wires the documented-but-unused
WEBHOOK_SIGNING_SECRETenv var into outbound webhook signing (Option A) instead of removing it, and adds the verification utility, replay protection, an introspection endpoint, and the documentation requested by the issue.What changed
apps/api/src/modules/webhook/signature.ts(signBody/verifySignature): every outbound webhook now uses one timestamped HMAC-SHA256 wire format —X-SaviTools-Signature: sha256=<hex>plusX-SaviTools-Timestamp: <unix seconds>, where the hex is HMAC-SHA256 over the UTF-8 bytes of<timestamp>.<body>with the exact body bytes sent.WEBHOOK_SIGNING_SECRETis now actually read:WebhookService.sendWebhookandEventsService.replayEventsresolve the signing secret as per-requestsecret→WEBHOOK_SIGNING_SECRET→ unsigned. Monitor alert webhooks keep signing with the per-webhook DB secret (source unchanged).verifySignaturerejects timestamps older thanDEFAULT_MAX_AGE_SECONDS(300 s) or more thanDEFAULT_MAX_SKEW_SECONDS(60 s) in the future.GET /webhooks/signingreturns{ enabled, algorithm, signatureHeader, timestampHeader, replayWindowSeconds }.docs/api-reference.md(new endpoint + verification instructions), ASSUMPTIONS.md items 3–4 rewritten,.env.examplecomment updated.<timestamp>.<payload>client-side and includesX-SaviTools-Timestamp, so the preview matches what the API actually sends.Key design decisions
X-Savitura-Signature, but the repo (ASSUMPTIONS.md item 3 and all three existing senders) already standardizes onX-SaviTools-Signature: sha256=<hex>. I kept the repo-standard header name — shipping a third name for the same signature would recreate the exact confusion this issue is about. The algorithm (HMAC-SHA256), body-bytes + canonical-timestamp coverage, and replay prevention are exactly as specified.X-SaviTools-Timestamp) rather than embedded in the signature value — keeps the existingsha256=<hex>shape recognizable while letting receivers enforce freshness.SendWebhookDto.secret/ReplayEventsDto.secretbehavior); the env var is purely a fallback.Acceptance criteria (Option A)
X-SaviTools-Signature: sha256=<hex>attached byWebhookService,EventsService.deliverOne, andNotificationWorkerServicewhenever a secret is in play (see decision 2 for the header name). Covering tests:webhook.service.spec.ts("includes a timestamped HMAC signature…"),events.service.spec.ts("signs each POST with a timestamped HMAC…"),notification-worker.service.spec.ts("sends the full event payload with a valid webhook HMAC").verifySignatureinsignature.ts;signature.spec.tscovers success, every named failure reason, window boundaries, and the constant-time path.<timestamp>.<body>(exact wire bytes); verifier enforces a 300 s replay window and 60 s future-skew guard, with boundary tests at exactly max-age and skew.docs/api-reference.md, ASSUMPTIONS.md.GET /webhooks/signing(public, no secrets exposed), tested viagetSigningStatus.Test output + coverage
API suite (
npx jest --coverage): 434 passed / 5 failed. The 5 failures (federation.service.spec,orderbook.service.spec×2,composer.service.spectype errors) are pre-existing onmain— verified by stashing this change and re-running: they fail identically without it. All new/updated suites pass (webhook, events, notification-worker group: 90/90).Coverage for changed files (no
coverageThresholdis configured inapps/api/package.json, so no threshold to meet):signature.ts(new)webhook.service.tsevents.service.tsnotification-worker.service.ts(
notification-worker.service.tsis a whole-file figure; the uncovered lines are pre-existing email/worker plumbing, not the signing path.)Typecheck:
tsc --noEmitis clean inapps/web;apps/apiis clean except the same 3 pre-existingcomposer.service.spec.tserrors that exist onmain. Lint:next lintis clean inapps/web;apps/apihas no ESLint config in the repo (pre-existing — theeslintscript cannot run in that workspace as committed).Follow-ups (honest)
notification-worker.service.tsoverall coverage is low because the Resend email path and BullMQ worker lifecycle are untested — pre-existing, not introduced here.composer.service.spec.tstype error) would makenpm run lintand APItscfully green.ReplayEventsDto.secret's doc string could mention the env-var fallback.Security note
WEBHOOK_SIGNING_SECRETmust stay out of client bundles. The Webhook Tester's cURL preview only signs in-browser when the user supplies their own secret; the env-var fallback is applied server-side.verifySignatureusescrypto.timingSafeEqual).WEBHOOK_SIGNING_SECRET.