From b787e29e8089161008074a350743e03032647d8a Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 1 Jul 2026 10:30:42 -0700 Subject: [PATCH 1/2] feat(control-plane): proxy webhook subscription create/delete to relayfile-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 --- cmd/relayfile-cli/control_plane.go | 92 +++++++++++++++++++ cmd/relayfile-cli/control_plane_test.go | 50 ++++++++++ .../relayfile-control-plane-v1.openapi.yaml | 73 +++++++++++++++ packages/client/package.json | 4 - packages/client/src/client.test.ts | 36 ++++++++ packages/client/src/client.ts | 20 +++- .../client/src/generated/control-plane.ts | 87 ++++++++++++++++++ 7 files changed, 357 insertions(+), 5 deletions(-) diff --git a/cmd/relayfile-cli/control_plane.go b/cmd/relayfile-cli/control_plane.go index 0c094e71..7b550696 100644 --- a/cmd/relayfile-cli/control_plane.go +++ b/cmd/relayfile-cli/control_plane.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/signal" "path/filepath" @@ -119,6 +120,23 @@ type writebackSecretData struct { Secret string `json:"secret"` } +type webhookSubscriptionRequest struct { + Workspace string `json:"workspace,omitempty"` + URL string `json:"url"` + PathGlobs []string `json:"pathGlobs"` + Secret string `json:"secret"` +} + +type webhookSubscriptionResponse struct { + SubscriptionID string `json:"subscriptionId"` + Secret string `json:"secret,omitempty"` +} + +type deleteWebhookSubscriptionRequest struct { + Workspace string `json:"workspace,omitempty"` + SubscriptionID string `json:"subscriptionId"` +} + func runControlPlane(args []string, stdout io.Writer) error { if len(args) == 0 { return errors.New("control-plane subcommand is required: serve") @@ -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 } @@ -449,6 +468,79 @@ func handleControlPlaneWritebackSecret(w http.ResponseWriter, r *http.Request) { writeControlPlaneJSON(w, http.StatusOK, data) } +func handleControlPlaneWebhookSubscription(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + var req webhookSubscriptionRequest + if err := decodeControlPlaneJSON(r, &req); err != nil { + writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, err.Error()) + return + } + if strings.TrimSpace(req.URL) == "" || len(req.PathGlobs) == 0 || strings.TrimSpace(req.Secret) == "" { + writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, "url, pathGlobs, and secret are required") + return + } + commandClient, err := prepareWorkspaceCommandClient(strings.TrimSpace(req.Workspace), "", "", defaultJoinScopes) + if err != nil { + writeControlPlaneMappedError(w, err) + return + } + workspaceID := strings.TrimSpace(commandClient.workspaceID) + if workspaceID == "" { + writeControlPlaneMappedError(w, errors.New("could not resolve relayfile workspace id")) + return + } + var response webhookSubscriptionResponse + if err := commandClient.client.postJSON( + context.Background(), + fmt.Sprintf("/v1/workspaces/%s/webhooks", url.PathEscape(workspaceID)), + map[string]any{ + "url": strings.TrimSpace(req.URL), + "pathGlobs": req.PathGlobs, + "secret": strings.TrimSpace(req.Secret), + }, + &response, + ); err != nil { + writeControlPlaneMappedError(w, err) + return + } + writeControlPlaneJSON(w, http.StatusOK, response) + case http.MethodDelete: + var req deleteWebhookSubscriptionRequest + if err := decodeControlPlaneJSON(r, &req); err != nil { + writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, err.Error()) + return + } + subscriptionID := strings.TrimSpace(req.SubscriptionID) + if subscriptionID == "" { + writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, "subscriptionId is required") + return + } + commandClient, err := prepareWorkspaceCommandClient(strings.TrimSpace(req.Workspace), "", "", defaultJoinScopes) + if err != nil { + writeControlPlaneMappedError(w, err) + return + } + workspaceID := strings.TrimSpace(commandClient.workspaceID) + if workspaceID == "" { + writeControlPlaneMappedError(w, errors.New("could not resolve relayfile workspace id")) + return + } + if err := commandClient.client.deleteJSON( + context.Background(), + fmt.Sprintf("/v1/workspaces/%s/webhooks/%s", url.PathEscape(workspaceID), url.PathEscape(subscriptionID)), + "", + nil, + ); err != nil { + writeControlPlaneMappedError(w, err) + return + } + writeControlPlaneJSON(w, http.StatusOK, map[string]bool{"ok": true}) + default: + writeControlPlaneError(w, http.StatusMethodNotAllowed, controlPlaneErrInvalidArgument, "method not allowed") + } +} + func controlPlaneHello() helloResponse { return helloResponse{ DaemonVersion: relayfileVersion, diff --git a/cmd/relayfile-cli/control_plane_test.go b/cmd/relayfile-cli/control_plane_test.go index 57615ced..87f706ff 100644 --- a/cmd/relayfile-cli/control_plane_test.go +++ b/cmd/relayfile-cli/control_plane_test.go @@ -187,6 +187,30 @@ func TestControlPlaneCloudIntegrationConformance(t *testing.T) { t.Fatalf("unexpected writeback-secret channel: %q", r.URL.Query().Get("channel")) } _, _ = w.Write([]byte(`{"ok":true,"data":{"url":"https://relay.test/writeback","secret":"sec_123"}}`)) + case "/v1/workspaces/ws_123/webhooks": + if r.Method != http.MethodPost { + t.Fatalf("expected webhooks POST, got %s", r.Method) + } + var body struct { + URL string `json:"url"` + PathGlobs []string `json:"pathGlobs"` + Secret string `json:"secret"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode webhook subscription body failed: %v", err) + } + if body.URL != "https://cast.test/v1/integrations/relayfile/inbound/ws/ch" || + len(body.PathGlobs) != 1 || + body.PathGlobs[0] != "/github/repos/acme/widgets/issues/**" || + body.Secret != "inbound-secret" { + t.Fatalf("unexpected webhook subscription body: %#v", body) + } + _, _ = w.Write([]byte(`{"subscriptionId":"whsub_123"}`)) + case "/v1/workspaces/ws_123/webhooks/whsub_123": + if r.Method != http.MethodDelete { + t.Fatalf("expected webhooks DELETE, got %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) default: t.Fatalf("unexpected path: %s", r.URL.Path) } @@ -240,6 +264,32 @@ func TestControlPlaneCloudIntegrationConformance(t *testing.T) { if secret.URL != "https://relay.test/writeback" || secret.Secret != "sec_123" { t.Fatalf("unexpected writeback secret: %#v", secret) } + + var subscription webhookSubscriptionResponse + status = controlPlaneJSON(t, client, http.MethodPost, baseURL+"/v1/integrations/webhook-subscriptions", webhookSubscriptionRequest{ + Workspace: "demo", + URL: "https://cast.test/v1/integrations/relayfile/inbound/ws/ch", + PathGlobs: []string{"/github/repos/acme/widgets/issues/**"}, + Secret: "inbound-secret", + }, &subscription) + if status != http.StatusOK { + t.Fatalf("webhook subscription status = %d", status) + } + if subscription.SubscriptionID != "whsub_123" { + t.Fatalf("unexpected webhook subscription: %#v", subscription) + } + + var deleted map[string]bool + status = controlPlaneJSON(t, client, http.MethodDelete, baseURL+"/v1/integrations/webhook-subscriptions", deleteWebhookSubscriptionRequest{ + Workspace: "demo", + SubscriptionID: "whsub_123", + }, &deleted) + if status != http.StatusOK { + t.Fatalf("delete webhook subscription status = %d", status) + } + if !deleted["ok"] { + t.Fatalf("unexpected delete response: %#v", deleted) + } } func startControlPlaneTestServer(t *testing.T) (*http.Client, string, func()) { diff --git a/openapi/relayfile-control-plane-v1.openapi.yaml b/openapi/relayfile-control-plane-v1.openapi.yaml index 65f5aac8..4f7859a2 100644 --- a/openapi/relayfile-control-plane-v1.openapi.yaml +++ b/openapi/relayfile-control-plane-v1.openapi.yaml @@ -228,6 +228,47 @@ paths: application/json: schema: $ref: "#/components/schemas/WritebackSecret" + /v1/integrations/webhook-subscriptions: + post: + operationId: createWebhookSubscription + summary: Create a server-side relayfile webhook subscription + parameters: + - $ref: "#/components/parameters/ApiVersionHeader" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookSubscriptionRequest" + responses: + "200": + description: Created webhook subscription + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookSubscriptionResponse" + delete: + operationId: deleteWebhookSubscription + summary: Delete a server-side relayfile webhook subscription + parameters: + - $ref: "#/components/parameters/ApiVersionHeader" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DeleteWebhookSubscriptionRequest" + responses: + "200": + description: Subscription deleted + content: + application/json: + schema: + type: object + required: [ok] + properties: + ok: + type: boolean components: parameters: ApiVersionHeader: @@ -485,6 +526,38 @@ components: type: string secret: type: string + WebhookSubscriptionRequest: + type: object + required: [url, pathGlobs, secret] + properties: + workspace: + type: string + url: + type: string + format: uri + pathGlobs: + type: array + minItems: 1 + items: + type: string + secret: + type: string + WebhookSubscriptionResponse: + type: object + required: [subscriptionId] + properties: + subscriptionId: + type: string + secret: + type: string + DeleteWebhookSubscriptionRequest: + type: object + required: [subscriptionId] + properties: + workspace: + type: string + subscriptionId: + type: string ErrorEnvelope: type: object required: [error] diff --git a/packages/client/package.json b/packages/client/package.json index 9779933d..cf5f9260 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,10 +29,6 @@ "typescript": "^5.7.3", "vitest": "^3.0.0" }, - "publishConfig": { - "access": "public", - "provenance": true - }, "repository": { "type": "git", "url": "https://github.com/AgentWorkforce/relayfile", diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index bd9ad989..2bdd8b5b 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -106,6 +106,42 @@ describe('RelayfileControlPlaneClient lifecycle', () => { }); }); +describe('RelayfileControlPlaneClient integration webhook subscriptions', () => { + it('posts and deletes webhook subscriptions through the control-plane socket', async () => { + const client = new RelayfileControlPlaneClient({ socketPath: '/nope.sock', autoStart: false }); + vi.spyOn(client, 'ensureReady').mockResolvedValue(undefined); + const rawRequest = vi.spyOn(client as unknown as { rawRequest: (opts: unknown) => Promise }, 'rawRequest'); + rawRequest.mockResolvedValueOnce({ subscriptionId: 'whsub_123' }).mockResolvedValueOnce(undefined); + + await expect( + client.createWebhookSubscription({ + workspace: 'demo', + url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', + pathGlobs: ['/github/repos/acme/widgets/issues/**'], + secret: 'inbound-secret', + }) + ).resolves.toEqual({ subscriptionId: 'whsub_123' }); + + await expect(client.deleteWebhookSubscription('whsub_123', 'demo')).resolves.toBeUndefined(); + + expect(rawRequest).toHaveBeenNthCalledWith(1, { + method: 'POST', + path: '/v1/integrations/webhook-subscriptions', + body: { + workspace: 'demo', + url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', + pathGlobs: ['/github/repos/acme/widgets/issues/**'], + secret: 'inbound-secret', + }, + }); + expect(rawRequest).toHaveBeenNthCalledWith(2, { + method: 'DELETE', + path: '/v1/integrations/webhook-subscriptions', + body: { subscriptionId: 'whsub_123', workspace: 'demo' }, + }); + }); +}); + // Real-daemon contract tests — opt-in via RELAYFILE_BIN (CI builds the binary: // `go build -o relayfile ./cmd/relayfile-cli`). Boots the daemon and drives the // client over the socket. diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 2f4ccd0d..fad0463a 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -106,6 +106,8 @@ export type ConnectRequestBody = Schemas['ConnectProviderRequest']; export type ConnectResult = Schemas['ConnectProviderResponse']; export type ProviderStatusResult = Schemas['ProviderStatus']; export type WritebackSecretResult = Schemas['WritebackSecret']; +export type WebhookSubscriptionRequestBody = Schemas['WebhookSubscriptionRequest']; +export type WebhookSubscriptionResult = Schemas['WebhookSubscriptionResponse']; export interface RelayfileClientOptions { /** Socket to connect to. Defaults to defaultRelayfileSocketPath(). */ @@ -124,7 +126,7 @@ export interface RelayfileClientOptions { } interface RequestOptions { - method: 'GET' | 'POST'; + method: 'DELETE' | 'GET' | 'POST'; path: string; query?: Record; body?: unknown; @@ -393,6 +395,22 @@ export class RelayfileControlPlaneClient { }); } + createWebhookSubscription(input: WebhookSubscriptionRequestBody): Promise { + return this.request({ + method: 'POST', + path: '/v1/integrations/webhook-subscriptions', + body: input, + }); + } + + deleteWebhookSubscription(subscriptionId: string, workspace?: string): Promise { + return this.request({ + method: 'DELETE', + path: '/v1/integrations/webhook-subscriptions', + body: { subscriptionId, ...(workspace ? { workspace } : {}) }, + }); + } + /** Endpoint request that ensures the daemon is up + version-compatible first. */ private async request(opts: RequestOptions): Promise { await this.ensureReady(); diff --git a/packages/client/src/generated/control-plane.ts b/packages/client/src/generated/control-plane.ts index 447bb810..465d04d6 100644 --- a/packages/client/src/generated/control-plane.ts +++ b/packages/client/src/generated/control-plane.ts @@ -159,6 +159,24 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/integrations/webhook-subscriptions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create a server-side relayfile webhook subscription */ + post: operations["createWebhookSubscription"]; + /** Delete a server-side relayfile webhook subscription */ + delete: operations["deleteWebhookSubscription"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -268,6 +286,21 @@ export interface components { url: string; secret: string; }; + WebhookSubscriptionRequest: { + workspace?: string; + /** Format: uri */ + url: string; + pathGlobs: string[]; + secret: string; + }; + WebhookSubscriptionResponse: { + subscriptionId: string; + secret?: string; + }; + DeleteWebhookSubscriptionRequest: { + workspace?: string; + subscriptionId: string; + }; ErrorEnvelope: { error: { /** @enum {string} */ @@ -599,4 +632,58 @@ export interface operations { }; }; }; + createWebhookSubscription: { + parameters: { + query?: never; + header?: { + "X-Relayfile-API-Version"?: components["parameters"]["ApiVersionHeader"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WebhookSubscriptionRequest"]; + }; + }; + responses: { + /** @description Created webhook subscription */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookSubscriptionResponse"]; + }; + }; + }; + }; + deleteWebhookSubscription: { + parameters: { + query?: never; + header?: { + "X-Relayfile-API-Version"?: components["parameters"]["ApiVersionHeader"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteWebhookSubscriptionRequest"]; + }; + }; + responses: { + /** @description Subscription deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ok: boolean; + }; + }; + }; + }; + }; } From d422ef64bcea60d03a9e634816a579c1526066ac Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 8 Jul 2026 10:00:52 +0200 Subject: [PATCH 2/2] Address webhook subscription review feedback --- cmd/relayfile-cli/control_plane.go | 8 ++++---- cmd/relayfile-cli/control_plane_test.go | 7 +++++-- .../relayfile-control-plane-v1.openapi.yaml | 16 +++++++++------- packages/client/package.json | 4 ++++ packages/client/src/client.test.ts | 8 ++++---- packages/client/src/client.ts | 18 +++++++++++------- packages/client/src/generated/control-plane.ts | 9 +++++---- packages/client/src/index.ts | 4 ++++ 8 files changed, 46 insertions(+), 28 deletions(-) diff --git a/cmd/relayfile-cli/control_plane.go b/cmd/relayfile-cli/control_plane.go index 7b550696..8037c334 100644 --- a/cmd/relayfile-cli/control_plane.go +++ b/cmd/relayfile-cli/control_plane.go @@ -19,9 +19,9 @@ import ( "time" ) -const relayfileControlPlaneAPIVersion uint32 = 1 +const relayfileControlPlaneAPIVersion uint32 = 2 -var relayfileControlPlaneSupportedVersions = []uint32{relayfileControlPlaneAPIVersion} +var relayfileControlPlaneSupportedVersions = []uint32{1, relayfileControlPlaneAPIVersion} type controlPlaneErrorCode string @@ -492,7 +492,7 @@ func handleControlPlaneWebhookSubscription(w http.ResponseWriter, r *http.Reques } var response webhookSubscriptionResponse if err := commandClient.client.postJSON( - context.Background(), + r.Context(), fmt.Sprintf("/v1/workspaces/%s/webhooks", url.PathEscape(workspaceID)), map[string]any{ "url": strings.TrimSpace(req.URL), @@ -527,7 +527,7 @@ func handleControlPlaneWebhookSubscription(w http.ResponseWriter, r *http.Reques return } if err := commandClient.client.deleteJSON( - context.Background(), + r.Context(), fmt.Sprintf("/v1/workspaces/%s/webhooks/%s", url.PathEscape(workspaceID), url.PathEscape(subscriptionID)), "", nil, diff --git a/cmd/relayfile-cli/control_plane_test.go b/cmd/relayfile-cli/control_plane_test.go index 87f706ff..c3d1bb47 100644 --- a/cmd/relayfile-cli/control_plane_test.go +++ b/cmd/relayfile-cli/control_plane_test.go @@ -35,12 +35,15 @@ func TestControlPlaneHelloVersionAndCLIVersion(t *testing.T) { if status != http.StatusOK { t.Fatalf("hello status = %d", status) } - if hello.DaemonVersion != "0.10.17" || hello.APIVersion != 1 { + if hello.DaemonVersion != "0.10.17" || hello.APIVersion != 2 { t.Fatalf("unexpected hello response: %#v", hello) } + if len(hello.SupportedAPIVersions) != 2 || hello.SupportedAPIVersions[0] != 1 || hello.SupportedAPIVersions[1] != 2 { + t.Fatalf("unexpected supported API versions: %#v", hello.SupportedAPIVersions) + } var errResp map[string]controlPlaneError - status = controlPlaneJSONWithoutVersion(t, client, http.MethodGet, baseURL+"/v1/hello?apiVersion=2", nil, &errResp) + status = controlPlaneJSONWithoutVersion(t, client, http.MethodGet, baseURL+"/v1/hello?apiVersion=3", nil, &errResp) if status != http.StatusUpgradeRequired { t.Fatalf("incompatible hello status = %d, want %d", status, http.StatusUpgradeRequired) } diff --git a/openapi/relayfile-control-plane-v1.openapi.yaml b/openapi/relayfile-control-plane-v1.openapi.yaml index 4f7859a2..f095bdd0 100644 --- a/openapi/relayfile-control-plane-v1.openapi.yaml +++ b/openapi/relayfile-control-plane-v1.openapi.yaml @@ -6,7 +6,7 @@ info: Versioned local control-plane contract for the relayfile daemon/CLI. The service listens on a user-owned Unix domain socket (`RELAYFILE_SOCK`, or `$XDG_RUNTIME_DIR/relayfile.sock`) and uses - `X-Relayfile-API-Version: 1` for client compatibility checks. + `X-Relayfile-API-Version: 2` for client compatibility checks. servers: - url: http://relayfile.local description: Logical HTTP origin over the relayfile Unix domain socket. @@ -264,11 +264,7 @@ paths: content: application/json: schema: - type: object - required: [ok] - properties: - ok: - type: boolean + $ref: "#/components/schemas/DeleteWebhookSubscriptionResponse" components: parameters: ApiVersionHeader: @@ -278,7 +274,7 @@ components: schema: type: integer format: uint32 - const: 1 + const: 2 ApiVersionQuery: name: apiVersion in: query @@ -558,6 +554,12 @@ components: type: string subscriptionId: type: string + DeleteWebhookSubscriptionResponse: + type: object + required: [ok] + properties: + ok: + type: boolean ErrorEnvelope: type: object required: [error] diff --git a/packages/client/package.json b/packages/client/package.json index cf5f9260..9779933d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,6 +29,10 @@ "typescript": "^5.7.3", "vitest": "^3.0.0" }, + "publishConfig": { + "access": "public", + "provenance": true + }, "repository": { "type": "git", "url": "https://github.com/AgentWorkforce/relayfile", diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index 2bdd8b5b..0da09181 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -64,8 +64,8 @@ describe('RelayfileControlPlaneClient lifecycle', () => { const client = new RelayfileControlPlaneClient({ socketPath: '/nope.sock', autoStart: false }); vi.spyOn(client, 'hello').mockResolvedValue({ daemonVersion: '0.10.17', - apiVersion: 2, - supportedApiVersions: [2], + apiVersion: 1, + supportedApiVersions: [1], }); await expect(client.ensureReady()).rejects.toMatchObject({ code: 'VERSION_INCOMPATIBLE' }); }); @@ -111,7 +111,7 @@ describe('RelayfileControlPlaneClient integration webhook subscriptions', () => const client = new RelayfileControlPlaneClient({ socketPath: '/nope.sock', autoStart: false }); vi.spyOn(client, 'ensureReady').mockResolvedValue(undefined); const rawRequest = vi.spyOn(client as unknown as { rawRequest: (opts: unknown) => Promise }, 'rawRequest'); - rawRequest.mockResolvedValueOnce({ subscriptionId: 'whsub_123' }).mockResolvedValueOnce(undefined); + rawRequest.mockResolvedValueOnce({ subscriptionId: 'whsub_123' }).mockResolvedValueOnce({ ok: true }); await expect( client.createWebhookSubscription({ @@ -122,7 +122,7 @@ describe('RelayfileControlPlaneClient integration webhook subscriptions', () => }) ).resolves.toEqual({ subscriptionId: 'whsub_123' }); - await expect(client.deleteWebhookSubscription('whsub_123', 'demo')).resolves.toBeUndefined(); + await expect(client.deleteWebhookSubscription('whsub_123', 'demo')).resolves.toEqual({ ok: true }); expect(rawRequest).toHaveBeenNthCalledWith(1, { method: 'POST', diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index fad0463a..8bb415ca 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -17,14 +17,13 @@ import type { components } from './generated/control-plane.js'; * rename in the contract is a compile error here. */ -/** Control-plane API version this client speaks. Bump on a breaking wire change. */ -export const RELAYFILE_API_VERSION = 1; +/** Control-plane API version this client speaks. Bump when adding required endpoint support. */ +export const RELAYFILE_API_VERSION = 2; /** * Minimum `relayfile` daemon version this client requires. The control-plane * first shipped in 0.10.17, so anything that answers `/v1/hello` is already >= - * this — the check is belt-and-suspenders and the bump point for future - * contract changes. + * this — API compatibility is enforced by RELAYFILE_API_VERSION. */ export const MIN_RELAYFILE_VERSION = '0.10.17'; @@ -108,6 +107,8 @@ export type ProviderStatusResult = Schemas['ProviderStatus']; export type WritebackSecretResult = Schemas['WritebackSecret']; export type WebhookSubscriptionRequestBody = Schemas['WebhookSubscriptionRequest']; export type WebhookSubscriptionResult = Schemas['WebhookSubscriptionResponse']; +export type DeleteWebhookSubscriptionRequestBody = Schemas['DeleteWebhookSubscriptionRequest']; +export type DeleteWebhookSubscriptionResult = Schemas['DeleteWebhookSubscriptionResponse']; export interface RelayfileClientOptions { /** Socket to connect to. Defaults to defaultRelayfileSocketPath(). */ @@ -403,11 +404,14 @@ export class RelayfileControlPlaneClient { }); } - deleteWebhookSubscription(subscriptionId: string, workspace?: string): Promise { - return this.request({ + deleteWebhookSubscription( + subscriptionId: DeleteWebhookSubscriptionRequestBody['subscriptionId'], + workspace?: DeleteWebhookSubscriptionRequestBody['workspace'] + ): Promise { + return this.request({ method: 'DELETE', path: '/v1/integrations/webhook-subscriptions', - body: { subscriptionId, ...(workspace ? { workspace } : {}) }, + body: { subscriptionId, ...(workspace ? { workspace } : {}) } satisfies DeleteWebhookSubscriptionRequestBody, }); } diff --git a/packages/client/src/generated/control-plane.ts b/packages/client/src/generated/control-plane.ts index 465d04d6..a02f98e2 100644 --- a/packages/client/src/generated/control-plane.ts +++ b/packages/client/src/generated/control-plane.ts @@ -301,6 +301,9 @@ export interface components { workspace?: string; subscriptionId: string; }; + DeleteWebhookSubscriptionResponse: { + ok: boolean; + }; ErrorEnvelope: { error: { /** @enum {string} */ @@ -339,7 +342,7 @@ export interface components { }; }; parameters: { - ApiVersionHeader: 1; + ApiVersionHeader: 2; ApiVersionQuery: number; }; requestBodies: never; @@ -679,9 +682,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": { - ok: boolean; - }; + "application/json": components["schemas"]["DeleteWebhookSubscriptionResponse"]; }; }; }; diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 805c9b3a..1ffcf257 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -20,6 +20,10 @@ export type { ConnectResult, ProviderStatusResult, WritebackSecretResult, + WebhookSubscriptionRequestBody, + WebhookSubscriptionResult, + DeleteWebhookSubscriptionRequestBody, + DeleteWebhookSubscriptionResult, } from './client.js'; export type { components, paths, operations } from './generated/control-plane.js';