Skip to content

feat(api): outbound workspace webhooks with signed delivery and logs - #306

Merged
martian56 merged 2 commits into
Devlaner:mainfrom
cavidelizade:feat/outbound-webhooks
Jul 13, 2026
Merged

feat(api): outbound workspace webhooks with signed delivery and logs#306
martian56 merged 2 commits into
Devlaner:mainfrom
cavidelizade:feat/outbound-webhooks

Conversation

@cavidelizade

@cavidelizade cavidelizade commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Feature summary

Workspace admins can now register outbound webhooks that fire signed HTTP POST payloads to their own endpoints when things happen in the workspace, and inspect each delivery attempt without leaving Settings.

Linked issues / discussion

Closes #195

User-facing behavior

Under Settings → Webhooks, an admin adds an endpoint URL and picks which events it subscribes to (issues, projects, modules, cycles, issue comments). On save, a signing secret is shown once. From then on, each subscribed event POSTs a JSON payload to the endpoint with an X-Devlane-Signature: sha256=<hmac> header (and X-Devlane-Event) that the receiver verifies with the secret. Admins can pause/resume, delete, and open a delivery-log viewer showing recent attempts with their response status and retry count. Issue create/update/delete are wired to dispatch today.

What changed

API (apps/api/)

  • New store/service/handler for webhooks. Routes (workspace admin only):
    • GET /api/workspaces/:slug/webhooks/
    • POST /api/workspaces/:slug/webhooks/
    • PATCH /api/workspaces/:slug/webhooks/:webhookId/
    • DELETE /api/workspaces/:slug/webhooks/:webhookId/
    • GET /api/workspaces/:slug/webhooks/:webhookId/logs/
  • Delivery runs on the existing RabbitMQ QueueWebhooks consumer: sign body with HMAC-SHA256, POST with retries + backoff, persist a webhook_logs row (request/response, retry count).
  • Dial-time SSRF guard: resolves the target host and refuses non-public IPs (loopback/private/link-local/unspecified/multicast), dials the validated IP so DNS rebinding can't slip through, and disables redirects. Literal private/loopback IPs are also rejected at create time so admins get a clear 400 instead of a webhook that silently fails every delivery.
  • IssueService dispatches created/updated/deleted to matching active webhooks (drafts skipped).

UI (apps/web/)

  • WebhooksSettings component mounted in SettingsPage under the existing ?section=webhooks tab (replaces the old placeholder), plus webhookService.ts and response types.

Database

  • No schema changes. The webhooks and webhook_logs tables already exist in the init schema; the old model incorrectly mapped a non-existent project_id column, which this PR corrects.

Why this design

Reused the existing RabbitMQ publish/consume path rather than delivering inline so a slow or failing endpoint never blocks the user action that triggered it, and so retries are natural. The real SSRF enforcement lives at connect time (the only place that defeats DNS rebinding); the create-time literal-IP check is just fast, clear feedback. Events are stored as boolean columns per the existing table shape, so subscription filtering is a plain indexed query.

Test plan

  • npm run validate green (typecheck + lint + prettier + go vet + go test)
  • Manual end-to-end walkthrough:
    1. Settings → Webhooks → Add webhook (URL + events), secret shown once
    2. Created an issue → delivery enqueued, HMAC-signed POST attempted with retries, webhook_logs row written and visible in the log viewer
    3. Create-time rejection verified for 127.0.0.1, 10.0.0.5, 169.254.169.254, [::1]; valid public URL accepted
  • Verified in the browser (Playwright) end to end
  • Tested at narrow viewport

AI assistance

  • AI tools were used — tool(s): Claude Code (Claude Opus 4.8) — and AI-assisted commits include a Co-Authored-By: trailer

Checklist

  • PR title follows Conventional Commits and is ≤ 100 chars
  • Trailing slashes on new routes match neighboring routes
  • New env vars documented in internal/config/config.go (none added)
  • Acceptance criteria from the linked issue are all met

Summary by CodeRabbit

  • New Features
    • Workspace admins can manage outbound workspace webhooks (create, update, pause/resume, delete) and choose subscribed event types.
    • Issue lifecycle events can trigger best-effort webhook delivery to active endpoints.
    • Webhook delivery logs are available, showing event metadata, request/response details, retries, and timestamps.
  • Security
    • Webhook requests are signed per webhook.
    • Webhook delivery is protected against SSRF by validating URLs and blocking private/local targets.
    • Access is restricted to workspace administrators for all webhook operations and log viewing.

Add workspace-scoped outbound webhooks. Admins register HTTPS endpoints,
choose which events they subscribe to (issues, projects, modules, cycles,
issue comments), and inspect recent delivery attempts. Issue create/update/
delete now dispatch to matching active webhooks.

Delivery runs through the existing RabbitMQ queue: each request is signed
with HMAC-SHA256 (X-Devlane-Signature) using a per-webhook secret shown once
at creation, retried with backoff, and recorded in webhook_logs. A dial-time
SSRF guard resolves the target host and refuses non-public addresses (also
covering DNS rebinding); literal private/loopback IPs are rejected at create
time so admins get a clear error instead of a webhook that silently fails.

Adds the management UI under Settings -> Webhooks (list, create, pause/
resume, delete, delivery-log viewer).

Closes Devlaner#195

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds workspace webhook CRUD, event-based issue dispatch, signed and SSRF-protected delivery with logs, authenticated API routes, and a settings UI for webhook management.

Changes

Workspace webhook lifecycle

Layer / File(s) Summary
Webhook contracts and persistence
apps/api/internal/model/webhook.go, apps/api/internal/store/webhook.go, apps/api/internal/service/webhook.go
Defines webhook and delivery-log models, workspace-scoped storage, admin authorization, lifecycle operations, event subscriptions, and dispatch enqueueing.
Event dispatch and signed delivery
apps/api/internal/service/issue.go, apps/api/internal/service/webhook_delivery.go, apps/api/internal/queue/*, apps/api/cmd/api/main.go
Dispatches non-draft issue events, processes webhook queue payloads, sends signed HTTP requests with retries and SSRF protection, and records delivery logs.
HTTP API and routing
apps/api/internal/handler/webhook.go, apps/api/internal/router/router.go, apps/api/internal/handler/webhook_test.go
Adds authenticated webhook and log endpoints, service error mapping, router wiring, and CRUD, URL-validation, and authorization tests.
Settings management UI
apps/web/src/api/types.ts, apps/web/src/services/webhookService.ts, apps/web/src/components/settings/WebhooksSettings.tsx, apps/web/src/pages/SettingsPage.tsx
Adds typed client calls and UI flows for creating, toggling, deleting, and viewing webhook delivery logs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IssueService
  participant WebhookService
  participant WebhookStore
  participant RabbitMQ
  participant NewWebhookDeliverer
  participant WebhookEndpoint
  IssueService->>WebhookService: Dispatch issue event
  WebhookService->>WebhookStore: List active hooks for event
  WebhookService->>RabbitMQ: Publish WebhookPayload
  RabbitMQ->>NewWebhookDeliverer: Handle webhook payload
  NewWebhookDeliverer->>WebhookEndpoint: POST signed JSON payload
  NewWebhookDeliverer->>WebhookStore: Create delivery log
Loading

Suggested labels: enhancement, API, UI, Vulnerability

Poem

I’m a rabbit with hooks in a row,
Sending signed messages where they should go.
Logs hop back with each delivery,
Admins click buttons, light and merry.
SSRF shadows stay out of sight—
Webhooks now travel safely tonight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% 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 is concise, conventional, and accurately summarizes the main change: outbound workspace webhooks with signed delivery and logs.
Description check ✅ Passed The description covers the feature, linked issue, behavior, implementation, tests, and AI disclosure; a few template sections are omitted but not critical.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
apps/api/internal/handler/webhook_test.go (1)

28-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add coverage that secret_key is not re-exposed by List/Update.

Once the List/Update secret-leak (see handler/webhook.go) is fixed, add assertions here that list[0]["secret_key"] and updated["secret_key"] are empty/absent, so a regression is caught by CI.

🤖 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 `@apps/api/internal/handler/webhook_test.go` around lines 28 - 40, Add
regression assertions in the webhook test after unmarshalling the List and
Update responses to verify list[0]["secret_key"] and updated["secret_key"] are
empty or absent. Keep the existing status, length, and is_active assertions
unchanged.
apps/web/src/components/settings/WebhooksSettings.tsx (1)

58-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate webhook-list fetch logic between load and the mount effect.

load (58-72) and the mount useEffect (74-99) implement the same fetch + 403-aware error handling twice — the effect never calls load(). Consider having the effect just call load() (with a cancellation guard inside load itself), so the two code paths can't drift apart.

♻️ Suggested consolidation
   useEffect(() => {
     let cancelled = false;
-    setLoading(true);
-    setLoadError(null);
-    webhookService
-      .list(workspaceSlug)
-      .then((res) => {
-        if (!cancelled) setWebhooks(res);
-      })
-      .catch((err) => {
-        if (cancelled) return;
-        const status = (err as { response?: { status?: number } })?.response?.status;
-        setWebhooks([]);
-        setLoadError(
-          status === 403
-            ? 'Only workspace admins can manage webhooks.'
-            : 'Could not load webhooks.',
-        );
-      })
-      .finally(() => {
-        if (!cancelled) setLoading(false);
-      });
+    load();
     return () => {
       cancelled = true;
     };
   }, [workspaceSlug, load]);

(Requires load to itself no-op on a stale request, e.g. via a ref, if strict double-invoke ordering matters.)

🤖 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 `@apps/web/src/components/settings/WebhooksSettings.tsx` around lines 58 - 99,
Consolidate the duplicate webhook-fetch logic by having the mount useEffect
invoke the existing load callback instead of reimplementing webhookService.list,
error handling, and loading state updates. Update load to guard against stale or
cancelled requests as needed, using the existing workspaceSlug dependency, so
strict-mode or unmount ordering cannot apply outdated state updates.
apps/api/internal/service/webhook.go (1)

182-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dispatch silently discards queue publish errors.

The _ = s.queue.PublishWebhook(...) pattern provides no observability when dispatch fails. If the queue is temporarily down, webhooks are silently dropped with no log or metric to indicate the failure. Adding a logger to WebhookService and logging publish errors would make delivery failures debuggable.

♻️ Suggested refactor: add logger to WebhookService
 type WebhookService struct {
 	webhooks *store.WebhookStore
 	ws       *store.WorkspaceStore
 	queue    *queue.Publisher // optional; when nil, dispatch is a no-op
+	log      *slog.Logger     // optional; when nil, errors are silently dropped
 }

 func NewWebhookService(webhooks *store.WebhookStore, ws *store.WorkspaceStore, q *queue.Publisher) *WebhookService {
-	return &WebhookService{webhooks: webhooks, ws: ws, queue: q}
+	return &WebhookService{webhooks: webhooks, ws: ws, queue: q, log: slog.Default()}
 }

+func (s *WebhookService) SetLogger(l *slog.Logger) { s.log = l }
+
 func (s *WebhookService) Dispatch(ctx context.Context, workspaceID uuid.UUID, event string, payload map[string]interface{}) {
 	if s == nil || s.queue == nil || !store.IsValidWebhookEvent(event) {
 		return
 	}
 	hooks, err := s.webhooks.ListActiveByWorkspaceAndEvent(ctx, workspaceID, event)
 	if err != nil {
+		if s.log != nil {
+			s.log.Warn("webhook dispatch: list active webhooks failed", "workspace_id", workspaceID, "event", event, "error", err)
+		}
 		return
 	}
 	for i := range hooks {
-		_ = s.queue.PublishWebhook(ctx, queue.WebhookPayload{
+		if err := s.queue.PublishWebhook(ctx, queue.WebhookPayload{
 			WebhookID:   hooks[i].ID.String(),
 			WorkspaceID: workspaceID.String(),
 			URL:         hooks[i].URL,
 			Secret:      hooks[i].SecretKey,
 			Event:       event,
 			Payload:     payload,
-		})
+		}); err != nil && s.log != nil {
+			s.log.Warn("webhook dispatch: publish failed", "webhook_id", hooks[i].ID, "event", event, "error", 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 `@apps/api/internal/service/webhook.go` around lines 182 - 200, Update
WebhookService to include its logger dependency, then modify Dispatch to capture
each PublishWebhook error and log it with relevant webhook and workspace context
instead of discarding it. Preserve the existing dispatch loop and payload
behavior.
apps/api/internal/service/webhook_delivery_internal_test.go (1)

10-22: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Add a CGNAT test case to TestIsPublicIP.

The test covers RFC 1918, loopback, link-local, and unspecified ranges but not 100.64.0.0/10 (RFC 6598 CGNAT). Adding it would prevent regressions if the isPublicIP guard is later extended.

✨ Suggested addition
 	blocked := []string{"127.0.0.1", "::1", "10.0.0.5", "192.168.1.1", "172.16.0.1", "169.254.169.254", "0.0.0.0",
+		"100.64.0.1",
 	}
🤖 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 `@apps/api/internal/service/webhook_delivery_internal_test.go` around lines 10
- 22, Add an address from the RFC 6598 100.64.0.0/10 CGNAT range to the blocked
cases in TestIsPublicIP, preserving the existing assertions and public-IP cases.
apps/api/internal/service/webhook_delivery.go (2)

108-111: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log all sent request headers for audit completeness.

RequestHeaders records Content-Type, X-Devlane-Event, and X-Devlane-Signature, but omits X-Devlane-Delivery and User-Agent which are actually sent in deliverOnce. This creates an audit gap when debugging delivery issues from logs.

🤖 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 `@apps/api/internal/service/webhook_delivery.go` around lines 108 - 111, Update
the request-header logging construction in deliverOnce to include every header
actually sent, especially X-Devlane-Delivery and User-Agent, alongside the
existing Content-Type, X-Devlane-Event, and optional X-Devlane-Signature
entries. Keep the logged values synchronized with the headers used by the
outgoing request.

62-71: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use context-aware backoff instead of time.Sleep.

time.Sleep blocks the consumer goroutine without checking ctx.Done(). During shutdown, this delays graceful termination by up to 1.5 seconds per in-flight delivery. Replace with a select that also listens on ctx.Done().

♻️ Proposed fix
 		if attempts < webhookMaxAttempts {
-			time.Sleep(time.Duration(attempts) * 500 * time.Millisecond)
+			select {
+			case <-time.After(time.Duration(attempts) * 500 * time.Millisecond):
+			case <-ctx.Done():
+				return ctx.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 `@apps/api/internal/service/webhook_delivery.go` around lines 62 - 71, Replace
the time.Sleep backoff in the webhook delivery retry loop with a context-aware
select that waits for the existing attempt-based duration or returns promptly
when ctx.Done() is signaled. Preserve the retry limit and successful-delivery
behavior around deliverOnce.
🤖 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 `@apps/api/internal/handler/webhook.go`:
- Around line 41-53: Redact secret_key from webhook responses returned by the
List and Update handler paths. Introduce or reuse a response DTO/mapper that
preserves the other webhook fields while omitting secret_key, and apply it
before the c.JSON calls in WebhookHandler.List and the corresponding Update
method; do not alter persistence or creation behavior.

In `@apps/api/internal/model/webhook.go`:
- Around line 10-29: Update the SecretKey field in Webhook to use json:"-" so it
is never serialized through the model in list, create, or update responses. Add
or reuse a dedicated create-response type that explicitly includes the secret
only for the create flow, and adjust the create handler to return that type
while preserving the existing secret value there.

In `@apps/api/internal/service/webhook_delivery.go`:
- Around line 164-170: Update isPublicIP to explicitly reject RFC 6598 CGNAT
addresses in the 100.64.0.0/10 range, alongside the existing non-public IP
checks, while preserving the current behavior for all other addresses.
- Around line 78-86: Generate the delivery UUID once before the retry loop, then
pass that value through each retry invocation into deliverOnce. Update
deliverOnce to accept and use the provided UUID for X-Devlane-Delivery, removing
the per-attempt uuid.New().String() call so all retries share the same
identifier.

---

Nitpick comments:
In `@apps/api/internal/handler/webhook_test.go`:
- Around line 28-40: Add regression assertions in the webhook test after
unmarshalling the List and Update responses to verify list[0]["secret_key"] and
updated["secret_key"] are empty or absent. Keep the existing status, length, and
is_active assertions unchanged.

In `@apps/api/internal/service/webhook_delivery_internal_test.go`:
- Around line 10-22: Add an address from the RFC 6598 100.64.0.0/10 CGNAT range
to the blocked cases in TestIsPublicIP, preserving the existing assertions and
public-IP cases.

In `@apps/api/internal/service/webhook_delivery.go`:
- Around line 108-111: Update the request-header logging construction in
deliverOnce to include every header actually sent, especially X-Devlane-Delivery
and User-Agent, alongside the existing Content-Type, X-Devlane-Event, and
optional X-Devlane-Signature entries. Keep the logged values synchronized with
the headers used by the outgoing request.
- Around line 62-71: Replace the time.Sleep backoff in the webhook delivery
retry loop with a context-aware select that waits for the existing attempt-based
duration or returns promptly when ctx.Done() is signaled. Preserve the retry
limit and successful-delivery behavior around deliverOnce.

In `@apps/api/internal/service/webhook.go`:
- Around line 182-200: Update WebhookService to include its logger dependency,
then modify Dispatch to capture each PublishWebhook error and log it with
relevant webhook and workspace context instead of discarding it. Preserve the
existing dispatch loop and payload behavior.

In `@apps/web/src/components/settings/WebhooksSettings.tsx`:
- Around line 58-99: Consolidate the duplicate webhook-fetch logic by having the
mount useEffect invoke the existing load callback instead of reimplementing
webhookService.list, error handling, and loading state updates. Update load to
guard against stale or cancelled requests as needed, using the existing
workspaceSlug dependency, so strict-mode or unmount ordering cannot apply
outdated state updates.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 34aafa82-3c49-44e0-a5e3-8d08bf688979

📥 Commits

Reviewing files that changed from the base of the PR and between 247d342 and da8bca1.

📒 Files selected for processing (16)
  • apps/api/cmd/api/main.go
  • apps/api/internal/handler/webhook.go
  • apps/api/internal/handler/webhook_test.go
  • apps/api/internal/model/webhook.go
  • apps/api/internal/queue/consumer.go
  • apps/api/internal/queue/queue.go
  • apps/api/internal/router/router.go
  • apps/api/internal/service/issue.go
  • apps/api/internal/service/webhook.go
  • apps/api/internal/service/webhook_delivery.go
  • apps/api/internal/service/webhook_delivery_internal_test.go
  • apps/api/internal/store/webhook.go
  • apps/web/src/api/types.ts
  • apps/web/src/components/settings/WebhooksSettings.tsx
  • apps/web/src/pages/SettingsPage.tsx
  • apps/web/src/services/webhookService.ts

Comment thread apps/api/internal/handler/webhook.go
Comment thread apps/api/internal/model/webhook.go
Comment thread apps/api/internal/service/webhook_delivery.go Outdated
Comment thread apps/api/internal/service/webhook_delivery.go
…GNAT

Address review findings on the webhooks feature:

- The signing secret was serialized on every webhook response (secret_key
  json tag), so admins could read it on any list/update fetch. Mark it
  json:"-" and return it exactly once, from the create handler.
- X-Devlane-Delivery was regenerated per retry attempt, so retries looked
  like distinct events to receivers doing idempotency dedup. Generate one
  delivery id per delivery, reuse it across attempts, and record it in the
  delivery log.
- isPublicIP now also rejects CGNAT shared address space (RFC 6598,
  100.64.0.0/10), which ip.IsPrivate() does not cover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/api/internal/handler/webhook_test.go (1)

28-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Also verify that PATCH responses omit secret_key.

The test covers secret omission from the list response, but the subsequent PATCH response is only checked for is_active. Since the contract is “returned only once at creation,” assert that updated also lacks secret_key; otherwise an update-path secret leak could go undetected.

Proposed test assertion
 require.Equal(t, false, updated["is_active"])
+require.NotContains(t, updated, "secret_key", "the secret must not leak on update")
🤖 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 `@apps/api/internal/handler/webhook_test.go` around lines 28 - 35, Extend the
PATCH response assertions in the webhook test to verify that the updated
response, referenced as updated, does not contain secret_key. Keep the existing
is_active assertion and creation/list secret-omission coverage 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.

Nitpick comments:
In `@apps/api/internal/handler/webhook_test.go`:
- Around line 28-35: Extend the PATCH response assertions in the webhook test to
verify that the updated response, referenced as updated, does not contain
secret_key. Keep the existing is_active assertion and creation/list
secret-omission coverage unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 83d06c96-ba89-499b-894b-d6dff248fb41

📥 Commits

Reviewing files that changed from the base of the PR and between da8bca1 and c6e813f.

📒 Files selected for processing (5)
  • apps/api/internal/handler/webhook.go
  • apps/api/internal/handler/webhook_test.go
  • apps/api/internal/model/webhook.go
  • apps/api/internal/service/webhook_delivery.go
  • apps/api/internal/service/webhook_delivery_internal_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/api/internal/service/webhook_delivery_internal_test.go
  • apps/api/internal/model/webhook.go
  • apps/api/internal/service/webhook_delivery.go
  • apps/api/internal/handler/webhook.go

@cavidelizade

Copy link
Copy Markdown
Contributor Author

@martian56 this one is green now, CI passing and all CodeRabbit comments addressed (secret no longer returned after creation, stable delivery id across retries, and CGNAT blocked in the SSRF guard). Good to merge whenever you're happy with it.

@martian56
martian56 merged commit dc28bb0 into Devlaner:main Jul 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Implement outbound workspace webhooks (CRUD, signed delivery, logs)

2 participants