Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/wsdl-soap-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@executor-js/plugin-wsdl": minor
"@executor-js/local": minor
"@executor-js/desktop": minor
"@executor-js/host-selfhost": minor
---

Add WSDL integration import and SOAP operation tools for self-contained WSDL 1.1 document/literal contracts, with schema validation, SOAP fault handling, and HTTP authentication templates.
2 changes: 2 additions & 0 deletions apps/cloud/executor.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wsdlHttpPlugin } from "@executor-js/plugin-wsdl/api";
import { defineExecutorConfig } from "@executor-js/sdk";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
Expand Down Expand Up @@ -61,6 +62,7 @@ export default defineExecutorConfig({
dangerouslyAllowStdioMCP: false,
}),
graphqlHttpPlugin(),
wsdlHttpPlugin(),
toolkitsPlugin({ activeToolkitSlug }),
workosVaultPlugin({
credentials: workosCredentials ?? { apiKey: "", clientId: "" },
Expand Down
1 change: 1 addition & 0 deletions apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/plugin-toolkits": "workspace:*",
"@executor-js/plugin-workos-vault": "workspace:*",
"@executor-js/plugin-wsdl": "workspace:*",
"@executor-js/react": "workspace:*",
"@executor-js/runtime-dynamic-worker": "workspace:*",
"@executor-js/runtime-quickjs": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"typecheck:slow": "tsc --noEmit"
},
"dependencies": {
"@executor-js/plugin-wsdl": "workspace:*",
"@sentry/bun": "^10.57.0",
"@sentry/electron": "7.19.0",
"electron-log": "^5",
Expand Down
2 changes: 2 additions & 0 deletions apps/host-cloudflare/executor.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wsdlHttpPlugin } from "@executor-js/plugin-wsdl/api";
import { defineExecutorConfig } from "@executor-js/sdk";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
Expand Down Expand Up @@ -31,6 +32,7 @@ export default defineExecutorConfig({
}),
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
graphqlHttpPlugin(),
wsdlHttpPlugin(),
toolkitsPlugin({ activeToolkitSlug }),
encryptedSecretsPlugin({ key: process.env.EXECUTOR_SECRET_KEY ?? "build-time-placeholder" }),
] as const,
Expand Down
1 change: 1 addition & 0 deletions apps/host-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/plugin-provider-service-split": "workspace:*",
"@executor-js/plugin-toolkits": "workspace:*",
"@executor-js/plugin-wsdl": "workspace:*",
"@executor-js/react": "workspace:*",
"@executor-js/runtime-dynamic-worker": "workspace:*",
"@executor-js/runtime-quickjs": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions apps/host-cloudflare/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wsdlHttpPlugin } from "@executor-js/plugin-wsdl/api";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
googleCatalog,
Expand Down Expand Up @@ -34,6 +35,7 @@ export const makeCloudflarePlugins = (
}),
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
graphqlHttpPlugin(),
wsdlHttpPlugin(),
toolkitsPlugin({ activeToolkitSlug: options.activeToolkitSlug }),
encryptedSecretsPlugin({ key: secretKey }),
] as const;
Expand Down
2 changes: 2 additions & 0 deletions apps/host-selfhost/executor.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wsdlHttpPlugin } from "@executor-js/plugin-wsdl/api";
import { defineExecutorConfig } from "@executor-js/sdk";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
Expand Down Expand Up @@ -43,6 +44,7 @@ export default defineExecutorConfig({
dangerouslyAllowStdioMCP: process.env.EXECUTOR_ALLOW_STDIO_MCP === "true",
}),
graphqlHttpPlugin(),
wsdlHttpPlugin(),
toolkitsPlugin({ activeToolkitSlug }),
// First writable secret provider -> the default for `secrets.set`.
encryptedSecretsPlugin({ key: resolveSecretKey() }),
Expand Down
1 change: 1 addition & 0 deletions apps/host-selfhost/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/plugin-provider-service-split": "workspace:*",
"@executor-js/plugin-toolkits": "workspace:*",
"@executor-js/plugin-wsdl": "workspace:*",
"@executor-js/react": "workspace:*",
"@executor-js/runtime-quickjs": "workspace:*",
"@executor-js/sdk": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions apps/local/executor.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wsdlHttpPlugin } from "@executor-js/plugin-wsdl/api";
import { defineExecutorConfig } from "@executor-js/sdk";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
Expand Down Expand Up @@ -38,6 +39,7 @@ export default defineExecutorConfig({
}),
mcpHttpPlugin({ dangerouslyAllowStdioMCP: true }),
graphqlHttpPlugin(),
wsdlHttpPlugin(),
toolkitsPlugin({ activeToolkitSlug }),
// The durable file store must register before keychain: the first
// writable provider becomes the default for minted OAuth tokens, and on
Expand Down
1 change: 1 addition & 0 deletions apps/local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/plugin-provider-service-split": "workspace:*",
"@executor-js/plugin-toolkits": "workspace:*",
"@executor-js/plugin-wsdl": "workspace:*",
"@executor-js/react": "workspace:*",
"@executor-js/runtime-quickjs": "workspace:*",
"@executor-js/sdk": "workspace:*",
Expand Down
45 changes: 45 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@executor-js/plugin-mcp": "workspace:*",
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/plugin-toolkits": "workspace:*",
"@executor-js/plugin-wsdl": "workspace:*",
"@executor-js/sdk": "workspace:*",
"@kitlangton/terminal-control": "^0.3.0",
"@modelcontextprotocol/sdk": "^1.29.0",
Expand Down
81 changes: 81 additions & 0 deletions e2e/scenarios/wsdl-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { randomBytes } from "node:crypto";
import { expect } from "@effect/vitest";
import { Effect, Exit } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { wsdlHttpPlugin } from "@executor-js/plugin-wsdl/api";
import { ordersWsdl } from "@executor-js/plugin-wsdl/testing";
import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";

const api = composePluginApi([wsdlHttpPlugin()] as const);
scenario(
"WSDL · import a contract and discover SOAP operations",
{},
Effect.gen(function* () {
const target = yield* Target;
const browser = yield* Browser;
const { client: makeClient } = yield* Api;
const identity = yield* target.newIdentity();
const client = yield* makeClient(api, identity);
const slug = IntegrationSlug.make(`orders-${randomBytes(4).toString("hex")}`);
yield* Effect.ensuring(
Effect.gen(function* () {
const rejected = yield* Effect.exit(
client.wsdl.addIntegration({
payload: {
slug,
name: "Orders",
wsdl: ordersWsdl.replace('style="document"', 'style="rpc"'),
},
}),
);
expect(Exit.isFailure(rejected), "RPC contracts must fail import").toBe(true);
yield* browser.session(identity, async ({ page, step }) => {
await step("Open the WSDL import form", async () => {
await page.goto("/integrations/add/wsdl", { waitUntil: "networkidle" });
await page.getByLabel("WSDL contract").waitFor();
});
await step("Import the Orders WSDL contract", async () => {
await page.getByLabel("Integration name").fill("Orders SOAP");
await page.getByLabel("Namespace", { exact: true }).fill(slug);
await page.getByLabel("WSDL contract").fill(ordersWsdl);
const submitted = page.waitForResponse(
(response) =>
new URL(response.url()).pathname === "/api/wsdl/integrations" &&
response.request().method() === "POST",
);
await page.getByRole("button", { name: "Add WSDL integration" }).click();
const response = await submitted;
expect(response.status(), await response.text()).toBe(200);
await page.waitForURL(`**/integrations/${slug}`, { timeout: 30_000 });
await page.getByText("Orders SOAP", { exact: true }).first().waitFor();
await page.getByText("No connections yet", { exact: true }).waitFor();
expect(await page.getByText("Orders SOAP", { exact: true }).count()).toBeGreaterThan(0);
});
});
yield* client.connections.create({
payload: {
owner: "org",
name: ConnectionName.make("main"),
integration: slug,
template: AuthTemplateSlug.make("none"),
values: {},
},
});
const tools = yield* client.tools.list({ query: {} });
expect(tools.filter((tool) => tool.integration === slug).map((tool) => tool.name)).toEqual([
"GetOrder",
]);
}),
Effect.gen(function* () {
yield* client.connections
.remove({
params: { owner: "org", integration: slug, name: ConnectionName.make("main") },
})
.pipe(Effect.ignore);
yield* client.integrations.remove({ params: { slug } }).pipe(Effect.ignore);
}),
);
}),
);
1 change: 1 addition & 0 deletions packages/plugins/wsdl/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @executor-js/plugin-wsdl
58 changes: 58 additions & 0 deletions packages/plugins/wsdl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# WSDL / SOAP plugin

Import a self-contained WSDL contract in **Add integration → WSDL / SOAP**, or
call the `wsdl.addIntegration` SDK extension / static tool. Create a connection
against the integration to expose one tool per operation. Tool calls take
`{ body: ... }`, where `body` is the selected input element's value.

```ts
import { IntegrationSlug } from "@executor-js/sdk";
import { wsdlPlugin } from "@executor-js/plugin-wsdl";

// Include wsdlPlugin() in your executor's plugins.
const addOrders = executor.wsdl.addIntegration({
slug: IntegrationSlug.make("orders"),
name: "Orders",
wsdl: contractXml,
service: "Orders", // required when service/port selection is ambiguous
port: "OrdersPort",
// endpoint: "https://example.com/soap", // optional override
});
```

The SDK extension returns an Effect; compose it with your application's Effect
runtime to execute it. The HTTP variant is `wsdlHttpPlugin`
from `@executor-js/plugin-wsdl/api`, with `POST /wsdl/integrations`.

## Supported profile

- WSDL 1.1, SOAP 1.1 with UTF-8 XML over HTTP(S), document/literal request-response operations.
- One element body part per input/output; wrapped and bare bodies retain their
declared XML shape. Service and port selection use their local names.
- Inline XSD schemas, global element references, named or inline complex types
with sequences, optional/repeated elements, and nillable values.
- XSD `string`, `boolean`, `int`, `integer`, and `decimal`. Unbounded integers
and decimals use validated JSON strings, preserving precision.
- SOAP faults become tool failures, including on HTTP error statuses.
- SDK/API registration accepts shared `authenticationTemplate` entries of kind
`apikey` (header/query placements) or `none`. Credentials come from Executor
connections. The initial import form creates an unauthenticated integration.

Unsupported contracts fail import: external WSDL/XSD imports/includes, WSDL 2,
SOAP 1.2, RPC/encoded bindings, attachments, SOAP header bindings, WS-Security,
recursive types, and XSD constructs outside the profile above. Fault details
are not decoded into typed values. No automatic SOAP calls are used as health
checks, and all operations require approval by default.

XML rejects DTD/entity declarations, limits input/response size to 2 MB, depth
to 64, and elements to 50,000. Expanded schemas are limited to 10,000 elements
and 32 levels; repeated fields permit at most 10,000 values. HTTP calls use the
host's injected Effect HttpClient, a 110-second deadline, and no automatic retry.

## Development

Run `bun run typecheck` and `bun run test` from this package. The cross-target
browser scenario is `e2e/scenarios/wsdl-integration.test.ts`.

Protocol references: [WSDL 1.1](https://www.w3.org/TR/wsdl.html) and
[SOAP 1.1](https://www.w3.org/TR/2000/NOTE-SOAP-20000508/).
Loading
Loading