Skip to content

Webhook secret - #268

Merged
pikonha merged 12 commits into
devfrom
feat/webhook-secret
Jul 17, 2026
Merged

Webhook secret#268
pikonha merged 12 commits into
devfrom
feat/webhook-secret

Conversation

@pikonha

@pikonha pikonha commented Jul 15, 2026

Copy link
Copy Markdown
Member

Note

Medium Risk
Touches credential storage, encryption, and outbound signing semantics; incorrect race or merge behavior could leak or rotate secrets wrongly, but changes are scoped to webhook channel with tests.

Overview
Adds webhook signing secrets end-to-end: new nullable users.secret (encrypted at rest), generated only when a webhook channel user is first created, returned once on subscription API responses, with race-safe onConflict merges so concurrent registrations do not overwrite an existing secret.

The consumer webhook path now returns that secret on first POST /webhooks, signs outbound notifications as HMAC-SHA256(secret, "{timestamp}.{rawBody}") via X-Webhook-Timestamp and X-Webhook-Signature-V2, and skips POSTs when no secret is available. Subscriber lookup decrypts the stored secret into the existing delivery credential field used for signing.

OpenAPI/Swagger is exposed at /docs on the webhook Fastify server, with richer route schemas and README verification guidance. Slack’s HTTP receiver gains a GET /health route. Tests cover registration, signing, docs, and subscription secret behavior.

Reviewed by Cursor Bugbot for commit 8d42737. Configure here.

pikonha and others added 7 commits July 15, 2026 14:48
…creation

Adds a nullable, encrypted `secret` column to `users`, generated server-side
via crypto.randomBytes and encrypted with the existing CryptoUtil/
TOKEN_ENCRYPTION_KEY (same as workspace bot tokens) whenever a brand-new
channel: 'webhook' user is created through the subscription flow. The
plaintext secret is returned exactly once, only in the POST /subscriptions/:dao
response for that first-creation call, and is never accepted as caller input
or overwritten on existing rows.
…eport webhook secret

onConflict(...).merge() on users.create() merged ALL insert columns on conflict,
and Postgres ON CONFLICT DO UPDATE never raises a unique-violation error - so two
concurrent first-time inserts for the same new webhook user could silently
clobber each other's secret, and both callers would return their own (possibly
stale) plaintext secret.

Restrict the merge to the conflict key columns only (channel, channel_user_id),
so a racing insert can never touch secret/token. In handleSubscription, compare
the row Postgres actually persisted against what this call tried to insert;
only return the locally generated plaintext secret when they match, otherwise
this call lost the race and must behave like the "already existed" path.
POST /webhooks now returns the secret from subscription-server exactly
once, on first registration, threaded through SubscriptionAPIService
and WebhookService.registerWebhook.
Extend findByIdsWithWorkspaceTokens to decrypt the webhook `secret`
column onto the same user.token field the Slack/Telegram bot-token
lane uses, so the dispatcher forwards it as payload.bot_token with no
dispatcher changes. WebhookService.sendNotification now HMAC-signs
each POST body with that token (X-Webhook-Timestamp/X-Webhook-Signature
headers) and skips delivery (logged) when no token/secret is present,
so no unsigned webhook delivery can go out.
Wire up @fastify/swagger + @fastify/swagger-ui on WebhookServer,
mirroring subscription-server's setup, and complete the Zod route
schemas (tags, descriptions, response shapes) so GET /docs/json and
GET /docs document the existing HMAC secret issuance and delivery
verification recipe.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Documents webhook registration/unregistration and the HMAC-SHA256 delivery
verification steps in plain prose, mirroring the OpenAPI descriptions added
in Task 4, so subscribers don't need to load Swagger UI.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…secret decrypt failure

Final branch review flagged the secret-decrypt error log as a copy-pasted
"failed to decrypt workspace token" message, misleading in ops logs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@pikonha pikonha self-assigned this Jul 15, 2026
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flow-editor Ready Ready Preview, Comment Jul 17, 2026 12:50pm
notification-system-dashboard Ready Ready Preview, Comment Jul 17, 2026 12:50pm

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Existing webhooks never get secrets
    • handleSubscription now backfills a signing secret via updateSecret when an existing webhook user has a null secret, so re-registration returns the one-time plaintext secret and delivery can proceed.

Create PR

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 8d42737. Configure here.

Comment thread apps/subscription-server/src/services/subscription.service.ts
Existing webhook users created before the secret migration had null secrets
and could not receive notifications after signed delivery was enforced.
When handleSubscription finds an existing webhook user without a secret,
generate and persist one via updateSecret and return the plaintext once.
@pikonha
pikonha merged commit 1944814 into dev Jul 17, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants