Skip to content

feat: complete outbound event delivery, billing webhooks, and Stripe setup#1

Merged
khaliqgant merged 8 commits into
mainfrom
claude/audit-codebase-stubs-qvib9
Feb 8, 2026
Merged

feat: complete outbound event delivery, billing webhooks, and Stripe setup#1
khaliqgant merged 8 commits into
mainfrom
claude/audit-codebase-stubs-qvib9

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Feb 8, 2026

Copy link
Copy Markdown
Member
  • Implement outbound event subscription delivery engine with HMAC-SHA256
    signing, channel/mentions filtering, and retry with exponential backoff
  • Wire publishEvent() and deliverEvent() into all mutation route handlers:
    message.created, thread.reply, reaction.added/removed, channel.created/
    archived, dm.received, group_dm.received, file.uploaded, message.read
  • Complete billing webhook stubs: handleInvoicePaid resets usage counters,
    handlePaymentFailed downgrades workspace to free plan
  • Add Stripe product/price setup script (scripts/stripe-setup.ts) for
    creating Free/Pro/Enterprise pricing tiers idempotently
  • Add 29 new tests across 3 test files (eventDelivery, eventPublishing,
    webhooks) bringing total from 378 to 407 passing tests

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o


Open with Devin

…setup

- Implement outbound event subscription delivery engine with HMAC-SHA256
  signing, channel/mentions filtering, and retry with exponential backoff
- Wire publishEvent() and deliverEvent() into all mutation route handlers:
  message.created, thread.reply, reaction.added/removed, channel.created/
  archived, dm.received, group_dm.received, file.uploaded, message.read
- Complete billing webhook stubs: handleInvoicePaid resets usage counters,
  handlePaymentFailed downgrades workspace to free plan
- Add Stripe product/price setup script (scripts/stripe-setup.ts) for
  creating Free/Pro/Enterprise pricing tiers idempotently
- Add 29 new tests across 3 test files (eventDelivery, eventPublishing,
  webhooks) bringing total from 378 to 407 passing tests

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o
Adjust pricing based on competitive analysis (Pusher, Ably, PubNub):
- Pro: $49/mo → $99/mo (1M messages, 100 agents, 50GB files, 1200 rpm)
- Enterprise: $249/mo → $799/mo (unlimited msgs/agents, 500GB, 6000 rpm)
- Free tier unchanged ($0, 10K messages, 5 agents)

Update all locations where plan limits are defined:
- packages/server/src/engine/billing.ts
- packages/server/src/middleware/planLimits.ts
- scripts/stripe-setup.ts
- middleware plan limits test assertions

Add pricing section to landing page (site/index.html + styles.css)
with three-column card layout, featured Pro tier, and nav link.

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread packages/server/src/engine/eventDelivery.ts Outdated
- Fix matchesFilter to reject events with no channel info when a channel
  filter is set (e.g. dm.received should not match filter: {channel: 'alerts'})
- Remove redundant response.ok || status check (response.ok is already 200-299)
- Fix misleading comment about returning promise
- Add test for channel filter rejecting events without channel info (408 tests)

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o
…tency

- Add Stripe webhook signature verification (HMAC-SHA256) when
  STRIPE_WEBHOOK_SECRET is configured
- Wire command.invoked and webhook.received event publishing to their
  route handlers (were defined in types but never published)
- Add channel_id to reaction events so they broadcast to the channel
  instead of the entire workspace
- Add deliverEvent to presenceRefresh so agent.online events are also
  sent to outbound webhook subscriptions
- Add channel.updated, member.joined, member.left event types and
  publish from channel update/join/leave routes
- Add 3 new webhook signature verification tests

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/server/src/routes/webhooks.ts Outdated
- Add 7 missing event types to SubscribableEventType (channel.updated,
  member.joined, member.left, group_dm.received, message.read,
  webhook.received, command.invoked)
- Update MCP WsBridge to map new event types to resource URIs
- Add member.joined event to channel invite route (was missing)
- Add pubsub.js and eventDelivery.js mocks to all 17 route test files
  (previously only webhooks.test.ts had them)
- Add 5 new ws-bridge mapping tests for new event types

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/server/src/routes/channel.ts
…rds, Node 22

- Use raw body buffer (via express.json verify callback) for Stripe webhook
  signature verification instead of re-serialized JSON.stringify(req.body)
- Add res.headersSent guards to all 17 catch blocks where event publishing
  follows res.send(), preventing potential double-response on sync throw
- Upgrade CI, deploy, and Dockerfile from Node 20 to Node 22 (current LTS)
  to fix MCP integration test failure caused by global.fetch mock differences

https://claude.ai/code/session_01NoF17Dac9CpDzg9qScB39o

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/server/src/routes/reaction.ts Outdated
Comment thread packages/server/src/routes/webhooks.ts
khaliqgant and others added 2 commits February 8, 2026 16:21
…ebhooks, reactions

- Strip channel_id from reaction API response before sending to client (#2)
- Add 5-minute timestamp tolerance to webhook sig verification to prevent replay attacks (#3)
- Return 500 on webhook processing failure so Stripe retries instead of swallowing errors (#4)
- Use word boundary in mentions filter to prevent @bob matching @bobby (#5)
- Fix backoff comment (1s, 2s — not 1s, 2s, 4s) (#6)
- Replace redundant void Promise.allSettled with .catch() (#7)
- Clear stripeSubscriptionId on payment failure downgrade (#14)

All 692 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit 3cde54a into main Feb 8, 2026
1 check passed
@khaliqgant khaliqgant deleted the claude/audit-codebase-stubs-qvib9 branch February 8, 2026 15:28

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +29 to +34
app.use(express.json({
verify: (req, _res, buf) => {
// Preserve raw body for Stripe webhook signature verification
(req as Request & { rawBody?: Buffer }).rawBody = buf;
},
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Raw body buffer stored on every request, not just webhook requests

The verify callback in express.json() stores the raw body buffer on every incoming JSON request, not just the Stripe webhook endpoint that needs it. This doubles memory usage for every API call across the entire application.

Performance Impact Details

At packages/server/src/app.ts:29-34, the verify callback unconditionally stores rawBody on every request:

app.use(express.json({
  verify: (req, _res, buf) => {
    (req as Request & { rawBody?: Buffer }).rawBody = buf;
  },
}));

This means every single API call (messages, channels, agents, DMs, etc.) will have its raw body buffered in memory alongside the parsed JSON body. For a messaging platform handling high throughput, this effectively doubles the memory cost of every request body.

The rawBody is only needed by the Stripe webhook endpoint at packages/server/src/routes/webhooks.ts:41 for signature verification. A better approach would be to use a separate express.raw() middleware only on the webhook route, or conditionally store the raw body based on the request path.

Impact: Increased memory usage and GC pressure proportional to total API traffic, not just webhook traffic.

Prompt for agents
Instead of storing rawBody on every request, only store it for the webhook route. One approach is to use a path check inside the verify callback:

app.use(express.json({
  verify: (req, _res, buf) => {
    // Only preserve raw body for Stripe webhook signature verification
    if ((req as any).url === '/billing/webhooks' || (req as any).originalUrl?.endsWith('/billing/webhooks')) {
      (req as Request & { rawBody?: Buffer }).rawBody = buf;
    }
  },
}));

Alternatively, you could use a separate express.raw() middleware mounted only on the webhook route before the JSON parser, or use a dedicated body parser for that specific route.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

} catch (err) {
// Return 500 so Stripe retries on processing failures
if (!res.headersSent) {
res.status(500).json({ ok: false, error: { code: 'webhook_processing_error', message: (err as Error).message } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Webhook processing errors expose internal error messages to external callers

When processWebhook throws an error, the error message is directly included in the 500 response body, potentially leaking internal implementation details (database errors, stack traces, etc.) to external Stripe webhook callers.

Security Concern Details

At packages/server/src/routes/webhooks.ts:58:

res.status(500).json({ ok: false, error: { code: 'webhook_processing_error', message: (err as Error).message } });

The processWebhook function at packages/server/src/engine/webhooks.ts calls database operations (getDb(), db.update(), db.select(), resetUsageCounters()). If any of these fail, the raw error message (which could contain database connection strings, SQL errors, or internal state) is sent directly to the external caller.

Since the /v1/billing/webhooks endpoint is unauthenticated (it doesn't require auth — it's called by Stripe), this could leak internal information to anyone who sends a POST to this endpoint.

Impact: Information disclosure of internal error details to unauthenticated external callers.

Suggested change
res.status(500).json({ ok: false, error: { code: 'webhook_processing_error', message: (err as Error).message } });
res.status(500).json({ ok: false, error: { code: 'webhook_processing_error', message: 'Internal webhook processing error' } });
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

2 participants