Skip to content

Dev - #269

Merged
pikonha merged 19 commits into
mainfrom
dev
Jul 17, 2026
Merged

Dev#269
pikonha merged 19 commits into
mainfrom
dev

Conversation

@pikonha

@pikonha pikonha commented Jul 17, 2026

Copy link
Copy Markdown
Member

Note

High Risk
Touches secret generation, encryption-at-rest, HMAC signing, and subscriber credential flow—security-sensitive changes plus a DB migration that must run before deploy.

Overview
Adds HTTP webhook subscriptions with cryptographic verification: subscribers register an HTTPS URL, receive a one-time signing secret on first registration, and get notification POSTs signed with HMAC-SHA256 (X-Webhook-Timestamp, X-Webhook-Signature-V2 over {timestamp}.{rawBody}). Deliveries are skipped when no secret is available.

The subscription-server persists an encrypted users.secret (new migration), generates secrets only for the webhook channel, exposes optional secret on subscribe responses, and decrypts webhook secrets into subscriber tokens when fan-out loads users. User insert conflicts no longer overwrite existing secret/token columns.

The consumers webhook API returns the secret on first POST /webhooks, documents verification in OpenAPI/Swagger at /docs, and README covers receiver verification. Railway PR deploys for several apps are set to NOOP; Slack Bolt adds a /health route for checks.

Reviewed by Cursor Bugbot for commit 1944814. Configure here.

pikonha and others added 19 commits May 27, 2026 16:40
…check

Railway gates deploys by probing $PORT/health, and the Slack Bolt receiver is
the server bound to $PORT (3004). The /health endpoint only existed on the
separate webhook server (WEBHOOK_API_PORT, 3003), which Railway never probes,
so the healthcheck 404'd and dev deploys failed (replicas never became healthy).

Add a /health custom route to the Bolt HTTPReceiver so the probe on $PORT
returns 200 and deploys can complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
fix(consumers): serve /health on the Bolt receiver for Railway healthcheck
…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]>
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 self-assigned this Jul 17, 2026
@vercel

vercel Bot commented Jul 17, 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:51pm
notification-system-dashboard Ready Ready Preview, Comment Jul 17, 2026 12:51pm

Request Review

@LeonardoVieira1630
LeonardoVieira1630 self-requested a review July 17, 2026 13:10
@pikonha
pikonha merged commit 3844073 into main Jul 17, 2026
14 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