Skip to content

Feat/standard webhooks - #61

Merged
thedevyashsaini merged 13 commits into
mainfrom
feat/standard-webhooks
May 29, 2026
Merged

Feat/standard webhooks#61
thedevyashsaini merged 13 commits into
mainfrom
feat/standard-webhooks

Conversation

@thedevyashsaini

@thedevyashsaini thedevyashsaini commented May 28, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Webhook endpoint management API: create, retrieve, delete endpoints and fetch public keys.
    • Send a test webhook from the API.
    • Forward signed payment events (payment.succeeded / payment.failed) to registered webhooks with delivery recordings.
    • Persistent delivery records for webhook attempts and storage of endpoint keys.
    • Certain API routes now require a configured webhook endpoint.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thedevyashsaini, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 56 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee0f4c9d-a9af-4851-9ed5-0da2b1909afe

📥 Commits

Reviewing files that changed from the base of the PR and between d8c082b and 99a6ba3.

📒 Files selected for processing (4)
  • src/interceptors/auth.ts
  • src/routes/http/api/webhookEndpoints.ts
  • src/routes/http/createdCheckout.ts
  • src/routes/http/forwardWebhook.ts
📝 Walkthrough

Walkthrough

Adds end-to-end webhook support: DB schema and relations, Ed25519 keypair generation, storage helpers, a signing+forwarding engine that records deliveries, HTTP handlers/routes (create/get/delete/send-test/public-key), auth enforcement for specific RPCs, integration into payment webhook flow, and a proto submodule pointer update.

Changes

Webhook Endpoint Management and Event Forwarding

Layer / File(s) Summary
Database schema and key generation
src/storage/db/postgres/schema.ts, src/utils/generateWebhookKeyPair.ts
webhookEndpointsTable stores API-key-owned endpoints and PEM key material; webhookDeliveriesTable logs per-delivery events and status. generateWebhookKeyPair() produces Ed25519 PEM keys and a whpk_-prefixed public key form with conversion helpers.
Storage layer for webhook endpoints
src/storage/db/postgres/helpers/webhookEndpoints.ts
getWebhookEndpointByApiKeyId() selects endpoint by API key; upsertWebhookEndpoint() updates-or-inserts URL and key material with .returning() and StorageError wrapping; deleteWebhookEndpoint() soft-deletes by apiKeyId and returns a boolean.
Webhook forwarding engine
src/routes/http/forwardWebhook.ts
forwardWebhook() loads endpoint by apiKeyId, builds JSON body and signature headers, signs using stored PEM private key, POSTs with timeout/abort, captures non-2xx/errors to Sentry, and records a delivered or failed entry in webhookDeliveriesTable.
Webhook endpoint HTTP API handlers and routes
src/routes/http/api/webhookEndpoints.ts, src/routes/http/api/registerApiRoutes.ts
Adds handlers (handleCreateWebhookEndpoint, handleGetWebhookEndpoint, handleDeleteWebhookEndpoint, handleSendTestWebhook, handleGetPublicKey) that authenticate via API key, validate input with Zod, manage endpoints via storage layer (upsert/get/delete), enforce role for test sending, and register routes under /api/v1/internals/webhook-endpoint.
Auth interceptor enforcement
src/interceptors/auth.ts
Adds a cached check that requires a configured, non-deleted webhook endpoint for certain gRPC routes when the caller role is not dashboard, denying permission when absent.
Payment webhook integration
src/routes/http/createdCheckout.ts
Imports and calls forwardWebhook() after successful session updates in payment.failed and payment.succeeded branches, forwarding events with payment/session identifiers, amount/currency/mode, billed cutoff, and timestamps.

Proto Submodule Update

Layer / File(s) Summary
Proto submodule commit
proto
Submodule pointer updated to a new commit revision.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ScrawnDotDev/Scrawn#54: Modifies src/routes/http/createdCheckout.ts for payment.failed/payment.succeeded handling; this PR extends that flow by forwarding events.
  • ScrawnDotDev/Scrawn#34: Changes to API key authentication surface (authenticateHttpApiKey(...)) that the new webhook handlers rely on.

Suggested reviewers

  • SteakFisher

Poem

🐰 A webhook journey, signed and true,
Keys born whpk_ from a rabbit's view,
Events hop outward, stamped and neat,
Delivered or logged when they meet,
Cheers — the forest's webhook is complete!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Feat/standard webhooks" directly reflects the primary change: implementing standard webhook functionality across multiple new files and integrations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/standard-webhooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routes/http/api/webhookEndpoints.ts`:
- Around line 19-21: The createEndpointSchema currently only does syntactic
validation; tighten it by requiring the URL use the https scheme and adding a
Zod refinement on createEndpointSchema that parses the URL (new URL(value)) and
rejects hostnames or IPs that are loopback (127.0.0.0/8, ::1), link-local
(169.254.0.0/16, fe80::/10), or private ranges (10.0.0.0/8, 172.16.0.0/12,
192.168.0.0/16, fc00::/7), and disallow plain "localhost"; and then implement a
runtime re-check in your forwarding code in forwardWebhook.ts (the code that
performs the server-side fetch) to perform DNS resolution
(dns.promises.resolve4/resolve6 or lookup) of the target host, verify each
resolved IP is not in those disallowed ranges before calling fetch, and
throw/reject if any resolved address is disallowed so SSRF attempts are blocked
server-side as well.

In `@src/routes/http/forwardWebhook.ts`:
- Around line 49-53: Replace the built-in Date usage with Luxon UTC APIs: import
{ DateTime } from 'luxon', compute the integer seconds with const timestamp =
Math.floor(DateTime.utc().toSeconds()), and generate the ISO timestamp with
DateTime.fromSeconds(timestamp).toUTC().toISO() (use this in the body where new
Date(timestamp * 1000).toISOString() was used). Update the body construction in
forwardWebhook (variables timestamp and body) to use these Luxon values.

In `@src/storage/db/postgres/helpers/webhookEndpoints.ts`:
- Around line 38-69: The current get -> conditional update/insert in the webhook
endpoint logic (using getWebhookEndpointByApiKeyId then db.update or db.insert
on webhookEndpointsTable) is non-atomic and can race on the unique apiKeyId;
change it to an atomic upsert by using Drizzle's insert ... on conflict do
update (e.g., db.insert(webhookEndpointsTable).values(...).onConflictDoUpdate({
target: webhookEndpointsTable.apiKeyId, set: { url, privateKey, publicKey,
updatedAt: new Date().toISOString() } }).returning()) so a single query handles
insert-or-update deterministically, validate inputs before calling the DB, and
still throw StorageError.emptyResult if the returning result is missing;
alternatively wrap the existing logic in a transaction and explicitly catch
unique-constraint violations when performing the insert and then retry the
update path.
- Line 47: Replace the built-in Date usage for updatedAt with Luxon's UTC
timestamp: import DateTime from Luxon (import { DateTime } from 'luxon') in
webhookEndpoints.ts and set updatedAt to DateTime.utc().toISO(); update any
related create/update blocks in the same module that use new Date() to use
DateTime.utc().toISO() to comply with the repo's UTC/Luxon requirement.
- Line 79: Replace the name-based error check with a typed instanceof check: in
the catch handling where you currently do `if (e instanceof Error && (e as
any).name === "StorageError")`, change it to `if (e instanceof StorageError) {
throw e; }` so the code uses the StorageError class directly (referencing the
StorageError type and the caught variable `e`) and remove the `(e as any).name`
usage.

In `@src/storage/db/postgres/schema.ts`:
- Around line 301-303: The foreign key for webhook_deliveries.endpointId (the
line using uuid("endpoint_id").references(() => webhookEndpointsTable.id)) needs
an explicit delete policy to avoid referential integrity errors when removing
endpoints; update that FK definition to include an onDelete behavior (for
example .onDelete("CASCADE")) so deleting a webhook endpoint either cascades to
its deliveries or uses your chosen policy, keeping the .references(() =>
webhookEndpointsTable.id) and .notNull() intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5dc98042-b65c-4f0f-99ee-c1916d1be754

📥 Commits

Reviewing files that changed from the base of the PR and between 40d505f and 936a631.

📒 Files selected for processing (8)
  • proto
  • src/routes/http/api/registerApiRoutes.ts
  • src/routes/http/api/webhookEndpoints.ts
  • src/routes/http/createdCheckout.ts
  • src/routes/http/forwardWebhook.ts
  • src/storage/db/postgres/helpers/webhookEndpoints.ts
  • src/storage/db/postgres/schema.ts
  • src/utils/generateWebhookKeyPair.ts

Comment thread src/routes/http/api/webhookEndpoints.ts Outdated
Comment thread src/routes/http/forwardWebhook.ts Outdated
Comment thread src/storage/db/postgres/helpers/webhookEndpoints.ts Outdated
Comment thread src/storage/db/postgres/helpers/webhookEndpoints.ts Outdated
Comment thread src/storage/db/postgres/helpers/webhookEndpoints.ts Outdated
Comment thread src/storage/db/postgres/schema.ts
@SteakFisher
SteakFisher self-requested a review May 28, 2026 20:40

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/utils/generateWebhookKeyPair.ts (1)

34-37: 💤 Low value

Remove unused prefixedToPublicKeyPem from src/utils/generateWebhookKeyPair.ts (lines 34-37).

The function is defined but never referenced anywhere in the repo (only its declaration is found), so it’s dead/unreachable code and can be deleted (or wired into the intended verification path).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/generateWebhookKeyPair.ts` around lines 34 - 37, Remove the dead
helper prefixedToPublicKeyPem by deleting its declaration from the module, or if
intended to be used, wire it into the verification flow: locate the verification
function(s) that accept a prefixed public key (e.g., any verifyWebhookSignature
or similar routines in the generateWebhookKeyPair module) and replace inline
base64-to-PEM logic with a call to prefixedToPublicKeyPem, or simply remove
prefixedToPublicKeyPem if it is unused; ensure imports/exports are updated
accordingly and run tests to confirm nothing references prefixedToPublicKeyPem.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/utils/generateWebhookKeyPair.ts`:
- Around line 34-37: Remove the dead helper prefixedToPublicKeyPem by deleting
its declaration from the module, or if intended to be used, wire it into the
verification flow: locate the verification function(s) that accept a prefixed
public key (e.g., any verifyWebhookSignature or similar routines in the
generateWebhookKeyPair module) and replace inline base64-to-PEM logic with a
call to prefixedToPublicKeyPem, or simply remove prefixedToPublicKeyPem if it is
unused; ensure imports/exports are updated accordingly and run tests to confirm
nothing references prefixedToPublicKeyPem.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4568906d-d893-442c-bbfe-0f13362cd5ef

📥 Commits

Reviewing files that changed from the base of the PR and between 936a631 and 115df80.

📒 Files selected for processing (2)
  • src/storage/db/postgres/schema.ts
  • src/utils/generateWebhookKeyPair.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/storage/db/postgres/schema.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/routes/http/api/webhookEndpoints.ts (1)

22-36: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

SSRF surface remains only partially mitigated.

Production now enforces https://, but there is still no protection against private/loopback/link-local targets or DNS-rebinding, and test mode accepts any valid URL (including http:// to internal hosts). This value is later used for a server-side fetch in forwardWebhook.ts. Consider rejecting private/internal address ranges and re-validating the resolved IP at send time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/http/api/webhookEndpoints.ts` around lines 22 - 36, The current
validation in webhookEndpoints.ts (mode variable and the z.object/url/refine
chain) only enforces HTTPS and doesn't block private/loopback/link-local hosts
or DNS-rebinding; update validation to reject IP literals in private ranges and
common local hostnames (e.g., 127.0.0.1, ::1, localhost, 169.254.x.x, 10.x.x.x,
192.168.x.x, 172.16-31.x.x) by adding an extra refine that parses the URL host
and fails for those patterns, and in forwardWebhook.ts re-validate at send time
by resolving the hostname (DNS lookup) and checking the resolved IP(s) against
the same private/CIDR lists before performing fetch to prevent DNS-rebinding;
reference the schema in webhookEndpoints.ts and the send/fetch logic in
forwardWebhook.ts when implementing these checks.
🧹 Nitpick comments (1)
src/routes/http/api/webhookEndpoints.ts (1)

21-21: 💤 Low value

Add an explicit return type to getCreateEndpointSchema.

The two branches return different inferred Zod shapes; declare the contract explicitly.

♻️ Suggested signature
-function getCreateEndpointSchema(mode: "test" | "production" | null) {
+function getCreateEndpointSchema(
+  mode: "test" | "production" | null
+): z.ZodType<{ url: string }> {

As per coding guidelines: "Always use explicit types for function parameters and return values".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/http/api/webhookEndpoints.ts` at line 21, The function
getCreateEndpointSchema currently relies on inferred Zod return types from two
different branches; add an explicit return type such as ZodTypeAny (imported
from 'zod') to its signature — e.g. declare getCreateEndpointSchema(mode: "test"
| "production" | null): ZodTypeAny — and ensure you import the type (import type
{ ZodTypeAny } from 'zod'); this makes the contract explicit and avoids
mismatched inferred shapes between the branches (verify the returned zod schemas
from both branches still satisfy the declared type).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/interceptors/auth.ts`:
- Around line 79-98: The webhookEndpointCache TTL causes stale auth decisions
because checkWebhookEndpoint caches existence and upsert/delete flows never
invalidate it; update the upsertWebhookEndpoint and deleteWebhookEndpoint code
paths (or the HTTP handlers that call them) to call
webhookEndpointCache.delete(apiKeyId) after a successful upsert or delete so
checkWebhookEndpoint will re-query the DB, referencing the webhookEndpointCache
symbol and the checkWebhookEndpoint, upsertWebhookEndpoint,
deleteWebhookEndpoint functions (and if easier, add the same invalidation in the
RegisterEvent/StreamEvents/CreateCheckoutLink handlers after changes).

---

Duplicate comments:
In `@src/routes/http/api/webhookEndpoints.ts`:
- Around line 22-36: The current validation in webhookEndpoints.ts (mode
variable and the z.object/url/refine chain) only enforces HTTPS and doesn't
block private/loopback/link-local hosts or DNS-rebinding; update validation to
reject IP literals in private ranges and common local hostnames (e.g.,
127.0.0.1, ::1, localhost, 169.254.x.x, 10.x.x.x, 192.168.x.x, 172.16-31.x.x) by
adding an extra refine that parses the URL host and fails for those patterns,
and in forwardWebhook.ts re-validate at send time by resolving the hostname (DNS
lookup) and checking the resolved IP(s) against the same private/CIDR lists
before performing fetch to prevent DNS-rebinding; reference the schema in
webhookEndpoints.ts and the send/fetch logic in forwardWebhook.ts when
implementing these checks.

---

Nitpick comments:
In `@src/routes/http/api/webhookEndpoints.ts`:
- Line 21: The function getCreateEndpointSchema currently relies on inferred Zod
return types from two different branches; add an explicit return type such as
ZodTypeAny (imported from 'zod') to its signature — e.g. declare
getCreateEndpointSchema(mode: "test" | "production" | null): ZodTypeAny — and
ensure you import the type (import type { ZodTypeAny } from 'zod'); this makes
the contract explicit and avoids mismatched inferred shapes between the branches
(verify the returned zod schemas from both branches still satisfy the declared
type).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aad1ed74-d34f-459b-be5e-23fd01458a5e

📥 Commits

Reviewing files that changed from the base of the PR and between 115df80 and d8c082b.

📒 Files selected for processing (5)
  • src/interceptors/auth.ts
  • src/routes/http/api/registerApiRoutes.ts
  • src/routes/http/api/webhookEndpoints.ts
  • src/routes/http/forwardWebhook.ts
  • src/storage/db/postgres/helpers/webhookEndpoints.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/routes/http/api/registerApiRoutes.ts
  • src/routes/http/forwardWebhook.ts

Comment thread src/interceptors/auth.ts
@thedevyashsaini
thedevyashsaini merged commit b510af2 into main May 29, 2026
3 checks passed
This was referenced May 30, 2026
@SteakFisher
SteakFisher deleted the feat/standard-webhooks branch June 6, 2026 18:36
@SteakFisher
SteakFisher restored the feat/standard-webhooks branch June 6, 2026 18:36
@SteakFisher
SteakFisher deleted the feat/standard-webhooks branch June 8, 2026 16:31
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.

1 participant