@checkstack/integration-webex-backend 0.0.34 → 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 +206 -0
- package/package.json +8 -5
- package/src/automations.test.ts +199 -0
- package/src/automations.ts +138 -0
- package/src/index.ts +27 -6
- package/src/provider.ts +44 -272
- package/tsconfig.json +6 -0
- package/src/provider.test.ts +0 -383
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,211 @@
|
|
|
1
1
|
# @checkstack/integration-webex-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e2d6f25: feat(automation): connection picker for integration actions + restore Integrations menu
|
|
8
|
+
|
|
9
|
+
Connection-backed automation actions (Jira, Teams, Webex) now render a
|
|
10
|
+
working connection picker plus cascading provider dropdowns in the
|
|
11
|
+
visual editor, and the Integrations entry is back in the user menu.
|
|
12
|
+
|
|
13
|
+
**Contract.** `ActionDefinition` gained an optional
|
|
14
|
+
`connectionProviderId` (and it is surfaced on `ActionInfoSchema` and
|
|
15
|
+
mapped in the `listActions` router). It carries the integration
|
|
16
|
+
provider's fully-qualified id, derived from the provider plugin's own
|
|
17
|
+
`pluginMetadata.pluginId` (never a hardcoded string), so the editor
|
|
18
|
+
knows which provider backs an action's dropdowns and it matches the
|
|
19
|
+
`qualifiedId` the integration provider registry assigns.
|
|
20
|
+
|
|
21
|
+
**Providers.** Jira, Teams and Webex each export
|
|
22
|
+
`*_PROVIDER_LOCAL_ID` / `*_PROVIDER_QUALIFIED_ID`, register their
|
|
23
|
+
provider with the local id, and add a `CONNECTION_OPTIONS`
|
|
24
|
+
(`"connectionOptions"`) resolver name. Their `post_message` /
|
|
25
|
+
issue actions set `connectionProviderId` and expose `connectionId`
|
|
26
|
+
as an `x-options-resolver` dropdown instead of a hidden field.
|
|
27
|
+
|
|
28
|
+
**Frontend bridge.** A new `useConnectionOptionResolvers` hook
|
|
29
|
+
(`@checkstack/automation-frontend`, which now depends on
|
|
30
|
+
`@checkstack/integration-common`) turns an action's
|
|
31
|
+
`x-options-resolver` schema fields into live data: the
|
|
32
|
+
`connectionOptions` resolver lists the provider's connections via
|
|
33
|
+
`listConnections`, and every other resolver name is forwarded to
|
|
34
|
+
`getConnectionOptions` for the selected `connectionId`, passing the
|
|
35
|
+
live form values as `context` for dependent fields. `ProviderActionBody`
|
|
36
|
+
now passes this map to `DynamicForm` (it was previously missing
|
|
37
|
+
entirely, so connection-backed actions had no working dropdowns).
|
|
38
|
+
|
|
39
|
+
**frontend-api.** `usePluginClient` procedures now also expose a typed
|
|
40
|
+
imperative `.call(input)` alongside `.useQuery` / `.useMutation`, for
|
|
41
|
+
async callbacks that cannot host a hook (such as a `DynamicForm`
|
|
42
|
+
options resolver). Additive, non-breaking.
|
|
43
|
+
|
|
44
|
+
**Integrations menu.** Re-added `IntegrationMenuItem` and a new
|
|
45
|
+
`IntegrationsLandingPage`, wired into `integration-frontend` as a list
|
|
46
|
+
route and a `UserMenuItemsSlot` entry under the "Configuration" group.
|
|
47
|
+
|
|
48
|
+
**Action card polish.** The action editor's secondary metadata (id,
|
|
49
|
+
description, failure behaviour) is now grouped into one quiet settings
|
|
50
|
+
panel with consistent small uppercase "eyebrow" labels, so the action's
|
|
51
|
+
own configuration stays the focal point. The raw failure checkbox was
|
|
52
|
+
replaced with the standard `Checkbox` control, and the provider action
|
|
53
|
+
picker / configuration sections gained consistent section headers and a
|
|
54
|
+
divider. The per-step "type" dropdown was removed: an action's kind is
|
|
55
|
+
fixed at creation, so changing it now means adding a new step and
|
|
56
|
+
deleting the old one (avoids the surprising full-config reset that
|
|
57
|
+
switching kinds used to trigger).
|
|
58
|
+
|
|
59
|
+
**Add-step picker.** Adding a step now opens a Home-Assistant-style
|
|
60
|
+
dialog where the operator decides the step type up front: an "Actions"
|
|
61
|
+
tab lists the registered provider actions grouped by category
|
|
62
|
+
(searchable; picking one presets the step's `action`), and a "Blocks"
|
|
63
|
+
tab lists the structural building blocks (choose / parallel / repeat /
|
|
64
|
+
etc.). Because the concrete action is chosen here, the in-card action
|
|
65
|
+
switcher was removed - a step's action is fixed once created. Composite
|
|
66
|
+
blocks now start with an empty child list (filled via the nested
|
|
67
|
+
add-step picker) instead of seeding an unconfigurable empty action.
|
|
68
|
+
|
|
69
|
+
- 41c77f4: feat(automation): one-time migration of webhook subscriptions + remove legacy integration backend
|
|
70
|
+
|
|
71
|
+
**BREAKING CHANGES** (platform is in BETA — no major bump):
|
|
72
|
+
|
|
73
|
+
- `IntegrationProvider` no longer carries `config` (subscription
|
|
74
|
+
config) or `deliver`. The interface now models a connection provider
|
|
75
|
+
only: connection schema + `getConnectionOptions` + `testConnection`.
|
|
76
|
+
- The legacy subscription / delivery-log / event endpoints
|
|
77
|
+
(`listSubscriptions`, `createSubscription`, `getDeliveryLogs`,
|
|
78
|
+
`listEventTypes`, …) are removed from `integrationContract`.
|
|
79
|
+
- `delivery-coordinator`, `hook-subscriber`, `event-registry`, and the
|
|
80
|
+
`integrationEventExtensionPoint` are deleted. Plugins that
|
|
81
|
+
previously called `integrationEvents.registerEvent(...)` now
|
|
82
|
+
register their hooks as automation triggers via
|
|
83
|
+
`automationTriggerExtensionPoint.registerTrigger(...)`.
|
|
84
|
+
- Frontend pages `IntegrationsPage` and `DeliveryLogsPage` are gone;
|
|
85
|
+
the integration plugin's only remaining UI is connection
|
|
86
|
+
management. Subscription management lives under `/automation/...`.
|
|
87
|
+
- `webhook_subscriptions` and `delivery_logs` tables stay in the
|
|
88
|
+
database for one release as a safety net (no code reads or writes
|
|
89
|
+
them), and will be dropped in a follow-up migration.
|
|
90
|
+
|
|
91
|
+
**New**:
|
|
92
|
+
|
|
93
|
+
- `jira.create_issue`, `teams.post_message`, `webex.post_message`,
|
|
94
|
+
`webhook.send`, `integration-script.run_shell`, and
|
|
95
|
+
`integration-script.run_script` actions registered against the
|
|
96
|
+
Automation Platform with matching `*.message`, `*.delivery`,
|
|
97
|
+
`shell.result`, and `script.result` artifact types. The script
|
|
98
|
+
plugin exposes **two** actions — `run_shell` runs bash via the
|
|
99
|
+
shared `ShellScriptRunner` (Monaco `shell` editor), `run_script`
|
|
100
|
+
runs an ESM module in a Bun subprocess via `EsmScriptRunner`
|
|
101
|
+
(Monaco `typescript` editor + `defineIntegration` helper) — to
|
|
102
|
+
preserve the legacy provider split. `jira.create_issue` keeps the
|
|
103
|
+
dynamic field-mapping dropdown (driven by
|
|
104
|
+
`JIRA_RESOLVERS.FIELD_OPTIONS`).
|
|
105
|
+
- One-time data migration runs on boot in
|
|
106
|
+
`automation-backend.afterPluginsReady`. It reads
|
|
107
|
+
`webhook_subscriptions` via a new service RPC
|
|
108
|
+
`IntegrationApi.listLegacySubscriptions`, translates each row into
|
|
109
|
+
a single-trigger / single-action automation (marked with
|
|
110
|
+
`managed_by = "migrated-subscription:<id>"`), and is idempotent
|
|
111
|
+
across restarts.
|
|
112
|
+
- Failed translations are recorded in a new
|
|
113
|
+
`automation_migration_failures` table and surfaced via
|
|
114
|
+
`AutomationApi.listMigrationFailures` /
|
|
115
|
+
`acknowledgeMigrationFailure` so admins can review and re-create
|
|
116
|
+
failed entries by hand.
|
|
117
|
+
|
|
118
|
+
- 41c77f4: fix(automation): qualify action `produces` / `consumes` with the owning plugin id
|
|
119
|
+
|
|
120
|
+
`context.artifacts` showed up untyped (no fields) in the script editor
|
|
121
|
+
because action `produces` / `consumes` were hand-written full strings
|
|
122
|
+
(`"jira.issue"`) that did not match the artifact-type registry's
|
|
123
|
+
qualified id. The registry derives `${pluginId}.${id}`, and the plugin's
|
|
124
|
+
id is the package name `integration-jira`, so the artifact type actually
|
|
125
|
+
registers as `integration-jira.issue` — the editor's schema lookup
|
|
126
|
+
(`produces` vs registered `qualifiedId`) missed, leaving the artifact's
|
|
127
|
+
fields unknown. (Runtime store/consume happened to agree with each other
|
|
128
|
+
on the short string, so it "worked" but typed nothing.)
|
|
129
|
+
|
|
130
|
+
The action registry now qualifies `produces` with the owning plugin id,
|
|
131
|
+
exactly as it already qualifies the action's own `id` and as the
|
|
132
|
+
artifact-type registry qualifies the artifact type id — so the three can
|
|
133
|
+
never drift. Actions declare the **local** artifact id:
|
|
134
|
+
|
|
135
|
+
- `produces: "issue"` → registered as `integration-jira.issue`,
|
|
136
|
+
- `consumes: ["issue"]` → resolved against the owning plugin's namespace
|
|
137
|
+
at run time; `consumedArtifacts` is keyed by the local id, so an
|
|
138
|
+
action's `execute` reads `consumedArtifacts["issue"]`.
|
|
139
|
+
|
|
140
|
+
All five artifact-producing integration plugins (jira / teams / webex /
|
|
141
|
+
webhook / script) now declare local ids. With `produces` matching the
|
|
142
|
+
registered artifact type, the editor types `context.artifacts[...]` with
|
|
143
|
+
the real schema (e.g. `issueKey`, `projectKey`, `issueUrl`).
|
|
144
|
+
|
|
145
|
+
**BREAKING (beta):** the fully-qualified artifact type ids change from
|
|
146
|
+
the short form to the plugin-prefixed form, e.g. `jira.issue` →
|
|
147
|
+
`integration-jira.issue`. This affects how artifacts are referenced in
|
|
148
|
+
templates (`{{ artifact.integration-jira.issue.issueKey }}`), the TS
|
|
149
|
+
script `context.artifacts["integration-jira.issue"]`, and shell env names
|
|
150
|
+
(`$CHECKSTACK_ARTIFACT_INTEGRATION_JIRA_ISSUE_ISSUEKEY`). Artifacts are
|
|
151
|
+
per-run and ephemeral, so no stored-data migration is needed.
|
|
152
|
+
|
|
153
|
+
Note: this keeps the same-plugin produce→consume handoff (the current
|
|
154
|
+
pattern). Cross-plugin artifact consumption would need a follow-up to
|
|
155
|
+
allow a fully-qualified `consumes` ref.
|
|
156
|
+
|
|
157
|
+
### Patch Changes
|
|
158
|
+
|
|
159
|
+
- 41c77f4: fix(integration): resolve `connectionStoreRef` lazily inside action `execute`
|
|
160
|
+
|
|
161
|
+
The Phase 6/7/8 refactor wired every integration backend's
|
|
162
|
+
`registerInit` deps to include `connectionStore: connectionStoreRef`,
|
|
163
|
+
expecting `integration-backend` to register the service before the
|
|
164
|
+
sort. But `integration-backend` calls `env.registerService(connection
|
|
165
|
+
StoreRef, ...)` from inside its own `init()`, not at `register()`
|
|
166
|
+
time — so at topological-sort time the `providedBy` map doesn't know
|
|
167
|
+
the service exists yet, and the sort can put a consumer (e.g.
|
|
168
|
+
`integration-teams`) ahead of `integration-backend`. The dev server
|
|
169
|
+
then fails at boot with:
|
|
170
|
+
|
|
171
|
+
> Service 'integration.connectionStore' not found for plugin
|
|
172
|
+
> 'integration-teams'
|
|
173
|
+
|
|
174
|
+
This change drops the init-time dep from every integration plugin and
|
|
175
|
+
resolves the connection store **lazily at action-execute time** via
|
|
176
|
+
`context.getService(connectionStoreRef)`. By the time any action's
|
|
177
|
+
`execute` runs, every plugin has finished init + afterPluginsReady,
|
|
178
|
+
so the service is always available. Tests updated to thread a mock
|
|
179
|
+
store through a typed `getService` stub in the action context.
|
|
180
|
+
|
|
181
|
+
No behaviour change at runtime — the actions hit the connection store
|
|
182
|
+
at the same moment they always did (just inside `execute` rather than
|
|
183
|
+
through a captured init-time closure).
|
|
184
|
+
|
|
185
|
+
- Updated dependencies [e2d6f25]
|
|
186
|
+
- Updated dependencies [41c77f4]
|
|
187
|
+
- Updated dependencies [e1a2077]
|
|
188
|
+
- Updated dependencies [41c77f4]
|
|
189
|
+
- Updated dependencies [41c77f4]
|
|
190
|
+
- Updated dependencies [41c77f4]
|
|
191
|
+
- Updated dependencies [41c77f4]
|
|
192
|
+
- Updated dependencies [41c77f4]
|
|
193
|
+
- Updated dependencies [41c77f4]
|
|
194
|
+
- Updated dependencies [6d52276]
|
|
195
|
+
- Updated dependencies [6d52276]
|
|
196
|
+
- Updated dependencies [35bc682]
|
|
197
|
+
- @checkstack/automation-backend@0.2.0
|
|
198
|
+
- @checkstack/integration-backend@0.2.0
|
|
199
|
+
- @checkstack/common@0.12.0
|
|
200
|
+
- @checkstack/backend-api@0.18.0
|
|
201
|
+
|
|
202
|
+
## 0.0.35
|
|
203
|
+
|
|
204
|
+
### Patch Changes
|
|
205
|
+
|
|
206
|
+
- @checkstack/backend-api@0.17.1
|
|
207
|
+
- @checkstack/integration-backend@0.1.30
|
|
208
|
+
|
|
3
209
|
## 0.0.34
|
|
4
210
|
|
|
5
211
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/integration-webex-backend",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"checkstack": {
|
|
@@ -11,18 +11,21 @@
|
|
|
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.
|
|
18
|
-
"@checkstack/integration-backend": "0.1.
|
|
19
|
-
"@checkstack/
|
|
18
|
+
"@checkstack/backend-api": "0.17.1",
|
|
19
|
+
"@checkstack/integration-backend": "0.1.30",
|
|
20
|
+
"@checkstack/automation-backend": "0.1.0",
|
|
21
|
+
"@checkstack/common": "0.11.0",
|
|
20
22
|
"zod": "^4.2.1"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@types/bun": "^1.0.0",
|
|
24
26
|
"typescript": "^5.0.0",
|
|
25
|
-
"@checkstack/tsconfig": "0.0.7"
|
|
27
|
+
"@checkstack/tsconfig": "0.0.7",
|
|
28
|
+
"@checkstack/test-utils-backend": "0.1.30"
|
|
26
29
|
},
|
|
27
30
|
"description": "Checkstack integration-webex-backend plugin",
|
|
28
31
|
"author": {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour tests for the Webex automation action.
|
|
3
|
+
*
|
|
4
|
+
* The ConnectionStore is faked; fetch is stubbed in-process so we can
|
|
5
|
+
* exercise the full action.execute flow including the connection
|
|
6
|
+
* lookup + Graph-style response mapping.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, mock } from "bun:test";
|
|
9
|
+
import type { Logger } from "@checkstack/backend-api";
|
|
10
|
+
import { createMockLogger } from "@checkstack/test-utils-backend";
|
|
11
|
+
import {
|
|
12
|
+
connectionStoreRef,
|
|
13
|
+
type ConnectionStore,
|
|
14
|
+
} from "@checkstack/integration-backend";
|
|
15
|
+
import type { ServiceRef } from "@checkstack/backend-api";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createWebexActions,
|
|
19
|
+
webexMessageArtifactType,
|
|
20
|
+
} from "./automations";
|
|
21
|
+
|
|
22
|
+
const logger = createMockLogger() as Logger;
|
|
23
|
+
|
|
24
|
+
interface CapturedFetchCall {
|
|
25
|
+
url: string;
|
|
26
|
+
init: RequestInit;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface StagedResponse {
|
|
30
|
+
body?: unknown;
|
|
31
|
+
status?: number;
|
|
32
|
+
raw?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setupFetchSequence(responses: StagedResponse[]) {
|
|
36
|
+
const calls: CapturedFetchCall[] = [];
|
|
37
|
+
const originalFetch = globalThis.fetch;
|
|
38
|
+
let i = 0;
|
|
39
|
+
globalThis.fetch = (async (
|
|
40
|
+
input: string | URL | Request,
|
|
41
|
+
init?: RequestInit,
|
|
42
|
+
) => {
|
|
43
|
+
calls.push({ url: String(input), init: init ?? {} });
|
|
44
|
+
const next = responses[i++] ?? { body: {}, status: 200 };
|
|
45
|
+
return new Response(
|
|
46
|
+
next.raw !== undefined ? next.raw : JSON.stringify(next.body ?? {}),
|
|
47
|
+
{
|
|
48
|
+
status: next.status ?? 200,
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
}) as typeof fetch;
|
|
53
|
+
return {
|
|
54
|
+
calls,
|
|
55
|
+
restore: () => {
|
|
56
|
+
globalThis.fetch = originalFetch;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeConnectionStore(): ConnectionStore {
|
|
62
|
+
return {
|
|
63
|
+
listConnections: mock(async () => []),
|
|
64
|
+
getConnection: mock(async () => undefined),
|
|
65
|
+
getConnectionWithCredentials: mock(async (id: string) =>
|
|
66
|
+
id === "conn-1"
|
|
67
|
+
? {
|
|
68
|
+
id: "conn-1",
|
|
69
|
+
providerId: "webex",
|
|
70
|
+
name: "Webex Bot",
|
|
71
|
+
config: { botToken: "secret-bot-token" },
|
|
72
|
+
createdAt: new Date(),
|
|
73
|
+
updatedAt: new Date(),
|
|
74
|
+
}
|
|
75
|
+
: undefined,
|
|
76
|
+
),
|
|
77
|
+
createConnection: mock(async () => {
|
|
78
|
+
throw new Error("not implemented in stub");
|
|
79
|
+
}),
|
|
80
|
+
updateConnection: mock(async () => {
|
|
81
|
+
throw new Error("not implemented in stub");
|
|
82
|
+
}),
|
|
83
|
+
deleteConnection: mock(async () => false),
|
|
84
|
+
findConnectionProvider: mock(async () => undefined),
|
|
85
|
+
} as unknown as ConnectionStore;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let fetchFixture: ReturnType<typeof setupFetchSequence> | undefined;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
fetchFixture?.restore();
|
|
92
|
+
fetchFixture = undefined;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function makeCtx(store: ConnectionStore) {
|
|
96
|
+
return {
|
|
97
|
+
runId: "run-1",
|
|
98
|
+
automationId: "auto-1",
|
|
99
|
+
contextKey: null,
|
|
100
|
+
logger,
|
|
101
|
+
getService: async <T,>(ref: ServiceRef<T>): Promise<T> => {
|
|
102
|
+
if (ref.id === connectionStoreRef.id) {
|
|
103
|
+
return store as unknown as T;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Unstubbed service requested: ${ref.id}`);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ctxBase = makeCtx(makeConnectionStore());
|
|
111
|
+
|
|
112
|
+
describe("webexMessageArtifactType", () => {
|
|
113
|
+
it("validates the canonical artifact shape", () => {
|
|
114
|
+
const ok = webexMessageArtifactType.schema.safeParse({
|
|
115
|
+
messageId: "msg-1",
|
|
116
|
+
roomId: "room-1",
|
|
117
|
+
});
|
|
118
|
+
expect(ok.success).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("rejects when messageId is missing", () => {
|
|
122
|
+
const bad = webexMessageArtifactType.schema.safeParse({
|
|
123
|
+
roomId: "room-1",
|
|
124
|
+
});
|
|
125
|
+
expect(bad.success).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("webex.post_message", () => {
|
|
130
|
+
it("posts a markdown message and emits a webex.message artifact", async () => {
|
|
131
|
+
fetchFixture = setupFetchSequence([{ body: { id: "msg-42" } }]);
|
|
132
|
+
const actions = createWebexActions();
|
|
133
|
+
const post = actions[0]!;
|
|
134
|
+
const result = await post.execute({
|
|
135
|
+
...ctxBase,
|
|
136
|
+
consumedArtifacts: {},
|
|
137
|
+
config: {
|
|
138
|
+
connectionId: "conn-1",
|
|
139
|
+
roomId: "room-1",
|
|
140
|
+
markdown: "**hello**",
|
|
141
|
+
} as never,
|
|
142
|
+
});
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
if (!result.success) return;
|
|
145
|
+
expect(result.externalId).toBe("msg-42");
|
|
146
|
+
const artifact = result.artifact as { messageId: string; roomId: string };
|
|
147
|
+
expect(artifact.messageId).toBe("msg-42");
|
|
148
|
+
expect(artifact.roomId).toBe("room-1");
|
|
149
|
+
const call = fetchFixture.calls[0]!;
|
|
150
|
+
expect(call.url).toBe("https://webexapis.com/v1/messages");
|
|
151
|
+
expect(call.init.method).toBe("POST");
|
|
152
|
+
const headers = call.init.headers as Record<string, string>;
|
|
153
|
+
expect(headers.Authorization).toBe("Bearer secret-bot-token");
|
|
154
|
+
const body = JSON.parse(call.init.body as string) as {
|
|
155
|
+
roomId: string;
|
|
156
|
+
markdown: string;
|
|
157
|
+
};
|
|
158
|
+
expect(body.roomId).toBe("room-1");
|
|
159
|
+
expect(body.markdown).toBe("**hello**");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("returns failure when the connection is missing", async () => {
|
|
163
|
+
const actions = createWebexActions();
|
|
164
|
+
const post = actions[0]!;
|
|
165
|
+
const result = await post.execute({
|
|
166
|
+
...ctxBase,
|
|
167
|
+
consumedArtifacts: {},
|
|
168
|
+
config: {
|
|
169
|
+
connectionId: "missing",
|
|
170
|
+
roomId: "room-1",
|
|
171
|
+
markdown: "x",
|
|
172
|
+
} as never,
|
|
173
|
+
});
|
|
174
|
+
expect(result.success).toBe(false);
|
|
175
|
+
if (result.success) return;
|
|
176
|
+
expect(result.error).toMatch(/connection not found/i);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns failure when the Webex API responds non-OK", async () => {
|
|
180
|
+
fetchFixture = setupFetchSequence([
|
|
181
|
+
{ status: 400, raw: "bad request" },
|
|
182
|
+
]);
|
|
183
|
+
const actions = createWebexActions();
|
|
184
|
+
const post = actions[0]!;
|
|
185
|
+
const result = await post.execute({
|
|
186
|
+
...ctxBase,
|
|
187
|
+
consumedArtifacts: {},
|
|
188
|
+
config: {
|
|
189
|
+
connectionId: "conn-1",
|
|
190
|
+
roomId: "room-1",
|
|
191
|
+
markdown: "x",
|
|
192
|
+
} as never,
|
|
193
|
+
});
|
|
194
|
+
expect(result.success).toBe(false);
|
|
195
|
+
if (result.success) return;
|
|
196
|
+
expect(result.error).toMatch(/Webex API error \(400\)/);
|
|
197
|
+
expect(result.error).toMatch(/bad request/);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webex action — post a markdown message to a Webex room.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the legacy `webexProvider.deliver`. Template expansion is
|
|
5
|
+
* handled by the dispatch engine, so `config.markdown` is already the
|
|
6
|
+
* final string by the time `execute` runs.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import {
|
|
10
|
+
configString,
|
|
11
|
+
Versioned,
|
|
12
|
+
type Logger,
|
|
13
|
+
} from "@checkstack/backend-api";
|
|
14
|
+
import type { ActionDefinition } from "@checkstack/automation-backend";
|
|
15
|
+
import { connectionStoreRef } from "@checkstack/integration-backend";
|
|
16
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
WEBEX_RESOLVERS,
|
|
20
|
+
WEBEX_PROVIDER_QUALIFIED_ID,
|
|
21
|
+
type WebexConnectionConfig,
|
|
22
|
+
} from "./provider";
|
|
23
|
+
|
|
24
|
+
const WEBEX_API_BASE = "https://webexapis.com/v1";
|
|
25
|
+
|
|
26
|
+
const webexMessageDataSchema = z.object({
|
|
27
|
+
messageId: z.string(),
|
|
28
|
+
roomId: z.string(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const webexMessageArtifactType = {
|
|
32
|
+
id: "message",
|
|
33
|
+
displayName: "Webex Message",
|
|
34
|
+
description: "A message posted to a Webex room",
|
|
35
|
+
schema: webexMessageDataSchema,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const webexPostMessageConfigSchema = z.object({
|
|
39
|
+
connectionId: configString({
|
|
40
|
+
"x-options-resolver": WEBEX_RESOLVERS.CONNECTION_OPTIONS,
|
|
41
|
+
}).describe("Webex connection"),
|
|
42
|
+
roomId: configString({
|
|
43
|
+
"x-options-resolver": WEBEX_RESOLVERS.ROOM_OPTIONS,
|
|
44
|
+
"x-depends-on": ["connectionId"],
|
|
45
|
+
}).describe("Target Webex space"),
|
|
46
|
+
markdown: configString({ "x-editor-types": ["raw"] })
|
|
47
|
+
.min(1)
|
|
48
|
+
.describe("Markdown message body"),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
type WebexPostMessageConfig = z.infer<typeof webexPostMessageConfigSchema>;
|
|
52
|
+
|
|
53
|
+
async function postWebexMessage(params: {
|
|
54
|
+
botToken: string;
|
|
55
|
+
roomId: string;
|
|
56
|
+
markdown: string;
|
|
57
|
+
logger: Logger;
|
|
58
|
+
}): Promise<
|
|
59
|
+
{ success: true; messageId: string } | { success: false; error: string }
|
|
60
|
+
> {
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(`${WEBEX_API_BASE}/messages`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${params.botToken}`,
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
roomId: params.roomId,
|
|
70
|
+
markdown: params.markdown,
|
|
71
|
+
}),
|
|
72
|
+
signal: AbortSignal.timeout(10_000),
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorText = await response.text();
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: `Webex API error (${response.status}): ${errorText.slice(0, 200)}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const data = (await response.json()) as { id: string };
|
|
82
|
+
return { success: true, messageId: data.id };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createWebexActions(): ActionDefinition<unknown, unknown>[] {
|
|
89
|
+
const postMessage: ActionDefinition<
|
|
90
|
+
WebexPostMessageConfig,
|
|
91
|
+
z.infer<typeof webexMessageDataSchema>
|
|
92
|
+
> = {
|
|
93
|
+
id: "post_message",
|
|
94
|
+
displayName: "Post Webex Message",
|
|
95
|
+
description: "Send a markdown message to a Webex space",
|
|
96
|
+
category: "Webex",
|
|
97
|
+
icon: "MessageSquare",
|
|
98
|
+
connectionProviderId: WEBEX_PROVIDER_QUALIFIED_ID,
|
|
99
|
+
config: new Versioned({
|
|
100
|
+
version: 1,
|
|
101
|
+
schema: webexPostMessageConfigSchema,
|
|
102
|
+
}),
|
|
103
|
+
produces: "message",
|
|
104
|
+
// Fetch the connection store lazily here rather than baking it in
|
|
105
|
+
// at registration time: integration-backend registers
|
|
106
|
+
// `connectionStoreRef` inside its own `init()`, so the topological
|
|
107
|
+
// sort can't statically order this plugin's init after that.
|
|
108
|
+
execute: async ({ config, logger, getService }) => {
|
|
109
|
+
const connectionStore = await getService(connectionStoreRef);
|
|
110
|
+
const connection = await connectionStore.getConnectionWithCredentials(
|
|
111
|
+
config.connectionId,
|
|
112
|
+
);
|
|
113
|
+
if (!connection) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: `Webex connection not found: ${config.connectionId}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const connConfig = connection.config as WebexConnectionConfig;
|
|
120
|
+
const result = await postWebexMessage({
|
|
121
|
+
botToken: connConfig.botToken,
|
|
122
|
+
roomId: config.roomId,
|
|
123
|
+
markdown: config.markdown,
|
|
124
|
+
logger,
|
|
125
|
+
});
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
return { success: false, error: result.error };
|
|
128
|
+
}
|
|
129
|
+
logger.info(`Posted Webex message ${result.messageId}`);
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
externalId: result.messageId,
|
|
133
|
+
artifact: { messageId: result.messageId, roomId: config.roomId },
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
return [postMessage as ActionDefinition<unknown, unknown>];
|
|
138
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import { createBackendPlugin } from "@checkstack/backend-api";
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
2
2
|
import { providerExtensionPoint } from "@checkstack/integration-backend";
|
|
3
|
+
import {
|
|
4
|
+
automationActionExtensionPoint,
|
|
5
|
+
automationArtifactTypeExtensionPoint,
|
|
6
|
+
} from "@checkstack/automation-backend";
|
|
3
7
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
8
|
import { webexProvider } from "./provider";
|
|
9
|
+
import {
|
|
10
|
+
createWebexActions,
|
|
11
|
+
webexMessageArtifactType,
|
|
12
|
+
} from "./automations";
|
|
5
13
|
|
|
6
14
|
export default createBackendPlugin({
|
|
7
15
|
metadata: pluginMetadata,
|
|
8
|
-
|
|
9
16
|
register(env) {
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
env
|
|
18
|
+
.getExtensionPoint(providerExtensionPoint)
|
|
19
|
+
.addProvider(webexProvider, pluginMetadata);
|
|
20
|
+
env
|
|
21
|
+
.getExtensionPoint(automationArtifactTypeExtensionPoint)
|
|
22
|
+
.registerArtifactType(webexMessageArtifactType, pluginMetadata);
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
env.registerInit({
|
|
25
|
+
deps: {
|
|
26
|
+
logger: coreServices.logger,
|
|
27
|
+
},
|
|
28
|
+
init: async ({ logger }) => {
|
|
29
|
+
const actions = env.getExtensionPoint(automationActionExtensionPoint);
|
|
30
|
+
for (const action of createWebexActions()) {
|
|
31
|
+
actions.registerAction(action, pluginMetadata);
|
|
32
|
+
}
|
|
33
|
+
logger.debug("✅ Webex automation actions registered");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
15
36
|
},
|
|
16
37
|
});
|