Skip to content

feat(webhooks): enforce standard webhook API access#2244

Open
riderx wants to merge 8 commits into
mainfrom
codex/standard-webhooks-compliance
Open

feat(webhooks): enforce standard webhook API access#2244
riderx wants to merge 8 commits into
mainfrom
codex/standard-webhooks-compliance

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented May 12, 2026

Summary (AI generated)

  • Add Standard Webhooks-compatible payload type, webhook-id, webhook-timestamp, and webhook-signature headers while keeping legacy X-Capgo-* headers for compatibility.
  • Move console webhook CRUD and delivery views through the webhook API, and lock direct webhooks / webhook_deliveries table access away from anon/authenticated Supabase SDK clients.
  • Replace webhook retry behavior with the Standard Webhooks schedule, Retry-After handling, throttling for load-related responses, 410 auto-disable, and disable-after-max-attempts.
  • Update verification docs and tests for API-only access and Standard Webhooks signatures.

Motivation (AI generated)

Webhook secrets and delivery payloads should not be readable or mutable directly from the console Supabase SDK. Centralizing CRUD through API handlers removes duplicated access logic and gives us one authorization path while moving the delivery protocol closer to the Standard Webhooks spec: https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md

Business Impact (AI generated)

This reduces the risk of webhook secret exposure, keeps webhook behavior consistent between API users and the console, and improves interoperability for customers using Standard Webhooks-compatible verification tooling without breaking existing Capgo webhook consumers.

Test Plan (AI generated)

  • bun run lint:sql supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • bun lint
  • bun run lint:backend
  • bun typecheck
  • bunx vitest run tests/webhook-delivery-security.unit.test.ts tests/webhook-delivery-redirect.unit.test.ts tests/webhook-signature.test.ts tests/webhooks.test.ts tests/webhooks-apikey-policy.test.ts tests/webhook-queue-processing.test.ts
  • bunx vitest run tests/hashed-apikey-rls.test.ts
  • bun test:backend

Generated with AI

Summary by CodeRabbit

  • New Features

    • Standard Webhooks with v1,{base64_signature} signatures and option to choose legacy vs. standard delivery; webhook secrets now use whsec_ base64 format
    • New delivery headers (webhook-id, webhook-timestamp, webhook-signature); verifier accepts multiple signatures
    • Delivery preview body capped; retry scheduling honors Retry-After, uses jittered delays, default max attempts = 10; auto-disable on permanent failures
  • Security Improvements

    • Stricter row-level access and admin-only table access
  • UI

    • Signing-secret shown only on creation; UI copy and delivery-version selector updated
  • Tests

    • Expanded coverage for signatures, retry scheduling, delivery limits, and access-denial behavior

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces legacy Capgo webhook signing with Standard Webhooks v1 (base64), moves frontend webhook CRUD to Edge Functions, migrates handlers to AuthInfo + supabaseAdmin via middlewareV2, tightens DB RLS/privileges, and adds retry scheduling and automatic disablement.

Changes

Standard Webhooks Migration

Layer / File(s) Summary
UI strings and verification
messages/en.json, src/pages/settings/organization/Webhooks.vue
Localizations updated to document Standard Webhooks v1,{base64} and webhook-signature; frontend verification example now decodes whsec_ secrets and validates webhook-id, webhook-timestamp, and webhook-signature; secret shown only when present.
Frontend types and form UI
src/components/WebhookDeliveryLog.vue, src/components/WebhookForm.vue, src/pages/settings/organization/Webhooks.vue, src/auto-imports.d.ts, src/types/supabase.types.ts
Introduced exported Webhook and WebhookDeliveryVersion types; updated component prop and page state typings; added Delivery Version select and included deliveryVersion in submit payloads; updated auto-imported type re-exports.
Frontend store → Edge Functions
src/stores/webhooks.ts
Migrated Pinia store to call Supabase Edge Functions for webhook CRUD, deliveries, tests, and retries; added function-path and error helpers; store state and actions now use the Webhook model and accept deliveryVersion.
Public handlers, routing, and response schemas
supabase/functions/_backend/public/webhooks/*.ts, response.ts, index.ts
Handlers use middlewareV2/AuthInfo + checkWebhookPermissionV2, operate via supabaseAdmin(c), accept optional deliveryVersion, select via webhookPublicSelect/webhookCreatedSelect, and index.ts extracts auth and passes it into handlers.
Delivery utilities, signatures & retry helpers
supabase/functions/_backend/utils/webhook.ts
Added type to payloads; implemented Standard Webhooks signature generation (v1,<base64>) alongside legacy signatures; added payload-version abstractions, response-preview cap, parseRetryAfterSeconds, getWebhookRetryDelaySeconds (jitter/throttling/caps), scheduleRetry, disableWebhook, and extended deliverWebhook to accept deliveryVersion and return retryAfter.
Dispatcher & trigger orchestration
supabase/functions/_backend/triggers/webhook_dispatcher.ts, supabase/functions/_backend/triggers/webhook_delivery.ts
Dispatcher builds delivery-specific payloads per webhook delivery_version; trigger marks 410 responses as failed and disables webhook, schedules retries via scheduleRetry using retryAfter/status with `delivery.max_attempts
Database migration: secrets, defaults & RLS
supabase/migrations/20260512080234_standard_webhook_secrets.sql
Set webhooks.secret default to whsec_ + base64(32) and add comment; added delivery_version columns and CHECK constraints; set webhook_deliveries.max_attempts default to 10; revoked direct privileges from anon/authenticated/public and granted to service_role; recreated deny-direct RLS policies for both tables.
Tests & helpers
tests/*.test.ts
Refactored tests to validate Standard signatures with Node crypto and timing-safe comparisons, assert whsec_ secret format and 32-byte decode, expect direct SDK access denied (42501), add delivery header and response-preview assertions, retry-scheduling tests, update polling helper to wait for attempt, and add endpoint/auth helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Cap-go/capgo#2199: Both PRs modify the same webhook utility and public webhook handler files (notably supabase/functions/_backend/utils/webhook.ts and related handlers).
  • Cap-go/capgo#2016: Related webhook permission/auth logic and checkWebhookPermissionV2 changes.
  • Cap-go/capgo#2090: Prior edits to webhook utilities overlapping with signature/delivery utilities.

"🐇 I nibbled hex, then baked base64 bright,
Whsec_ tucked in secrets, signed by night.
Edge Functions hum while RLS stands guard,
Retries jittered, webhooks disabled when marred,
A rabbit’s small hop for signatures done right."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% 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
Title check ✅ Passed The title clearly and specifically describes the main change: enforcing Standard Webhooks API access by restricting direct table access and routing through API endpoints.
Description check ✅ Passed The PR description includes a comprehensive summary, motivation, business impact, and detailed test plan that align with the repository's template structure and requirements.
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 codex/standard-webhooks-compliance

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 12, 2026

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing codex/standard-webhooks-compliance (3f185ac) with main (a03601d)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@riderx riderx force-pushed the codex/standard-webhooks-compliance branch 2 times, most recently from efb8b83 to 3f58b47 Compare May 12, 2026 09:18
@riderx riderx marked this pull request as ready for review May 12, 2026 09:36
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
supabase/functions/_backend/utils/webhook.ts (1)

371-386: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the delivery timeout active until the response body is fully read.

Line 383 clears the abort timer as soon as headers arrive. A receiver can then stall response.text() indefinitely, and an early fetch() failure skips clearTimeout() entirely. This leaves an external call effectively unbounded on the worker path.

💡 Suggested fix
   try {
     const controller = new AbortController()
     const timeoutId = setTimeout(() => controller.abort(), WEBHOOK_DELIVERY_TIMEOUT_MS)
-
-    const response = await fetch(url, {
-      method: 'POST',
-      headers,
-      body: payloadString,
-      redirect: 'manual',
-      signal: controller.signal,
-    })
-
-    clearTimeout(timeoutId)
-    const duration = Date.now() - startTime
-    const responseBody = await response.text()
-    const retryAfter = response.headers.get('retry-after')
+    try {
+      const response = await fetch(url, {
+        method: 'POST',
+        headers,
+        body: payloadString,
+        redirect: 'manual',
+        signal: controller.signal,
+      })
+
+      const duration = Date.now() - startTime
+      const responseBody = await response.text()
+      const retryAfter = response.headers.get('retry-after')

-    cloudlog({
-      requestId: c.get('requestId'),
-      message: 'Webhook delivery attempt',
-      deliveryId,
-      url,
-      status: response.status,
-      success: response.ok,
-      duration,
-    })
+      cloudlog({
+        requestId: c.get('requestId'),
+        message: 'Webhook delivery attempt',
+        deliveryId,
+        url,
+        status: response.status,
+        success: response.ok,
+        duration,
+      })

-    return {
-      success: response.ok,
-      status: response.status,
-      body: responseBody.slice(0, 10000), // Limit stored body size
-      duration,
-      retryAfter,
+      return {
+        success: response.ok,
+        status: response.status,
+        body: responseBody.slice(0, 10000), // Limit stored body size
+        duration,
+        retryAfter,
+      }
+    }
+    finally {
+      clearTimeout(timeoutId)
     }
   }
🤖 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 `@supabase/functions/_backend/utils/webhook.ts` around lines 371 - 386, The
abort timer is cleared immediately after fetch resolves headers, allowing a
stalled response.text() to hang; also if fetch throws the timeout isn't cleared.
Keep the delivery timeout active until the full body is read by moving
clearTimeout(timeoutId) to after await response.text() (so the timer and
AbortController.signal cover the entire read), and ensure timeoutId is cleared
in a finally block so it's always cleaned up if fetch or response.text() throws;
reference the AbortController, timeoutId, WEBHOOK_DELIVERY_TIMEOUT_MS, the
fetch(...) call, and response.text() in the changes.
supabase/functions/_backend/public/webhooks/deliveries.ts (1)

148-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing error handling for delivery update.

The update to reset delivery state doesn't check for errors. If this fails, the delivery would be queued but with incorrect state data.

Suggested fix
   // Reset delivery status and queue for retry
-  await supabase
+  const { error: updateError } = await supabase
     .from('webhook_deliveries')
     .update({
       status: 'pending',
       attempt_count: 0,
       response_status: null,
       response_body: null,
       completed_at: null,
       duration_ms: null,
       next_retry_at: null,
     })
     .eq('id', body.deliveryId)
+
+  if (updateError) {
+    throw simpleError('update_failed', 'Failed to reset delivery for retry', { error: updateError })
+  }
🤖 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 `@supabase/functions/_backend/public/webhooks/deliveries.ts` around lines 148 -
159, The update call to reset delivery state using
supabase.from('webhook_deliveries').update(...).eq('id', body.deliveryId) lacks
error handling; capture the result (e.g., const { data, error } = await
supabase.from(...).update(...).eq(...)), check if error is truthy, and handle it
(log the error with context and the deliveryId using your logger or
console.error and return or throw an appropriate error/HTTP response so the
caller knows the reset failed) to prevent leaving the delivery in an
inconsistent state.
supabase/functions/_backend/public/webhooks/put.ts (1)

80-94: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid returning full webhook rows from the update endpoint

This response currently returns the entire webhooks row (select()), which can leak sensitive columns (notably the webhook signing secret). Return an explicit safe column list instead.

Suggested fix
+const webhookPublicSelect = 'id, org_id, name, url, enabled, events, created_at, updated_at, created_by'
+
 const { data, error } = await supabase
   .from('webhooks')
   .update(updateData)
   .eq('id', body.webhookId)
-  .select()
+  .select(webhookPublicSelect)
   .single()
🤖 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 `@supabase/functions/_backend/public/webhooks/put.ts` around lines 80 - 94, The
update endpoint currently calls
supabase.from('webhooks').update(updateData).eq('id',
body.webhookId).select().single(), which returns the full row (including
sensitive fields like signing_secret); change the .select() call to explicitly
list only safe columns (e.g.,
.select('id,name,url,owner_id,created_at,updated_at') or whatever non-secret
columns your webhooks table exposes) so the response does not leak secrets, keep
the rest of the flow (error handling via simpleError and returning c.json with
webhook: data) unchanged.
🧹 Nitpick comments (1)
tests/webhook-queue-processing.test.ts (1)

185-185: 💤 Low value

Use it.concurrent() for parallel test execution.

Per coding guidelines, tests should use it.concurrent() instead of it() to enable parallel execution within the same file for faster CI/CD.

Suggested fix
-  it('dispatches and delivers webhook queue messages end to end', { timeout: 30000 }, async () => {
+  it.concurrent('dispatches and delivers webhook queue messages end to end', { timeout: 30000 }, async () => {

As per coding guidelines: "use it.concurrent() instead of it() to run tests in parallel within the same file for faster CI/CD"

🤖 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 `@tests/webhook-queue-processing.test.ts` at line 185, The test declaration
using it(...) should be changed to it.concurrent(...) to enable parallel
execution; locate the test with the title "dispatches and delivers webhook queue
messages end to end" (in tests/webhook-queue-processing.test.ts) and replace the
it(...) call with it.concurrent(...), preserving the existing timeout option ({
timeout: 30000 }) and the async test function signature so behavior and timing
remain unchanged.
🤖 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 `@supabase/functions/_backend/public/webhooks/get.ts`:
- Line 21: The webhook signing secret is being exposed because webhookSchema
includes secret and code uses select('*'); fix by removing/excluding the secret
from outgoing responses: create a separate response schema (e.g.,
WebhookResponse or stripSecretFromWebhook) that omits the secret field and use
that for serialization, and replace any select('*') calls with explicit column
lists that do not include "secret" (or explicitly omit "secret") in all handlers
that return webhooks (references: webhookSchema, select('*') usages and the
GET/list and single endpoint handlers currently returning the schema). Ensure
you update the list and single-response paths (and any other places noted) to
return the non-secret schema.

In `@supabase/functions/_backend/public/webhooks/put.ts`:
- Around line 29-33: The handler currently uses the admin client
supabaseAdmin(c) which bypasses RLS; replace it with the user-authenticated
Supabase client obtained from the request context (the same client used for
user-facing endpoints) so RLS and user session enforcement apply. Locate the
supabase variable created via supabaseAdmin(c) in put.ts and swap it for the
authenticated client factory (the one that reads the request/session) and ensure
you still query .from('webhooks') and handle existingWebhook and fetchError as
before; also ensure the request's auth token/session is forwarded to the client
so permission checks run.

In `@supabase/functions/_backend/utils/webhook.ts`:
- Around line 300-319: The function getWebhookRetryDelaySeconds allows negative
jitter to drop the final delay below enforced minimums (Retry-After and the
5-minute throttle); fix by computing the enforced minimum first (use
WEBHOOK_RETRY_DELAYS_SECONDS[retryIndex], increase to 5*60 when status in
WEBHOOK_RETRY_THROTTLE_STATUSES, and take parseRetryAfterSeconds(retryAfter) if
present) then add jitter to that base delay and finally clamp the result so it
cannot go below that enforced minimum and cannot exceed
WEBHOOK_MAX_RETRY_AFTER_SECONDS; update the jitter logic around the symbols
getWebhookRetryDelaySeconds, WEBHOOK_RETRY_DELAYS_SECONDS,
WEBHOOK_RETRY_THROTTLE_STATUSES, parseRetryAfterSeconds, and
WEBHOOK_MAX_RETRY_AFTER_SECONDS accordingly.

---

Outside diff comments:
In `@supabase/functions/_backend/public/webhooks/deliveries.ts`:
- Around line 148-159: The update call to reset delivery state using
supabase.from('webhook_deliveries').update(...).eq('id', body.deliveryId) lacks
error handling; capture the result (e.g., const { data, error } = await
supabase.from(...).update(...).eq(...)), check if error is truthy, and handle it
(log the error with context and the deliveryId using your logger or
console.error and return or throw an appropriate error/HTTP response so the
caller knows the reset failed) to prevent leaving the delivery in an
inconsistent state.

In `@supabase/functions/_backend/public/webhooks/put.ts`:
- Around line 80-94: The update endpoint currently calls
supabase.from('webhooks').update(updateData).eq('id',
body.webhookId).select().single(), which returns the full row (including
sensitive fields like signing_secret); change the .select() call to explicitly
list only safe columns (e.g.,
.select('id,name,url,owner_id,created_at,updated_at') or whatever non-secret
columns your webhooks table exposes) so the response does not leak secrets, keep
the rest of the flow (error handling via simpleError and returning c.json with
webhook: data) unchanged.

In `@supabase/functions/_backend/utils/webhook.ts`:
- Around line 371-386: The abort timer is cleared immediately after fetch
resolves headers, allowing a stalled response.text() to hang; also if fetch
throws the timeout isn't cleared. Keep the delivery timeout active until the
full body is read by moving clearTimeout(timeoutId) to after await
response.text() (so the timer and AbortController.signal cover the entire read),
and ensure timeoutId is cleared in a finally block so it's always cleaned up if
fetch or response.text() throws; reference the AbortController, timeoutId,
WEBHOOK_DELIVERY_TIMEOUT_MS, the fetch(...) call, and response.text() in the
changes.

---

Nitpick comments:
In `@tests/webhook-queue-processing.test.ts`:
- Line 185: The test declaration using it(...) should be changed to
it.concurrent(...) to enable parallel execution; locate the test with the title
"dispatches and delivers webhook queue messages end to end" (in
tests/webhook-queue-processing.test.ts) and replace the it(...) call with
it.concurrent(...), preserving the existing timeout option ({ timeout: 30000 })
and the async test function signature so behavior and timing remain unchanged.
🪄 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

Run ID: 6718a486-e9da-489c-bd2c-0727a7536d6d

📥 Commits

Reviewing files that changed from the base of the PR and between a4f7c93 and 3f58b47.

📒 Files selected for processing (19)
  • messages/en.json
  • src/pages/settings/organization/Webhooks.vue
  • src/stores/webhooks.ts
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/public/webhooks/get.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/webhook-delivery-redirect.unit.test.ts
  • tests/webhook-delivery-security.unit.test.ts
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-signature.test.ts
  • tests/webhooks.test.ts

Comment thread supabase/functions/_backend/public/webhooks/get.ts Outdated
Comment thread supabase/functions/_backend/public/webhooks/put.ts
Comment thread supabase/functions/_backend/utils/webhook.ts Outdated
@riderx riderx force-pushed the codex/standard-webhooks-compliance branch from 3f58b47 to 237cc74 Compare May 12, 2026 09:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/stores/webhooks.ts (2)

352-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset delivery state on missing org and failed delivery fetches.

deliveries and deliveryPagination stay populated when this request bails out, so switching webhooks/orgs and then hitting an API error can display the previous webhook’s payloads and response bodies under the new selection.

💡 Suggested fix
     if (!orgId) {
+      deliveries.value = []
+      deliveryPagination.value = null
       console.error('No organization selected')
       return
     }
@@
       if (error) {
+        deliveries.value = []
+        deliveryPagination.value = null
         console.error('Failed to fetch deliveries:', error)
         return
       }
@@
     catch (err) {
+      deliveries.value = []
+      deliveryPagination.value = null
       console.error('Error fetching deliveries:', err)
     }
🤖 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/stores/webhooks.ts` around lines 352 - 380, The deliveries list and
pagination are not cleared when the fetch bails (no orgId) or fails, causing old
data to show; update the logic in the block that calls supabase.functions.invoke
(using buildWebhookFunctionPath) so that when orgId is missing, when the
response has error, or in the catch handler you explicitly set deliveries.value
= [] and deliveryPagination.value = null before returning, and retain
isLoadingDeliveries.value reset in the finally block.

69-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear cached webhooks when org context is missing or the fetch fails.

These early returns leave the previous org’s webhook list in Pinia state, so a failed org switch can keep showing stale webhook URLs/events from the last org in the current view.

💡 Suggested fix
     if (!orgId) {
+      webhooks.value = []
       console.error('No organization selected')
       return
     }
@@
       if (error) {
+        webhooks.value = []
         console.error('Failed to fetch webhooks:', error)
         return
       }
@@
     catch (err) {
+      webhooks.value = []
       console.error('Error fetching webhooks:', err)
     }
🤖 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/stores/webhooks.ts` around lines 69 - 89, The early returns in the
webhook fetch leave stale webhooks in Pinia; update the fetch logic around
orgId, the supabase.functions.invoke error branch, and the catch block to clear
cached webhooks and reset loading — specifically set webhooks.value = [] (and
ensure isLoading.value = false) before returning when orgId is missing, when the
invoke returns an error, and inside the catch block; locate these changes around
the orgId check, the try/await call to
supabase.functions.invoke(buildWebhookFunctionPath('webhooks', { orgId })), and
the error handling surrounding isLoading.value and webhooks.value.
src/pages/settings/organization/Webhooks.vue (1)

205-221: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the verification example aligned with legacy secret decoding.

The backend still signs non-whsec_ secrets as raw UTF-8 bytes, and it also falls back to raw text for malformed historical whsec_ values. This sample always base64-decodes, so it will reject some existing secrets that the server still accepts.

💡 Suggested fix
+function decodeWebhookSecret(secret) {
+  if (!secret.startsWith('whsec_'))
+    return Buffer.from(secret, 'utf8')
+
+  const decoded = Buffer.from(secret.slice('whsec_'.length), 'base64')
+  return decoded.length >= 24 && decoded.length <= 64
+    ? decoded
+    : Buffer.from(secret, 'utf8')
+}
+
 function verifyWebhookSignature(rawBody, headers, secret) {
@@
-  const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
+  const secretBytes = decodeWebhookSecret(secret)
   const signaturePayload = `${messageId}.${timestamp}.${rawBody}`
   const hmac = crypto.createHmac('sha256', secretBytes)
🤖 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/pages/settings/organization/Webhooks.vue` around lines 205 - 221, The
verifyWebhookSignature function currently always base64-decodes the secret
(after stripping whsec_), which mismatches the backend that accepts raw UTF-8
secrets and falls back to raw text when historical whsec_ values are malformed;
update verifyWebhookSignature to: if secret startsWith 'whsec_' attempt to
base64-decode secret.slice(7) inside a try/catch and on decode error fall back
to Buffer.from(originalSuffix, 'utf8'); if secret does not start with 'whsec_'
use Buffer.from(secret, 'utf8'); then proceed to build signaturePayload and
compute expectedSignature as before.
🤖 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 `@supabase/functions/_backend/utils/webhook.ts`:
- Around line 379-408: The current code calls response.text() which loads the
entire response into memory; replace that with a streaming read that caps bytes
read (e.g., 10000) to avoid large allocations: add a helper like
readWebhookResponsePreview(response, maxBytes) that uses
response.body?.getReader(), a TextDecoder, reads chunks until maxBytes reached
or stream ends, decodes only the needed subarray, calls reader.cancel() and
final decoder.decode(), and returns the preview string; then use that helper
instead of response.text() when setting responseBody in the webhook delivery
logic (the try block that currently assigns responseBody and returns the
object).

---

Outside diff comments:
In `@src/pages/settings/organization/Webhooks.vue`:
- Around line 205-221: The verifyWebhookSignature function currently always
base64-decodes the secret (after stripping whsec_), which mismatches the backend
that accepts raw UTF-8 secrets and falls back to raw text when historical whsec_
values are malformed; update verifyWebhookSignature to: if secret startsWith
'whsec_' attempt to base64-decode secret.slice(7) inside a try/catch and on
decode error fall back to Buffer.from(originalSuffix, 'utf8'); if secret does
not start with 'whsec_' use Buffer.from(secret, 'utf8'); then proceed to build
signaturePayload and compute expectedSignature as before.

In `@src/stores/webhooks.ts`:
- Around line 352-380: The deliveries list and pagination are not cleared when
the fetch bails (no orgId) or fails, causing old data to show; update the logic
in the block that calls supabase.functions.invoke (using
buildWebhookFunctionPath) so that when orgId is missing, when the response has
error, or in the catch handler you explicitly set deliveries.value = [] and
deliveryPagination.value = null before returning, and retain
isLoadingDeliveries.value reset in the finally block.
- Around line 69-89: The early returns in the webhook fetch leave stale webhooks
in Pinia; update the fetch logic around orgId, the supabase.functions.invoke
error branch, and the catch block to clear cached webhooks and reset loading —
specifically set webhooks.value = [] (and ensure isLoading.value = false) before
returning when orgId is missing, when the invoke returns an error, and inside
the catch block; locate these changes around the orgId check, the try/await call
to supabase.functions.invoke(buildWebhookFunctionPath('webhooks', { orgId })),
and the error handling surrounding isLoading.value and webhooks.value.
🪄 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

Run ID: 2e5e0e25-39b7-45f2-af10-c143150382bd

📥 Commits

Reviewing files that changed from the base of the PR and between 3f58b47 and 237cc74.

📒 Files selected for processing (22)
  • messages/en.json
  • src/components/WebhookDeliveryLog.vue
  • src/components/WebhookForm.vue
  • src/pages/settings/organization/Webhooks.vue
  • src/stores/webhooks.ts
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/public/webhooks/get.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/webhook-delivery-redirect.unit.test.ts
  • tests/webhook-delivery-security.unit.test.ts
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-signature.test.ts
  • tests/webhooks.test.ts
✅ Files skipped from review due to trivial changes (3)
  • tests/webhook-delivery-redirect.unit.test.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • tests/webhook-delivery-security.unit.test.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • tests/webhooks.test.ts
  • tests/webhook-queue-processing.test.ts
  • messages/en.json
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • tests/hashed-apikey-rls.test.ts
  • tests/webhook-signature.test.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql

Comment thread supabase/functions/_backend/utils/webhook.ts
@subhajitlucky
Copy link
Copy Markdown

Review note: this still logs raw webhook URLs in delivery paths, which can expose customer endpoint secrets.

In deliverWebhook(), the validation-blocked path, normal attempt log, and catch path all include url directly in cloudlog / cloudlogErr. Webhook URLs often include shared tokens or signed query parameters, and this PR is otherwise moving webhook data behind API/service-role paths. Logging the full URL keeps those secrets in routine logs.

I would replace the raw url field with sanitized metadata, for example protocol, hostname length or hostname hash, path segment count, hasQuery, and hasCredentials. If host-level debugging is needed, a keyed hash of the normalized hostname is safer than the original URL. This should also be covered by a unit test with a URL like https://example.com/hook?token=secret asserting the secret is absent from logged fields.

@subhajitlucky
Copy link
Copy Markdown

Second review note: deliverWebhook() still reads the entire receiver response before truncating it.

The code does const responseBody = await response.text() and only then stores responseBody.slice(0, 10000). A webhook endpoint can return a very large body and force the worker to buffer it all in memory before the 10KB cap is applied. That means the stored payload is bounded, but the runtime memory/read cost is not.

I would use a bounded stream reader here, similar to the helper introduced in other hardening PRs, and stop reading once the response preview limit is exceeded. A unit test with a chunked response larger than the preview cap would cover the regression.

@riderx riderx force-pushed the codex/standard-webhooks-compliance branch from 237cc74 to af9a70f Compare May 12, 2026 11:04
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
supabase/functions/_backend/utils/webhook.ts (1)

219-241: 💤 Low value

Verify edge case handling in decodeSerializedWebhookSecret.

The function handles both new whsec_ prefixed base64 secrets and legacy raw-text secrets. However, codePointAt(i) can return undefined for indices beyond the string length, and the fallback ?? 0 handles this. The length check (24-64 bytes) ensures compatibility with Standard Webhooks symmetric keys.

Consider adding a brief comment explaining the byte length validation for future maintainers.

🤖 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 `@supabase/functions/_backend/utils/webhook.ts` around lines 219 - 241,
Summary: Add a clarifying comment about the byte-length check in
decodeSerializedWebhookSecret. Update the decodeSerializedWebhookSecret function
to include a short inline comment immediately above the bytes.length validation
explaining that Standard Webhooks symmetric keys are expected to be 24–64 random
bytes (and that existing Capgo hex strings decode to 24 bytes), so the length
check allows base64-decoded whsec_ secrets to be treated as binary keys while
falling back to legacy raw-text signing otherwise; keep existing handling of
codePointAt(i) with the ?? 0 fallback unchanged.
🤖 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 `@tests/webhooks.test.ts`:
- Line 5: Replace usages of the BASE_URL constant with the getEndpointUrl(path)
test helper in this test file so webhook endpoint calls route to the correct
backend in worker-mode; locate any references to BASE_URL (imported from
test-utils.ts and used to construct webhook URLs in tests/webhooks.test.ts) and
change them to call getEndpointUrl('/your/webhook/path') or compose the path via
getEndpointUrl, ensuring imports include getEndpointUrl and removing BASE_URL
where no longer needed.
- Around line 195-240: The tests rely on file-scoped mutable state
(createdWebhookId, lastDeliveryId) and explicit ordering; remove that dependency
by giving each test its own setup/teardown: replace uses of
createdWebhookId/lastDeliveryId with per-test values returned from a factory
helper (e.g., createTestWebhook(), createTestDelivery()) invoked in a beforeEach
or inline inside each it, and update tests that call
getAuthenticatedAnonClient()/fetchWithRetry() to operate on those per-test IDs;
alternatively, if ordering truly cannot be avoided, add a top-of-file comment
and test-runner annotation to mark the file as sequential (i.e., disable
concurrent execution) and document why.

---

Nitpick comments:
In `@supabase/functions/_backend/utils/webhook.ts`:
- Around line 219-241: Summary: Add a clarifying comment about the byte-length
check in decodeSerializedWebhookSecret. Update the decodeSerializedWebhookSecret
function to include a short inline comment immediately above the bytes.length
validation explaining that Standard Webhooks symmetric keys are expected to be
24–64 random bytes (and that existing Capgo hex strings decode to 24 bytes), so
the length check allows base64-decoded whsec_ secrets to be treated as binary
keys while falling back to legacy raw-text signing otherwise; keep existing
handling of codePointAt(i) with the ?? 0 fallback unchanged.
🪄 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

Run ID: cb8a2102-7826-4d68-8380-5879f57305cc

📥 Commits

Reviewing files that changed from the base of the PR and between 237cc74 and af9a70f.

📒 Files selected for processing (22)
  • messages/en.json
  • src/components/WebhookDeliveryLog.vue
  • src/components/WebhookForm.vue
  • src/pages/settings/organization/Webhooks.vue
  • src/stores/webhooks.ts
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/public/webhooks/get.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/webhook-delivery-redirect.unit.test.ts
  • tests/webhook-delivery-security.unit.test.ts
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-signature.test.ts
  • tests/webhooks.test.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/webhook-delivery-redirect.unit.test.ts
  • src/components/WebhookDeliveryLog.vue
🚧 Files skipped from review as they are similar to previous changes (13)
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • src/components/WebhookForm.vue
  • tests/hashed-apikey-rls.test.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • tests/webhook-queue-processing.test.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • supabase/functions/_backend/public/webhooks/get.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • tests/webhook-signature.test.ts

Comment thread tests/webhooks.test.ts Outdated
Comment thread tests/webhooks.test.ts
@riderx riderx force-pushed the codex/standard-webhooks-compliance branch from af9a70f to 899cb44 Compare May 12, 2026 11:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@supabase/functions/_backend/public/webhooks/test.ts`:
- Around line 82-85: The update to attempt_count on the webhook_deliveries row
is not checked; modify the call to await
supabase.from('webhook_deliveries').update(...) .eq('id', delivery.id) to
capture the result and error, verify that result.error or status indicates
success, and if it failed log the error (using the existing logger) and
return/throw an HTTP error or non-2xx response instead of continuing. Ensure you
reference the same update call and delivery.id, and keep behavior consistent
with other DB error handling in this file (e.g., propagate failure back to the
caller and avoid returning success when the write did not persist).
🪄 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

Run ID: f5b73094-7bee-43ed-b355-c9d2ed909780

📥 Commits

Reviewing files that changed from the base of the PR and between af9a70f and 899cb44.

📒 Files selected for processing (22)
  • messages/en.json
  • src/components/WebhookDeliveryLog.vue
  • src/components/WebhookForm.vue
  • src/pages/settings/organization/Webhooks.vue
  • src/stores/webhooks.ts
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/public/webhooks/get.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/webhook-delivery-redirect.unit.test.ts
  • tests/webhook-delivery-security.unit.test.ts
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-signature.test.ts
  • tests/webhooks.test.ts
✅ Files skipped from review due to trivial changes (2)
  • messages/en.json
  • supabase/functions/_backend/public/webhooks/index.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • tests/webhook-delivery-redirect.unit.test.ts
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • src/components/WebhookDeliveryLog.vue
  • tests/hashed-apikey-rls.test.ts
  • src/components/WebhookForm.vue
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-delivery-security.unit.test.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • supabase/functions/_backend/public/webhooks/get.ts
  • src/pages/settings/organization/Webhooks.vue
  • tests/webhook-signature.test.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • src/stores/webhooks.ts
  • tests/webhooks.test.ts

Comment thread supabase/functions/_backend/public/webhooks/test.ts Outdated
@riderx riderx force-pushed the codex/standard-webhooks-compliance branch from 899cb44 to 59e20a9 Compare May 12, 2026 11:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/functions/_backend/utils/webhook.ts (1)

426-433: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop logging raw webhook URLs.

These log entries still include the full url, which can carry shared secrets in query params or credentials. Please log sanitized metadata instead, otherwise delivery logs become a secret-leak surface.

💡 Suggested shape
+function getWebhookUrlLogMeta(url: string) {
+  try {
+    const parsed = new URL(url)
+    return {
+      protocol: parsed.protocol,
+      hostname: parsed.hostname,
+      pathSegmentCount: parsed.pathname.split('/').filter(Boolean).length,
+      hasQuery: parsed.search.length > 0,
+      hasCredentials: Boolean(parsed.username || parsed.password),
+    }
+  }
+  catch {
+    return { invalidUrl: true }
+  }
+}
+
   if (urlValidationError) {
     const duration = Date.now() - startTime
     cloudlogErr({
       requestId: c.get('requestId'),
       message: 'Webhook delivery blocked by URL validation',
       deliveryId,
-      url,
+      webhookTarget: getWebhookUrlLogMeta(url),
       error: urlValidationError,
       duration,
     })
@@
       cloudlog({
         requestId: c.get('requestId'),
         message: 'Webhook delivery attempt',
         deliveryId,
-        url,
+        webhookTarget: getWebhookUrlLogMeta(url),
         status: response.status,
         success: response.ok,
         duration,
       })
@@
     cloudlogErr({
       requestId: c.get('requestId'),
       message: 'Webhook delivery failed',
       deliveryId,
-      url,
+      webhookTarget: getWebhookUrlLogMeta(url),
       error: errorMessage,
       duration,
     })

Also worth adding a unit test that verifies logs never contain a secret-bearing URL like ?token=secret.

Also applies to: 483-491, 509-516

🤖 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 `@supabase/functions/_backend/utils/webhook.ts` around lines 426 - 433, The
cloudlogErr calls in webhook.ts are logging raw webhook URLs (e.g., the call
that passes url to cloudlogErr with message "Webhook delivery blocked by URL
validation"), which can leak secrets; update those logging sites (the
cloudlogErr invocations around the deliveryId/url/error/duration blocks and the
similar blocks at the other noted ranges) to log a sanitizedUrl or metadata
instead (e.g., host, pathname, and a redacted query string or a boolean
indicating presence of query params) rather than the full url, and add a unit
test that constructs a webhook URL containing a secret query param (like
?token=secret) and asserts that the captured logs do not contain the secret or
the full URL.
🤖 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 `@supabase/migrations/20260512080234_standard_webhook_secrets.sql`:
- Around line 37-38: Add a backfill UPDATE after the ALTER TABLE to bring
in-flight deliveries to the new policy: run an UPDATE on webhook_deliveries (the
same migration file) targeting rows where state = 'pending' and max_attempts is
NULL or less than 10, setting max_attempts = 10 (or use greatest(max_attempts,
10) if you prefer to preserve higher values); reference the table
webhook_deliveries and column max_attempts and ensure this UPDATE runs in the
same migration after ALTER COLUMN so existing pending deliveries adopt the new
default.

---

Outside diff comments:
In `@supabase/functions/_backend/utils/webhook.ts`:
- Around line 426-433: The cloudlogErr calls in webhook.ts are logging raw
webhook URLs (e.g., the call that passes url to cloudlogErr with message
"Webhook delivery blocked by URL validation"), which can leak secrets; update
those logging sites (the cloudlogErr invocations around the
deliveryId/url/error/duration blocks and the similar blocks at the other noted
ranges) to log a sanitizedUrl or metadata instead (e.g., host, pathname, and a
redacted query string or a boolean indicating presence of query params) rather
than the full url, and add a unit test that constructs a webhook URL containing
a secret query param (like ?token=secret) and asserts that the captured logs do
not contain the secret or the full URL.
🪄 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

Run ID: 1deceb20-0a88-40c6-b703-db342ff42642

📥 Commits

Reviewing files that changed from the base of the PR and between 899cb44 and bbbe337.

📒 Files selected for processing (26)
  • messages/en.json
  • src/auto-imports.d.ts
  • src/components/WebhookDeliveryLog.vue
  • src/components/WebhookForm.vue
  • src/pages/settings/organization/Webhooks.vue
  • src/stores/webhooks.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/public/webhooks/delete.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/public/webhooks/get.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • supabase/functions/_backend/public/webhooks/put.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/public/webhooks/test.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/triggers/webhook_dispatcher.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/migrations/20260512080234_standard_webhook_secrets.sql
  • tests/hashed-apikey-rls.test.ts
  • tests/webhook-delivery-redirect.unit.test.ts
  • tests/webhook-delivery-security.unit.test.ts
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-signature.test.ts
  • tests/webhooks.test.ts
✅ Files skipped from review due to trivial changes (3)
  • src/components/WebhookDeliveryLog.vue
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • tests/webhook-delivery-redirect.unit.test.ts
  • supabase/functions/_backend/public/webhooks/response.ts
  • supabase/functions/_backend/public/webhooks/deliveries.ts
  • supabase/functions/_backend/triggers/webhook_delivery.ts
  • supabase/functions/_backend/public/webhooks/index.ts
  • supabase/functions/_backend/public/webhooks/get.ts
  • tests/webhook-queue-processing.test.ts
  • tests/webhook-signature.test.ts
  • supabase/functions/_backend/public/webhooks/post.ts
  • tests/webhook-delivery-security.unit.test.ts
  • src/stores/webhooks.ts

Comment thread supabase/migrations/20260512080234_standard_webhook_secrets.sql
@nagiexplorer88
Copy link
Copy Markdown

retryDelivery() still allows retrying any non-success delivery, even though the comment says retries are only for failed deliveries. Because pending deliveries pass this check, an API caller can reset a delivery that is already queued/in-flight and enqueue it again, which can produce duplicate webhook sends and wipe the current attempt metadata.

I think this should require delivery.status === 'failed' before the reset/queue step, or explicitly reject pending/other in-progress states. A regression test for retrying a pending delivery would cover this path.

@nagiexplorer88
Copy link
Copy Markdown

One more independent pagination issue: GET /webhooks accepts page through getBodyOrQuery(), so query params arrive as strings. The list schema in get.ts currently declares page?: "number", which means a real request like /webhooks?orgId=...&page=1 will fail validation with invalid_body before it can use the pagination logic.

GET /webhooks/deliveries already handles this with page?: "number | string.numeric.parse", so I think the webhook list endpoint should use the same schema shape and add a small GET test with page=0 or page=1.

@subhajitlucky
Copy link
Copy Markdown

The new urlInfo logging fixes the delivery-attempt path, but the API validation paths still attach the raw webhook URL to error metadata. In current head, post.ts and put.ts throw simpleError("invalid_url", ..., { url: body.url }), and test.ts / deliveries.ts do the same with { url: webhook.url } after getWebhookPublicUrlValidationError(). A webhook URL commonly carries receiver-side bearer material in query params, so a failed create/update/test/retry can still put that full URL into the error response/logging path this PR is trying to harden. Could these error payloads use the same kind of sanitized URL metadata (valid, protocol, hostnameLength, hasQuery, hasCredentials) instead of the raw URL, and add a regression test for an invalid webhook URL containing ?token=...?

@sonarqubecloud
Copy link
Copy Markdown

@digzrow-coder
Copy link
Copy Markdown

There is still one raw webhook URL log before the new sanitization layer runs. webhook_delivery.ts logs the queue message with url: deliveryData.url immediately after unwrapping the payload, so any receiver URL containing a query token is still emitted to worker logs for every queued delivery. The new tests cover the logs inside deliverWebhook(), but they do not cover this earlier handler log, so the leak remains even though later success/failure/validation logs use urlInfo.

This should probably use getWebhookLogUrlMetadata(deliveryData.url) there too, and the invalid-delivery-data path should avoid dumping the full deliveryData object if it can include the URL/payload. A regression can call the delivery handler with https://example.com/hook?token=secret-token and assert no cloudlog/cloudlogErr entry contains the token.

@nagiexplorer88
Copy link
Copy Markdown

There is one more raw webhook URL egress path outside the delivery logs. After max retries, webhook_delivery.ts calls sendNotifOrg() with eventData.webhook_url = webhook.url; sendNotifOrg() forwards that object to trackBentoEvent() as Bento event details, and if the send fails it also logs eventData in the trackEvent failed path.

Webhook receiver URLs can carry bearer tokens or signed query params, so this still exports the full secret-bearing URL to the notification provider (and to logs on Bento failure), even though the delivery/logging paths now use sanitized urlInfo. I would pass sanitized URL metadata or a non-secret webhook identifier/name in the notification payload instead, and add a regression with https://example.com/hook?token=secret-token asserting neither the Bento payload nor the failure log contains the token.

Copy link
Copy Markdown

@KCDaemon KCDaemon left a comment

Choose a reason for hiding this comment

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

Rechecked the latest head (3f185ac). The URL redaction work in deliverWebhook() is good, but the delivery trigger still leaks the full customer webhook URL in two remaining paths.

In supabase/functions/_backend/triggers/webhook_delivery.ts, the initial cloudlog() includes url: deliveryData.url before validation/delivery. Later, after max retries, the sendNotifOrg() payload includes webhook_url: webhook.url. Both can contain customer secrets in query strings, path tokens, or embedded credentials. This PR already added getWebhookLogUrlMetadata() for safer logging elsewhere, so these remaining full-URL paths are inconsistent with the new privacy boundary.

I would keep this blocked until the trigger uses redacted/metadata-only URL information for logs and notification variables, or otherwise guarantees those sinks are safe for full customer URLs. The current GitHub status also has a failing Run tests job. git diff --check origin/main...origin/pr-2244 passes locally.

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.

5 participants