@checkstack/integration-webhook-backend 0.0.35 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # @checkstack/integration-webhook-backend
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 41c77f4: feat(automation): one-time migration of webhook subscriptions + remove legacy integration backend
8
+
9
+ **BREAKING CHANGES** (platform is in BETA — no major bump):
10
+
11
+ - `IntegrationProvider` no longer carries `config` (subscription
12
+ config) or `deliver`. The interface now models a connection provider
13
+ only: connection schema + `getConnectionOptions` + `testConnection`.
14
+ - The legacy subscription / delivery-log / event endpoints
15
+ (`listSubscriptions`, `createSubscription`, `getDeliveryLogs`,
16
+ `listEventTypes`, …) are removed from `integrationContract`.
17
+ - `delivery-coordinator`, `hook-subscriber`, `event-registry`, and the
18
+ `integrationEventExtensionPoint` are deleted. Plugins that
19
+ previously called `integrationEvents.registerEvent(...)` now
20
+ register their hooks as automation triggers via
21
+ `automationTriggerExtensionPoint.registerTrigger(...)`.
22
+ - Frontend pages `IntegrationsPage` and `DeliveryLogsPage` are gone;
23
+ the integration plugin's only remaining UI is connection
24
+ management. Subscription management lives under `/automation/...`.
25
+ - `webhook_subscriptions` and `delivery_logs` tables stay in the
26
+ database for one release as a safety net (no code reads or writes
27
+ them), and will be dropped in a follow-up migration.
28
+
29
+ **New**:
30
+
31
+ - `jira.create_issue`, `teams.post_message`, `webex.post_message`,
32
+ `webhook.send`, `integration-script.run_shell`, and
33
+ `integration-script.run_script` actions registered against the
34
+ Automation Platform with matching `*.message`, `*.delivery`,
35
+ `shell.result`, and `script.result` artifact types. The script
36
+ plugin exposes **two** actions — `run_shell` runs bash via the
37
+ shared `ShellScriptRunner` (Monaco `shell` editor), `run_script`
38
+ runs an ESM module in a Bun subprocess via `EsmScriptRunner`
39
+ (Monaco `typescript` editor + `defineIntegration` helper) — to
40
+ preserve the legacy provider split. `jira.create_issue` keeps the
41
+ dynamic field-mapping dropdown (driven by
42
+ `JIRA_RESOLVERS.FIELD_OPTIONS`).
43
+ - One-time data migration runs on boot in
44
+ `automation-backend.afterPluginsReady`. It reads
45
+ `webhook_subscriptions` via a new service RPC
46
+ `IntegrationApi.listLegacySubscriptions`, translates each row into
47
+ a single-trigger / single-action automation (marked with
48
+ `managed_by = "migrated-subscription:<id>"`), and is idempotent
49
+ across restarts.
50
+ - Failed translations are recorded in a new
51
+ `automation_migration_failures` table and surfaced via
52
+ `AutomationApi.listMigrationFailures` /
53
+ `acknowledgeMigrationFailure` so admins can review and re-create
54
+ failed entries by hand.
55
+
56
+ - 41c77f4: fix(automation): qualify action `produces` / `consumes` with the owning plugin id
57
+
58
+ `context.artifacts` showed up untyped (no fields) in the script editor
59
+ because action `produces` / `consumes` were hand-written full strings
60
+ (`"jira.issue"`) that did not match the artifact-type registry's
61
+ qualified id. The registry derives `${pluginId}.${id}`, and the plugin's
62
+ id is the package name `integration-jira`, so the artifact type actually
63
+ registers as `integration-jira.issue` — the editor's schema lookup
64
+ (`produces` vs registered `qualifiedId`) missed, leaving the artifact's
65
+ fields unknown. (Runtime store/consume happened to agree with each other
66
+ on the short string, so it "worked" but typed nothing.)
67
+
68
+ The action registry now qualifies `produces` with the owning plugin id,
69
+ exactly as it already qualifies the action's own `id` and as the
70
+ artifact-type registry qualifies the artifact type id — so the three can
71
+ never drift. Actions declare the **local** artifact id:
72
+
73
+ - `produces: "issue"` → registered as `integration-jira.issue`,
74
+ - `consumes: ["issue"]` → resolved against the owning plugin's namespace
75
+ at run time; `consumedArtifacts` is keyed by the local id, so an
76
+ action's `execute` reads `consumedArtifacts["issue"]`.
77
+
78
+ All five artifact-producing integration plugins (jira / teams / webex /
79
+ webhook / script) now declare local ids. With `produces` matching the
80
+ registered artifact type, the editor types `context.artifacts[...]` with
81
+ the real schema (e.g. `issueKey`, `projectKey`, `issueUrl`).
82
+
83
+ **BREAKING (beta):** the fully-qualified artifact type ids change from
84
+ the short form to the plugin-prefixed form, e.g. `jira.issue` →
85
+ `integration-jira.issue`. This affects how artifacts are referenced in
86
+ templates (`{{ artifact.integration-jira.issue.issueKey }}`), the TS
87
+ script `context.artifacts["integration-jira.issue"]`, and shell env names
88
+ (`$CHECKSTACK_ARTIFACT_INTEGRATION_JIRA_ISSUE_ISSUEKEY`). Artifacts are
89
+ per-run and ephemeral, so no stored-data migration is needed.
90
+
91
+ Note: this keeps the same-plugin produce→consume handoff (the current
92
+ pattern). Cross-plugin artifact consumption would need a follow-up to
93
+ allow a fully-qualified `consumes` ref.
94
+
95
+ ### Patch Changes
96
+
97
+ - Updated dependencies [e2d6f25]
98
+ - Updated dependencies [41c77f4]
99
+ - Updated dependencies [e1a2077]
100
+ - Updated dependencies [41c77f4]
101
+ - Updated dependencies [41c77f4]
102
+ - Updated dependencies [41c77f4]
103
+ - Updated dependencies [41c77f4]
104
+ - Updated dependencies [41c77f4]
105
+ - Updated dependencies [6d52276]
106
+ - Updated dependencies [6d52276]
107
+ - Updated dependencies [35bc682]
108
+ - @checkstack/automation-backend@0.2.0
109
+ - @checkstack/common@0.12.0
110
+ - @checkstack/backend-api@0.18.0
111
+
3
112
  ## 0.0.35
4
113
 
5
114
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/integration-webhook-backend",
3
- "version": "0.0.35",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -11,12 +11,12 @@
11
11
  "typecheck": "tsgo -b",
12
12
  "lint": "bun run lint:code",
13
13
  "lint:code": "eslint . --max-warnings 0",
14
+ "test": "bun test",
14
15
  "pack": "bunx @checkstack/scripts plugin-pack"
15
16
  },
16
17
  "dependencies": {
17
- "@checkstack/backend-api": "0.17.0",
18
- "@checkstack/integration-backend": "0.1.29",
19
- "@checkstack/integration-common": "0.5.0",
18
+ "@checkstack/backend-api": "0.17.1",
19
+ "@checkstack/automation-backend": "0.1.0",
20
20
  "@checkstack/common": "0.11.0",
21
21
  "zod": "^4.2.1"
22
22
  },
@@ -24,7 +24,8 @@
24
24
  "@types/bun": "^1.0.0",
25
25
  "typescript": "^5.0.0",
26
26
  "@checkstack/tsconfig": "0.0.7",
27
- "@checkstack/scripts": "0.3.3"
27
+ "@checkstack/scripts": "0.3.3",
28
+ "@checkstack/test-utils-backend": "0.1.30"
28
29
  },
29
30
  "description": "Checkstack integration-webhook-backend plugin",
30
31
  "author": {
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Behaviour tests for the webhook automation action.
3
+ *
4
+ * fetch is stubbed in-process so we can exercise the full
5
+ * `webhookSendAction.execute` flow including the retry/error mapping.
6
+ * Template expansion happens at the dispatch-engine level — by the
7
+ * time this action runs, `config.body` is already a rendered string,
8
+ * so we don't need to set up a template runtime here.
9
+ */
10
+ import { describe, it, expect, beforeEach } from "bun:test";
11
+ import type { Logger } from "@checkstack/backend-api";
12
+ import { createMockLogger } from "@checkstack/test-utils-backend";
13
+
14
+ import { webhookSendAction, webhookDeliveryArtifactType } from "./automations";
15
+
16
+ const logger = createMockLogger() as Logger;
17
+
18
+ interface CapturedFetchCall {
19
+ url: string;
20
+ init: RequestInit;
21
+ }
22
+
23
+ interface StagedResponse {
24
+ body?: unknown;
25
+ status?: number;
26
+ headers?: Record<string, string>;
27
+ raw?: string;
28
+ /** Throw this error instead of returning a response. */
29
+ throws?: Error;
30
+ }
31
+
32
+ function setupFetchSequence(responses: StagedResponse[]) {
33
+ const calls: CapturedFetchCall[] = [];
34
+ const originalFetch = globalThis.fetch;
35
+ let i = 0;
36
+ globalThis.fetch = (async (
37
+ input: string | URL | Request,
38
+ init?: RequestInit,
39
+ ) => {
40
+ calls.push({ url: String(input), init: init ?? {} });
41
+ const next = responses[i++] ?? { body: {}, status: 200 };
42
+ if (next.throws) throw next.throws;
43
+ const headers = new Headers({ "Content-Type": "application/json", ...next.headers });
44
+ const payload =
45
+ next.raw !== undefined ? next.raw : JSON.stringify(next.body ?? {});
46
+ return new Response(payload, {
47
+ status: next.status ?? 200,
48
+ headers,
49
+ });
50
+ }) as typeof fetch;
51
+ return {
52
+ calls,
53
+ restore: () => {
54
+ globalThis.fetch = originalFetch;
55
+ },
56
+ };
57
+ }
58
+
59
+ let fetchFixture: ReturnType<typeof setupFetchSequence> | undefined;
60
+
61
+ beforeEach(() => {
62
+ fetchFixture?.restore();
63
+ fetchFixture = undefined;
64
+ });
65
+
66
+ const ctxBase = {
67
+ runId: "run-1",
68
+ automationId: "auto-1",
69
+ contextKey: null,
70
+ logger,
71
+ getService: async <T,>(): Promise<T> => {
72
+ throw new Error("not used");
73
+ },
74
+ };
75
+
76
+ const baseConfig = {
77
+ url: "https://example.com/hook",
78
+ method: "POST" as const,
79
+ contentType: "application/json" as const,
80
+ authType: "none" as const,
81
+ timeout: 10_000,
82
+ };
83
+
84
+ describe("webhookDeliveryArtifactType", () => {
85
+ it("validates the canonical artifact shape", () => {
86
+ const ok = webhookDeliveryArtifactType.schema.safeParse({
87
+ url: "https://example.com/hook",
88
+ status: 200,
89
+ responseBody: "{}",
90
+ });
91
+ expect(ok.success).toBe(true);
92
+ });
93
+
94
+ it("rejects when status is missing", () => {
95
+ const bad = webhookDeliveryArtifactType.schema.safeParse({
96
+ url: "https://example.com/hook",
97
+ responseBody: "{}",
98
+ });
99
+ expect(bad.success).toBe(false);
100
+ });
101
+ });
102
+
103
+ describe("webhook.send", () => {
104
+ it("delivers a payload and emits a webhook.delivery artifact", async () => {
105
+ fetchFixture = setupFetchSequence([
106
+ { body: { id: "msg-42" }, status: 201 },
107
+ ]);
108
+ const result = await webhookSendAction.execute({
109
+ ...ctxBase,
110
+ consumedArtifacts: {},
111
+ config: { ...baseConfig, body: '{"x":1}' },
112
+ });
113
+ expect(result.success).toBe(true);
114
+ if (!result.success) return;
115
+ const artifact = result.artifact as {
116
+ url: string;
117
+ status: number;
118
+ externalId?: string;
119
+ responseBody: string;
120
+ };
121
+ expect(artifact.url).toBe("https://example.com/hook");
122
+ expect(artifact.status).toBe(201);
123
+ expect(artifact.externalId).toBe("msg-42");
124
+ expect(result.externalId).toBe("msg-42");
125
+ const call = fetchFixture.calls[0]!;
126
+ expect(call.init.method).toBe("POST");
127
+ expect(call.init.body).toBe('{"x":1}');
128
+ const headers = call.init.headers as Record<string, string>;
129
+ expect(headers["Content-Type"]).toBe("application/json");
130
+ });
131
+
132
+ it("attaches a bearer token to the Authorization header", async () => {
133
+ fetchFixture = setupFetchSequence([{ body: {} }]);
134
+ await webhookSendAction.execute({
135
+ ...ctxBase,
136
+ consumedArtifacts: {},
137
+ config: {
138
+ ...baseConfig,
139
+ authType: "bearer",
140
+ bearerToken: "secret-token",
141
+ },
142
+ });
143
+ const headers = fetchFixture.calls[0]!.init.headers as Record<string, string>;
144
+ expect(headers.Authorization).toBe("Bearer secret-token");
145
+ });
146
+
147
+ it("attaches Basic auth when username and password are set", async () => {
148
+ fetchFixture = setupFetchSequence([{ body: {} }]);
149
+ await webhookSendAction.execute({
150
+ ...ctxBase,
151
+ consumedArtifacts: {},
152
+ config: {
153
+ ...baseConfig,
154
+ authType: "basic",
155
+ basicUsername: "alice",
156
+ basicPassword: "wonderland",
157
+ },
158
+ });
159
+ const headers = fetchFixture.calls[0]!.init.headers as Record<string, string>;
160
+ const expected = `Basic ${Buffer.from("alice:wonderland").toString("base64")}`;
161
+ expect(headers.Authorization).toBe(expected);
162
+ });
163
+
164
+ it("attaches a custom header when authType is header", async () => {
165
+ fetchFixture = setupFetchSequence([{ body: {} }]);
166
+ await webhookSendAction.execute({
167
+ ...ctxBase,
168
+ consumedArtifacts: {},
169
+ config: {
170
+ ...baseConfig,
171
+ authType: "header",
172
+ authHeaderName: "X-API-Key",
173
+ authHeaderValue: "k123",
174
+ },
175
+ });
176
+ const headers = fetchFixture.calls[0]!.init.headers as Record<string, string>;
177
+ expect(headers["X-API-Key"]).toBe("k123");
178
+ });
179
+
180
+ it("appends extra custom headers", async () => {
181
+ fetchFixture = setupFetchSequence([{ body: {} }]);
182
+ await webhookSendAction.execute({
183
+ ...ctxBase,
184
+ consumedArtifacts: {},
185
+ config: {
186
+ ...baseConfig,
187
+ customHeaders: [
188
+ { name: "X-Source", value: "checkstack" },
189
+ { name: "X-Trace", value: "abc" },
190
+ ],
191
+ },
192
+ });
193
+ const headers = fetchFixture.calls[0]!.init.headers as Record<string, string>;
194
+ expect(headers["X-Source"]).toBe("checkstack");
195
+ expect(headers["X-Trace"]).toBe("abc");
196
+ });
197
+
198
+ it("returns a retryable failure when the status matches retryOnStatus", async () => {
199
+ fetchFixture = setupFetchSequence([
200
+ { status: 429, raw: "rate limited", headers: { "Retry-After": "5" } },
201
+ ]);
202
+ const result = await webhookSendAction.execute({
203
+ ...ctxBase,
204
+ consumedArtifacts: {},
205
+ config: { ...baseConfig, retryOnStatus: [429, 503] },
206
+ });
207
+ expect(result.success).toBe(false);
208
+ if (result.success) return;
209
+ expect(result.retryAfterMs).toBe(5_000);
210
+ expect(result.error).toMatch(/429/);
211
+ });
212
+
213
+ it("defaults retryAfterMs to 30s when Retry-After is missing", async () => {
214
+ fetchFixture = setupFetchSequence([
215
+ { status: 503, raw: "unavailable" },
216
+ ]);
217
+ const result = await webhookSendAction.execute({
218
+ ...ctxBase,
219
+ consumedArtifacts: {},
220
+ config: { ...baseConfig, retryOnStatus: [503] },
221
+ });
222
+ expect(result.success).toBe(false);
223
+ if (result.success) return;
224
+ expect(result.retryAfterMs).toBe(30_000);
225
+ });
226
+
227
+ it("returns a non-retryable failure for an unmatched 4xx/5xx", async () => {
228
+ fetchFixture = setupFetchSequence([
229
+ { status: 500, raw: "boom" },
230
+ ]);
231
+ const result = await webhookSendAction.execute({
232
+ ...ctxBase,
233
+ consumedArtifacts: {},
234
+ config: { ...baseConfig },
235
+ });
236
+ expect(result.success).toBe(false);
237
+ if (result.success) return;
238
+ expect(result.error).toMatch(/HTTP 500/);
239
+ expect(result.retryAfterMs).toBeUndefined();
240
+ });
241
+
242
+ it("classifies network errors as retryable", async () => {
243
+ fetchFixture = setupFetchSequence([
244
+ { throws: new Error("ECONNREFUSED 127.0.0.1") },
245
+ ]);
246
+ const result = await webhookSendAction.execute({
247
+ ...ctxBase,
248
+ consumedArtifacts: {},
249
+ config: { ...baseConfig },
250
+ });
251
+ expect(result.success).toBe(false);
252
+ if (result.success) return;
253
+ expect(result.retryAfterMs).toBe(30_000);
254
+ });
255
+
256
+ it("does not retry generic non-network errors", async () => {
257
+ fetchFixture = setupFetchSequence([
258
+ { throws: new Error("something else broke") },
259
+ ]);
260
+ const result = await webhookSendAction.execute({
261
+ ...ctxBase,
262
+ consumedArtifacts: {},
263
+ config: { ...baseConfig },
264
+ });
265
+ expect(result.success).toBe(false);
266
+ if (result.success) return;
267
+ expect(result.retryAfterMs).toBeUndefined();
268
+ });
269
+
270
+ it("returns success without externalId when the response body is not JSON", async () => {
271
+ fetchFixture = setupFetchSequence([
272
+ { status: 200, raw: "ok" },
273
+ ]);
274
+ const result = await webhookSendAction.execute({
275
+ ...ctxBase,
276
+ consumedArtifacts: {},
277
+ config: { ...baseConfig },
278
+ });
279
+ expect(result.success).toBe(true);
280
+ if (!result.success) return;
281
+ expect(result.externalId).toBeUndefined();
282
+ const artifact = result.artifact as { responseBody: string };
283
+ expect(artifact.responseBody).toBe("ok");
284
+ });
285
+
286
+ it("truncates the captured response body to 1000 chars", async () => {
287
+ const big = "x".repeat(5_000);
288
+ fetchFixture = setupFetchSequence([{ status: 200, raw: big }]);
289
+ const result = await webhookSendAction.execute({
290
+ ...ctxBase,
291
+ consumedArtifacts: {},
292
+ config: { ...baseConfig },
293
+ });
294
+ expect(result.success).toBe(true);
295
+ if (!result.success) return;
296
+ const artifact = result.artifact as { responseBody: string };
297
+ expect(artifact.responseBody.length).toBe(1_000);
298
+ });
299
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Webhook action — generic HTTP POST/PUT/PATCH to an external endpoint.
3
+ *
4
+ * Replaces the legacy `webhookProvider.deliver` path. All template
5
+ * expansion now happens in the dispatch engine before `execute` is
6
+ * called, so this action just builds the request and fires it.
7
+ */
8
+ import { z } from "zod";
9
+ import {
10
+ configNumber,
11
+ configString,
12
+ requestTimeoutMs,
13
+ Versioned,
14
+ } from "@checkstack/backend-api";
15
+ import type { ActionDefinition } from "@checkstack/automation-backend";
16
+ import { extractErrorMessage } from "@checkstack/common";
17
+
18
+ const webhookHeaderSchema = z.object({
19
+ name: z.string().min(1).describe("Header name"),
20
+ value: z.string().describe("Header value"),
21
+ });
22
+
23
+ export const webhookSendConfigSchema = z.object({
24
+ url: configString({}).url().describe("Endpoint URL"),
25
+ method: z
26
+ .enum(["POST", "PUT", "PATCH"])
27
+ .default("POST")
28
+ .describe("HTTP method"),
29
+ contentType: z
30
+ .enum(["application/json", "application/x-www-form-urlencoded"])
31
+ .default("application/json"),
32
+ authType: z
33
+ .enum(["none", "bearer", "basic", "header"])
34
+ .default("none")
35
+ .describe("Authentication method"),
36
+ bearerToken: configString({ "x-secret": true })
37
+ .optional()
38
+ .describe("Bearer token"),
39
+ basicUsername: configString({}).optional(),
40
+ basicPassword: configString({ "x-secret": true }).optional(),
41
+ authHeaderName: configString({}).optional(),
42
+ authHeaderValue: configString({ "x-secret": true }).optional(),
43
+ customHeaders: z.array(webhookHeaderSchema).optional(),
44
+ body: configString({
45
+ "x-editor-types": ["raw", "json", "yaml", "xml", "formdata"],
46
+ })
47
+ .optional()
48
+ .describe("Request body — already rendered when this action runs"),
49
+ timeout: requestTimeoutMs().describe("Request timeout in milliseconds"),
50
+ retryOnStatus: z
51
+ .array(configNumber({}))
52
+ .optional()
53
+ .describe("Retryable HTTP status codes (e.g., 429, 503)"),
54
+ });
55
+
56
+ export type WebhookSendConfig = z.infer<typeof webhookSendConfigSchema>;
57
+
58
+ interface WebhookDeliveryArtifact {
59
+ url: string;
60
+ status: number;
61
+ externalId?: string;
62
+ responseBody: string;
63
+ }
64
+
65
+ export const webhookSendAction: ActionDefinition<
66
+ WebhookSendConfig,
67
+ WebhookDeliveryArtifact
68
+ > = {
69
+ id: "send",
70
+ displayName: "Send Webhook",
71
+ description: "Deliver a payload to an HTTP endpoint",
72
+ category: "Webhook",
73
+ icon: "Webhook",
74
+ config: new Versioned({
75
+ version: 1,
76
+ schema: webhookSendConfigSchema,
77
+ }),
78
+ produces: "delivery",
79
+ execute: async ({ config, logger }) => {
80
+ const headers: Record<string, string> = {
81
+ "Content-Type": config.contentType,
82
+ "User-Agent": "Checkstack-Automation/1.0",
83
+ };
84
+
85
+ switch (config.authType) {
86
+ case "bearer": {
87
+ if (config.bearerToken) {
88
+ headers.Authorization = `Bearer ${config.bearerToken}`;
89
+ }
90
+ break;
91
+ }
92
+ case "basic": {
93
+ if (config.basicUsername && config.basicPassword) {
94
+ const credentials = Buffer.from(
95
+ `${config.basicUsername}:${config.basicPassword}`,
96
+ ).toString("base64");
97
+ headers.Authorization = `Basic ${credentials}`;
98
+ }
99
+ break;
100
+ }
101
+ case "header": {
102
+ if (config.authHeaderName && config.authHeaderValue) {
103
+ headers[config.authHeaderName] = config.authHeaderValue;
104
+ }
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (config.customHeaders) {
110
+ for (const header of config.customHeaders) {
111
+ headers[header.name] = header.value;
112
+ }
113
+ }
114
+
115
+ const body = config.body ?? "";
116
+
117
+ try {
118
+ const response = await fetch(config.url, {
119
+ method: config.method,
120
+ headers,
121
+ body,
122
+ signal: AbortSignal.timeout(config.timeout),
123
+ });
124
+
125
+ const responseText = await response.text();
126
+
127
+ if (config.retryOnStatus?.includes(response.status)) {
128
+ const retryAfter = response.headers.get("Retry-After");
129
+ const retryAfterMs = retryAfter
130
+ ? Number.parseInt(retryAfter, 10) * 1000
131
+ : 30_000;
132
+ return {
133
+ success: false,
134
+ error: `Received status ${response.status} (retryable)`,
135
+ retryAfterMs,
136
+ };
137
+ }
138
+
139
+ if (!response.ok) {
140
+ return {
141
+ success: false,
142
+ error: `HTTP ${response.status}: ${responseText.slice(0, 200)}`,
143
+ };
144
+ }
145
+
146
+ let externalId: string | undefined;
147
+ try {
148
+ const json = JSON.parse(responseText) as Record<string, unknown>;
149
+ const candidate = json.id ?? json.externalId ?? json.messageId;
150
+ if (typeof candidate === "string") externalId = candidate;
151
+ } catch {
152
+ // Not JSON — that's fine, no externalId.
153
+ }
154
+
155
+ logger.debug(
156
+ `Webhook delivered to ${config.url}: ${response.status}`,
157
+ );
158
+ return {
159
+ success: true,
160
+ externalId,
161
+ artifact: {
162
+ url: config.url,
163
+ status: response.status,
164
+ externalId,
165
+ responseBody: responseText.slice(0, 1000),
166
+ },
167
+ };
168
+ } catch (error) {
169
+ const message = extractErrorMessage(error);
170
+ logger.error(`Webhook delivery failed: ${message}`);
171
+ if (
172
+ message.includes("timeout") ||
173
+ message.includes("ECONNREFUSED") ||
174
+ message.includes("ENOTFOUND")
175
+ ) {
176
+ return { success: false, error: message, retryAfterMs: 30_000 };
177
+ }
178
+ return { success: false, error: message };
179
+ }
180
+ },
181
+ };
182
+
183
+ export const webhookDeliveryArtifactType = {
184
+ id: "delivery",
185
+ displayName: "Webhook Delivery",
186
+ description: "Result of an HTTP delivery attempt",
187
+ schema: z.object({
188
+ url: z.string(),
189
+ status: z.number(),
190
+ externalId: z.string().optional(),
191
+ responseBody: z.string(),
192
+ }),
193
+ } as const;
package/src/index.ts CHANGED
@@ -1,29 +1,22 @@
1
- import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
2
- import { integrationProviderExtensionPoint } from "@checkstack/integration-backend";
1
+ import { createBackendPlugin } from "@checkstack/backend-api";
2
+ import {
3
+ automationActionExtensionPoint,
4
+ automationArtifactTypeExtensionPoint,
5
+ } from "@checkstack/automation-backend";
3
6
  import { pluginMetadata } from "./plugin-metadata";
4
- import { webhookProvider } from "./provider";
7
+ import {
8
+ webhookDeliveryArtifactType,
9
+ webhookSendAction,
10
+ } from "./automations";
5
11
 
6
12
  export default createBackendPlugin({
7
13
  metadata: pluginMetadata,
8
-
9
14
  register(env) {
10
- env.registerInit({
11
- deps: {
12
- logger: coreServices.logger,
13
- },
14
- init: async ({ logger }) => {
15
- logger.debug("🔌 Registering Webhook Integration Provider...");
16
-
17
- // Get the integration provider extension point
18
- const extensionPoint = env.getExtensionPoint(
19
- integrationProviderExtensionPoint,
20
- );
21
-
22
- // Register the webhook provider
23
- extensionPoint.addProvider(webhookProvider, pluginMetadata);
24
-
25
- logger.debug("✅ Webhook Integration Provider registered.");
26
- },
27
- });
15
+ env
16
+ .getExtensionPoint(automationArtifactTypeExtensionPoint)
17
+ .registerArtifactType(webhookDeliveryArtifactType, pluginMetadata);
18
+ env
19
+ .getExtensionPoint(automationActionExtensionPoint)
20
+ .registerAction(webhookSendAction, pluginMetadata);
28
21
  },
29
22
  });