Skip to content

feat(control-plane): proxy webhook subscription create/delete to relayfile-cloud#345

Open
khaliqgant wants to merge 2 commits into
mainfrom
inbound-integration-bridge
Open

feat(control-plane): proxy webhook subscription create/delete to relayfile-cloud#345
khaliqgant wants to merge 2 commits into
mainfrom
inbound-integration-bridge

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 1, 2026

Copy link
Copy Markdown
Member

Part of the inbound integration bridge (provider → relay agent, server-side, no client process). Companion PRs: relaycast (ingress), relaycast-cloud (secret+expose), relay (CLI subscribe), relayfile (control-plane proxy), relayfile-cloud (snapshot enrichment). Cross-repo — review/merge together.

Local control-plane proxies webhook_subscription create/delete to relayfile-cloud; TS client + generated OpenAPI types updated.

Tests: @relayfile/client (10 passing) + typecheck + go test ./cmd/relayfile-cli -run TestControlPlaneCloudIntegrationConformance.

Review status

3-way fresh-eyes review completed. Fixed: fail-closed HMAC secret normalization, 5xx-on-transient (no silent loss), require-dedupe-key (no double-inject). Verified correct: exactly-once node delivery, snapshot bounds, signing order.

⚠️ Draft — before merge

  • Fix C: unsubscribe must delete the relayfile-cloud webhook_subscription (persist its id on the binding) — currently unsubscribe does not stop inbound delivery (cross-repo: relay CLI + relayfile daemon)
  • Add tests: golden-fixture HMAC round-trip, enrichment bounds/lazy hydration, CLI rollback/unsubscribe assertions
  • Add cast.agentrelay.com to RELAYFILE_WEBHOOK_HOST_ALLOWLIST for the preview/prod host
  • E2E on a preview stage: real Slack/GitHub/Linear message → relay channel, no client process
    🤖 Generated with Claude Code

Review in cubic

…yfile-cloud

Local control-plane now proxies webhook_subscription create/delete to relayfile-cloud;
TS client + generated OpenAPI types updated for the inbound integration bridge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@khaliqgant, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 051f0ed0-39b5-4e7f-a5ac-2780250beebf

📥 Commits

Reviewing files that changed from the base of the PR and between 9666354 and d422ef6.

⛔ Files ignored due to path filters (1)
  • packages/client/src/generated/control-plane.ts is excluded by !**/generated/**
📒 Files selected for processing (6)
  • cmd/relayfile-cli/control_plane.go
  • cmd/relayfile-cli/control_plane_test.go
  • openapi/relayfile-control-plane-v1.openapi.yaml
  • packages/client/src/client.test.ts
  • packages/client/src/client.ts
  • packages/client/src/index.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch inbound-integration-bridge

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds support for managing server-side webhook subscriptions by introducing new endpoints to the control plane daemon, updating the OpenAPI specification, and extending the TypeScript client. The review feedback suggests propagating the request context (r.Context()) instead of using context.Background() in the Go handler to support request cancellation. Additionally, the reviewer points out a type mismatch in the TypeScript client where deleteWebhookSubscription is typed to return Promise<void> instead of Promise<{ ok: boolean }>, and provides suggestions to align the client implementation and its unit tests with the actual daemon response.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +494 to +496
if err := commandClient.client.postJSON(
context.Background(),
fmt.Sprintf("/v1/workspaces/%s/webhooks", url.PathEscape(workspaceID)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Use r.Context() instead of context.Background() to ensure that the request context is propagated. This allows the outgoing HTTP request to be cancelled if the client disconnects or times out.

Suggested change
if err := commandClient.client.postJSON(
context.Background(),
fmt.Sprintf("/v1/workspaces/%s/webhooks", url.PathEscape(workspaceID)),
if err := commandClient.client.postJSON(
r.Context(),
fmt.Sprintf("/v1/workspaces/%s/webhooks", url.PathEscape(workspaceID)),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in d422ef6 — webhook subscription creation now passes r.Context() through to the upstream relayfile-cloud request.

Comment on lines +529 to +531
if err := commandClient.client.deleteJSON(
context.Background(),
fmt.Sprintf("/v1/workspaces/%s/webhooks/%s", url.PathEscape(workspaceID), url.PathEscape(subscriptionID)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Use r.Context() instead of context.Background() to propagate the request context and support cancellation.

Suggested change
if err := commandClient.client.deleteJSON(
context.Background(),
fmt.Sprintf("/v1/workspaces/%s/webhooks/%s", url.PathEscape(workspaceID), url.PathEscape(subscriptionID)),
if err := commandClient.client.deleteJSON(
r.Context(),
fmt.Sprintf("/v1/workspaces/%s/webhooks/%s", url.PathEscape(workspaceID), url.PathEscape(subscriptionID)),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in d422ef6 — webhook subscription deletion now passes r.Context() through to the upstream relayfile-cloud request.

Comment thread packages/client/src/client.ts Outdated
Comment on lines +406 to +412
deleteWebhookSubscription(subscriptionId: string, workspace?: string): Promise<void> {
return this.request<void>({
method: 'DELETE',
path: '/v1/integrations/webhook-subscriptions',
body: { subscriptionId, ...(workspace ? { workspace } : {}) },
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The deleteWebhookSubscription method is typed to return Promise<void>, but the control plane server actually returns {"ok": true} (as defined in the OpenAPI spec and implemented in control_plane.go). To align with the OpenAPI schema and the actual daemon response, the return type should be Promise<{ ok: boolean }>.

Suggested change
deleteWebhookSubscription(subscriptionId: string, workspace?: string): Promise<void> {
return this.request<void>({
method: 'DELETE',
path: '/v1/integrations/webhook-subscriptions',
body: { subscriptionId, ...(workspace ? { workspace } : {}) },
});
}
deleteWebhookSubscription(subscriptionId: string, workspace?: string): Promise<{ ok: boolean }> {
return this.request<{ ok: boolean }>({
method: 'DELETE',
path: '/v1/integrations/webhook-subscriptions',
body: { subscriptionId, ...(workspace ? { workspace } : {}) },
});
}

Comment thread packages/client/src/client.test.ts Outdated
Comment on lines +113 to +114
const rawRequest = vi.spyOn(client as unknown as { rawRequest: (opts: unknown) => Promise<unknown> }, 'rawRequest');
rawRequest.mockResolvedValueOnce({ subscriptionId: 'whsub_123' }).mockResolvedValueOnce(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update the mock for rawRequest to resolve with { ok: true } instead of undefined for the delete call, matching the actual daemon's response.

Suggested change
const rawRequest = vi.spyOn(client as unknown as { rawRequest: (opts: unknown) => Promise<unknown> }, 'rawRequest');
rawRequest.mockResolvedValueOnce({ subscriptionId: 'whsub_123' }).mockResolvedValueOnce(undefined);
const rawRequest = vi.spyOn(client as unknown as { rawRequest: (opts: unknown) => Promise<unknown> }, 'rawRequest');
rawRequest.mockResolvedValueOnce({ subscriptionId: 'whsub_123' }).mockResolvedValueOnce({ ok: true });

Comment thread packages/client/src/client.test.ts Outdated
})
).resolves.toEqual({ subscriptionId: 'whsub_123' });

await expect(client.deleteWebhookSubscription('whsub_123', 'demo')).resolves.toBeUndefined();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update the assertion to expect { ok: true } instead of undefined to match the actual response of the daemon.

Suggested change
await expect(client.deleteWebhookSubscription('whsub_123', 'demo')).resolves.toBeUndefined();
await expect(client.deleteWebhookSubscription('whsub_123', 'demo')).resolves.toEqual({ ok: true });

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-07-08T08-03-02-492Z-HEAD-provider
Mode: provider
Git SHA: 5fca1ba

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@khaliqgant khaliqgant marked this pull request as ready for review July 8, 2026 07:33
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines -32 to -35
"publishConfig": {
"access": "public",
"provenance": true
},

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.

🟡 Scoped npm package loses public access configuration, breaking publishing workflow

The publish configuration is removed (publishConfig at packages/client/package.json:32-35) unlike every other scoped package in the monorepo, so npm publish will default to restricted access for this @relayfile/client package and fail in CI.

Impact: Automated or manual npm publishing of this package will fail or produce a private package instead of a public one.

All other monorepo packages retain publishConfig with access: public

Every other package in packages/ (core, file-observer, local-mount, cli, sdk/typescript, mount-*) retains publishConfig: { access: "public", provenance: true }. Scoped packages on npm default to restricted access without this setting. The removal appears accidental — this PR's stated purpose is adding webhook subscription support, not changing publishing behavior.

Prompt for agents
The publishConfig block was removed from packages/client/package.json but all other scoped packages in the monorepo retain it. Restore the publishConfig section with access: public and provenance: true to match the convention used by packages/core/package.json, packages/cli/package.json, packages/sdk/typescript/package.json, etc. This ensures the scoped @relayfile/client package can be published publicly via npm.
Open in Devin Review

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b787e29e80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +398 to +403
createWebhookSubscription(input: WebhookSubscriptionRequestBody): Promise<WebhookSubscriptionResult> {
return this.request<WebhookSubscriptionResult>({
method: 'POST',
path: '/v1/integrations/webhook-subscriptions',
body: input,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate webhook calls on a daemon version that supports them

When this new method is used against any older relayfile daemon that still passes ensureReady() (the client still accepts API v1 and MIN_RELAYFILE_VERSION 0.10.17), the daemon will not have registered /v1/integrations/webhook-subscriptions, so callers get a generic non-JSON 404/DAEMON_UNAVAILABLE instead of the existing actionable version-incompatible error. Please bump the control-plane/minimum daemon version or feature-gate this endpoint before exposing the client method.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in d422ef6 — the control-plane client now negotiates API v2, and the daemon advertises v2 support for the webhook subscription endpoint while still accepting v1 clients.

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmd/relayfile-cli/control_plane.go">

<violation number="1" location="cmd/relayfile-cli/control_plane.go:231">
P1: The unbind handler (`handleControlPlaneUnbind`) only removes the local relay integration binding and does not clean up the corresponding relayfile-cloud webhook subscription. This creates a lifecycle leak: after unbind, the cloud subscription remains active and can continue delivering inbound webhooks even though the local binding no longer exists. You should persist the subscription ID on the binding so that unbind can proxy a DELETE to `/v1/workspaces/{workspaceID}/webhooks/{subscriptionID}` before (or as part of) removing the local binding.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

mux.HandleFunc("/v1/integrations/bindings", withControlPlaneVersion(handleControlPlaneListBindings))
mux.HandleFunc("/v1/integrations/unbind", withControlPlaneVersion(handleControlPlaneUnbind))
mux.HandleFunc("/v1/integrations/writeback-secret", withControlPlaneVersion(handleControlPlaneWritebackSecret))
mux.HandleFunc("/v1/integrations/webhook-subscriptions", withControlPlaneVersion(handleControlPlaneWebhookSubscription))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The unbind handler (handleControlPlaneUnbind) only removes the local relay integration binding and does not clean up the corresponding relayfile-cloud webhook subscription. This creates a lifecycle leak: after unbind, the cloud subscription remains active and can continue delivering inbound webhooks even though the local binding no longer exists. You should persist the subscription ID on the binding so that unbind can proxy a DELETE to /v1/workspaces/{workspaceID}/webhooks/{subscriptionID} before (or as part of) removing the local binding.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/relayfile-cli/control_plane.go, line 231:

<comment>The unbind handler (`handleControlPlaneUnbind`) only removes the local relay integration binding and does not clean up the corresponding relayfile-cloud webhook subscription. This creates a lifecycle leak: after unbind, the cloud subscription remains active and can continue delivering inbound webhooks even though the local binding no longer exists. You should persist the subscription ID on the binding so that unbind can proxy a DELETE to `/v1/workspaces/{workspaceID}/webhooks/{subscriptionID}` before (or as part of) removing the local binding.</comment>

<file context>
@@ -210,6 +228,7 @@ func newControlPlaneHandler() http.Handler {
 	mux.HandleFunc("/v1/integrations/bindings", withControlPlaneVersion(handleControlPlaneListBindings))
 	mux.HandleFunc("/v1/integrations/unbind", withControlPlaneVersion(handleControlPlaneUnbind))
 	mux.HandleFunc("/v1/integrations/writeback-secret", withControlPlaneVersion(handleControlPlaneWritebackSecret))
+	mux.HandleFunc("/v1/integrations/webhook-subscriptions", withControlPlaneVersion(handleControlPlaneWebhookSubscription))
 	return mux
 }
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for flagging this. I am not expanding this PR to make unbind proxy cloud subscription deletion: the local unbind path currently only owns local binding removal and does not carry the authenticated cloud workspace/session context needed to delete relayfile-cloud webhooks safely. The binding already stores subscriptionId for that follow-up lifecycle work, but tying teardown into unbind is a cross-repo behavior change tracked separately. This PR stays scoped to exposing create/delete webhook subscription proxy operations and the typed client API.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The unbind cleanup point is fair in general, but it’s out of scope for this PR. This change only adds the webhook-subscription proxy operations and typed client API; the unbind teardown is a separate cross-repo behavior change.

Comment thread packages/client/src/client.ts Outdated
Comment thread packages/client/src/client.ts
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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