diff --git a/.changeset/connector-descriptors-meet-their-contract.md b/.changeset/connector-descriptors-meet-their-contract.md new file mode 100644 index 0000000000..9832597c54 --- /dev/null +++ b/.changeset/connector-descriptors-meet-their-contract.md @@ -0,0 +1,62 @@ +--- +"@objectstack/spec": minor +"@objectstack/service-automation": patch +"@objectstack/runtime": patch +--- + +fix(spec,runtime,service-automation): `IAutomationService` declares the connector registry it already serves (#4127) + +The fourth and last of the dispatcher call sites #4127 found calling a method its +contract never declared. The first three shipped in #4143; this one was held back +because the fix is a **type move**, not a type addition — `ConnectorDescriptor` +was declared in `@objectstack/service-automation`'s engine, which is one +*implementation* of `IAutomationService`. A contract cannot name a type that +lives inside its own implementation, so `getConnectorDescriptors` could not be +declared at all until the type had a home in the spec. + +**`IAutomationService` += `getConnectorDescriptors?()`.** It is the sibling of +`getActionDescriptors`, which the contract has declared since ADR-0018: the two +fill the flow designer's `connector_action` node together — node vocabulary from +one, the connector → action → input pickers from the other. Only one of them was +written down. `GET /api/v1/automation/connectors` has served the other since +ADR-0022 by probing for the method and then re-typing its own result as `any` to +filter on `?type=`, which is a filter on a field the type system did not know +existed — one typo from silently matching nothing and answering an empty +registry, which is also what this route legitimately returns when the method is +absent, so the failure had no distinguishable symptom. + +Optional for the same reason `getActionDescriptors` is: a connector registry is a +capability of the flow-engine implementation, not a property of every automation +slot. A script-runner filling the slot has no connectors to describe, and the +route answers an empty registry rather than a 404 — the `handlerReady` posture +does not apply, since the slot is serveable and only this capability is absent. + +**`ConnectorDescriptor` / `ConnectorActionDescriptor` / `ConnectorOrigin` / +`ConnectorState` move to `@objectstack/spec/integration`**, beside the ADR-0097 +provider contract, for the reason that file already states about itself: they are +pure types, so a connector plugin — or a designer client, or the dispatcher — +speaks about registered connectors depending only on the spec, with no runtime +coupling to the engine. `ConnectorOrigin` is ADR-0097 §4 vocabulary and +`ConnectorState` is #3017 vocabulary; neither was ever engine-private in meaning, +only in location. + +Nothing is renamed and no shape changes. `@objectstack/service-automation` +imports the four back and re-exports them from its index — the same names, from +the same entry point — so every existing importer compiles unchanged. +`ConnectorState` joins that re-export, which it should have been in all along: it +is a required field of the descriptor the index has always exported. + +**The test fixture had already drifted, which is the concrete cost.** The +dispatcher's connector mock declared `{ name, label, type, actions }` and omitted +`origin` and `state` — both **required** on `ConnectorDescriptor`, and both the +fields a designer reads to tell a live declarative instance from a plugin one +(ADR-0097 §4), or a dispatchable connector from a degraded one that is listed +honestly rather than hidden (#3017). Nothing caught it, because an undeclared +return type cannot be checked against. The fixture is typed now, so it cannot +drift again, and a new test pins that `origin` / `state` / `degradedReason` +survive the hop through the route rather than only `name` and `type`. + +Verified: `@objectstack/spec` **7089 tests / 272 files** (2 new contract tests), +`@objectstack/service-automation` **457 / 41**, `@objectstack/runtime` +**218 http-dispatcher tests** (1 new), `tsc --noEmit`, `pnpm lint`, the liveness +and empty-state gates, and the three generated-artifact gates — all clean. diff --git a/packages/runtime/src/domains/automation.ts b/packages/runtime/src/domains/automation.ts index 0cebecb5a6..3406d46dc2 100644 --- a/packages/runtime/src/domains/automation.ts +++ b/packages/runtime/src/domains/automation.ts @@ -192,11 +192,16 @@ export async function handleAutomationRequest(deps: DomainHandlerDeps, path: str // empty in baseline and populated by connector plugins (e.g. // @objectstack/connector-rest, @objectstack/connector-slack). if (parts[0] === 'connectors' && parts.length === 1 && m === 'GET') { - if (typeof automationService.getConnectorDescriptors === 'function') { - let connectors = automationService.getConnectorDescriptors() ?? []; + // [#4127] The method is declared on IAutomationService now, so the + // `?type=` filter reads `ConnectorDescriptor['type']` instead of + // re-typing each element as `any` — a filter on a field the contract + // did not know existed was a typo away from silently matching nothing. + const svc = automationService as Pick; + if (typeof svc.getConnectorDescriptors === 'function') { + let connectors = svc.getConnectorDescriptors() ?? []; // Optional filter mirrors the descriptor's connector type. if (query?.type) { - connectors = connectors.filter((c: any) => c?.type === query.type); + connectors = connectors.filter((c) => c?.type === query.type); } return { handled: true, response: deps.success({ connectors, total: connectors.length }) }; } diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index 0ccfae8c18..c46e00d211 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { HttpDispatcher } from './http-dispatcher.js'; import { ObjectKernel } from '@objectstack/core'; import { ApiErrorSchema } from '@objectstack/spec/api'; +import type { ConnectorDescriptor } from '@objectstack/spec/integration'; describe('HttpDispatcher', () => { let kernel: ObjectKernel; @@ -179,11 +180,18 @@ describe('HttpDispatcher', () => { { type: 'http_request', name: 'HTTP Request', category: 'io', paradigms: ['flow', 'approval'], source: 'builtin' }, { type: 'send_sms', name: 'Send SMS', category: 'io', paradigms: ['flow'], source: 'plugin' }, ]), + // [#4127] Typed as `ConnectorDescriptor[]` now that the contract + // declares `getConnectorDescriptors`, so this fixture cannot + // drift from the shape the route serves. The previous untyped + // literal was already missing `origin` and `state` — both + // REQUIRED, and both the fields a designer reads to tell a + // declarative instance from a plugin one, or a degraded + // connector from a live one (#3017). getConnectorDescriptors: vi.fn().mockReturnValue([ - { name: 'rest', label: 'REST', type: 'api', actions: [{ key: 'request', label: 'Request' }] }, - { name: 'slack', label: 'Slack', type: 'api', actions: [{ key: 'chat.postMessage', label: 'Post Message' }] }, - { name: 'pg', label: 'Postgres', type: 'database', actions: [] }, - ]), + { name: 'rest', label: 'REST', type: 'api', origin: 'plugin', state: 'ready', actions: [{ key: 'request', label: 'Request' }] }, + { name: 'slack', label: 'Slack', type: 'api', origin: 'plugin', state: 'ready', actions: [{ key: 'chat.postMessage', label: 'Post Message' }] }, + { name: 'pg', label: 'Postgres', type: 'database', origin: 'declarative', state: 'degraded', degradedReason: 'upstream unreachable', actions: [] }, + ] satisfies ConnectorDescriptor[]), getFlowRuntimeStates: vi.fn().mockReturnValue([ { name: 'flow_a', enabled: true, bound: true }, { name: 'flow_b', enabled: false, bound: false }, @@ -510,6 +518,25 @@ describe('HttpDispatcher', () => { expect(result.response?.body?.data?.connectors[0].name).toBe('pg'); }); + // [#4127] The route serves the WHOLE descriptor. `origin` and `state` + // are what the designer reads to distinguish a live declarative + // instance from a plugin connector, and a dispatchable one from a + // degraded one (ADR-0097 §4, #3017) — while the contract did not + // declare the method, nothing pinned that they survive the hop. + it('should preserve origin / state / degradedReason on GET /connectors', async () => { + const result = await dispatcher.handleAutomation('connectors', 'GET', {}, { request: {} }); + expect(result.handled).toBe(true); + const byName = Object.fromEntries( + result.response?.body?.data?.connectors.map((c: ConnectorDescriptor) => [c.name, c]), + ); + expect(byName.rest).toMatchObject({ origin: 'plugin', state: 'ready' }); + expect(byName.pg).toMatchObject({ + origin: 'declarative', + state: 'degraded', + degradedReason: 'upstream unreachable', + }); + }); + it('should return an empty registry when the service lacks getConnectorDescriptors', async () => { delete mockAutomationService.getConnectorDescriptors; const result = await dispatcher.handleAutomation('connectors', 'GET', {}, { request: {} }); diff --git a/packages/services/service-automation/src/engine.ts b/packages/services/service-automation/src/engine.ts index 12cf222c49..7f80bd2004 100644 --- a/packages/services/service-automation/src/engine.ts +++ b/packages/services/service-automation/src/engine.ts @@ -9,7 +9,13 @@ import { FlowSchema, FLOW_STRUCTURAL_NODE_TYPES, validateControlFlow, findRegion import { resolveFlowNodeExpressions } from '@objectstack/spec/automation'; import { applyConversionsToFlow } from '@objectstack/spec'; import type { FlowRegionParsed } from '@objectstack/spec/automation'; -import type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration'; +import type { + Connector, + ConnectorProviderFactory, + ConnectorOrigin, + ConnectorState, + ConnectorDescriptor, +} from '@objectstack/spec/integration'; import { ConnectorSchema } from '@objectstack/spec/integration'; // Static import (not a lazy `require`): the engine ships as ESM ("type":"module"), // where a CommonJS `require('@objectstack/formula')` resolves to tsup's throwing @@ -210,25 +216,15 @@ export type ConnectorActionHandler = ( ctx: ConnectorActionContext, ) => Promise>; -/** - * How a registered connector reached the engine (ADR-0097 §4). `plugin` — a - * connector plugin called `registerConnector` directly (ADR-0018 §Addendum). - * `declarative` — the automation service materialized a provider-bound - * `connectors:` stack entry at boot. A name registered under one origin cannot - * be re-registered under the other: that two-sources-of-truth collision is a - * hard error, not a silent replace. - */ -export type ConnectorOrigin = 'plugin' | 'declarative'; - -/** - * Whether a registered connector is dispatchable (#3017). `ready` — the normal - * state: actions and handlers are live. `degraded` — a declarative instance - * whose provider factory could not reach its upstream (e.g. an MCP server was - * unreachable at boot): it is registered so `GET /connectors` shows it honestly - * instead of it silently missing, but it exposes no actions and every dispatch - * fails with a clear error until the materializer's retry succeeds. - */ -export type ConnectorState = 'ready' | 'degraded'; +// `ConnectorOrigin` / `ConnectorState` / `ConnectorDescriptor` / +// `ConnectorActionDescriptor` are declared in `@objectstack/spec/integration` +// (imported above, re-exported from this package's index). [#4127] They used to +// be declared HERE — which put the return type of `IAutomationService`'s +// `getConnectorDescriptors` inside one implementation of that contract, so the +// contract could not name the method and the dispatcher route serving it had to +// duck-type it. Same reason the provider contract lives in the spec: a +// connector plugin or a designer client speaks about registered connectors +// without importing this engine. /** * A connector registered on the engine: its validated {@link Connector} @@ -348,51 +344,6 @@ export type FlowRecordExpander = ( runContext: AutomationContext, ) => Promise | undefined> | Record | undefined; -/** - * A designer-facing view of one connector action — identity + its JSON-Schema - * input/output. The runtime handler is intentionally omitted; this is metadata. - */ -export interface ConnectorActionDescriptor { - readonly key: string; - readonly label: string; - readonly description?: string; - readonly inputSchema?: Record; - readonly outputSchema?: Record; -} - -/** - * A designer-facing descriptor for a registered connector: its identity plus - * the actions it exposes. Served by `GET /api/v1/automation/connectors` so the - * flow designer can populate the `connector_action` node's connector → action - * → input pickers (ADR-0018 §Addendum, ADR-0022). Mirrors `ActionDescriptor`'s - * role for node types, but for the connector registry. - */ -export interface ConnectorDescriptor { - readonly name: string; - readonly label: string; - readonly type: string; - readonly description?: string; - readonly icon?: string; - readonly actions: ConnectorActionDescriptor[]; - /** - * How the connector reached the registry (ADR-0097 §4): `plugin` — registered - * by a connector plugin via `registerConnector`; `declarative` — materialized - * from a provider-bound `connectors:` stack entry at boot. Lets a designer - * distinguish a live declarative instance from a plugin connector (and both - * from an inert catalog descriptor, which never reaches this list). - */ - readonly origin: ConnectorOrigin; - /** - * Dispatchability (#3017): `ready` — actions are live; `degraded` — the - * instance's upstream was unreachable when the provider factory ran, so it - * currently exposes no actions and cannot dispatch. The platform retries - * degraded instances automatically; `degradedReason` says what failed. - */ - readonly state: ConnectorState; - /** Why the connector is degraded — present only when `state` is `degraded`. */ - readonly degradedReason?: string; -} - // ─── Core Automation Engine ───────────────────────────────────────── /** diff --git a/packages/services/service-automation/src/index.ts b/packages/services/service-automation/src/index.ts index f93c537dc4..0c29588970 100644 --- a/packages/services/service-automation/src/index.ts +++ b/packages/services/service-automation/src/index.ts @@ -11,23 +11,31 @@ export type { ConnectorActionHandler, ConnectorActionContext, RegisteredConnector, - ConnectorOrigin, - ConnectorDescriptor, - ConnectorActionDescriptor, SuspendedRun, SuspendedRunStore, RunRecord, StepLogEntry, } from './engine.js'; -// Connector provider contract (ADR-0097) — re-exported from @objectstack/spec so -// hosts/tests can reach it via this package too. Connector plugins should import -// it directly from `@objectstack/spec/integration` (no coupling to this engine). +// Connector provider contract (ADR-0097) and the registry vocabulary that goes +// with it — re-exported from @objectstack/spec so hosts/tests can reach them via +// this package too. Connector plugins should import them directly from +// `@objectstack/spec/integration` (no coupling to this engine). +// +// [#4127] The descriptor types moved to the spec: `ConnectorDescriptor` is the +// return type of `IAutomationService.getConnectorDescriptors`, so declaring it +// here left the contract unable to name its own method. Same names, same +// shapes — this re-export keeps `@objectstack/service-automation` importers +// working unchanged. export type { ConnectorProviderFactory, ConnectorProviderContext, ConnectorMaterialization, ConnectorMaterializationHandler, + ConnectorOrigin, + ConnectorState, + ConnectorDescriptor, + ConnectorActionDescriptor, } from '@objectstack/spec/integration'; // Durable suspended-run persistence (ADR-0019). The in-memory store is the diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index b9b8ecc407..06a14958a8 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -3799,7 +3799,9 @@ "ConflictResolution (type)", "ConflictResolutionSchema (const)", "Connector (type)", + "ConnectorActionDescriptor (interface)", "ConnectorActionSchema (const)", + "ConnectorDescriptor (interface)", "ConnectorErrorCategory (type)", "ConnectorErrorCategorySchema (const)", "ConnectorHealth (type)", @@ -3813,11 +3815,13 @@ "ConnectorInstanceNoAuthSchema (const)", "ConnectorMaterialization (interface)", "ConnectorMaterializationHandler (type)", + "ConnectorOrigin (type)", "ConnectorProviderContext (interface)", "ConnectorProviderFactory (type)", "ConnectorRetryStrategy (type)", "ConnectorRetryStrategySchema (const)", "ConnectorSchema (const)", + "ConnectorState (type)", "ConnectorStatus (type)", "ConnectorStatusSchema (const)", "ConnectorTriggerSchema (const)", diff --git a/packages/spec/src/contracts/automation-service.test.ts b/packages/spec/src/contracts/automation-service.test.ts index 59be450706..92aaf7d598 100644 --- a/packages/spec/src/contracts/automation-service.test.ts +++ b/packages/spec/src/contracts/automation-service.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import type { IAutomationService, AutomationResult } from './automation-service'; import type { FlowParsed } from '../automation/flow.zod'; import type { ExecutionLog } from '../automation/execution.zod'; +import type { ConnectorDescriptor } from '../integration/connector-descriptor'; describe('Automation Service Contract', () => { it('should allow a minimal IAutomationService implementation with required methods', () => { @@ -167,4 +168,57 @@ describe('Automation Service Contract', () => { await service.toggleFlow!('test_flow', true); expect(flowEnabled).toBe(true); }); + + // [#4127] `getConnectorDescriptors` is the sibling of `getActionDescriptors` + // — the other half of the flow designer's `connector_action` pickers — and + // was the last of the four dispatcher routes calling a method the contract + // did not declare. Typing the return value here is the assertion: the + // descriptor must carry `origin` and `state`, so a shape that omits them no + // longer compiles anywhere the contract is honoured. + it('should return typed ConnectorDescriptor[] from getConnectorDescriptors', () => { + const service: IAutomationService = { + execute: async () => ({ success: true }), + listFlows: async () => [], + getConnectorDescriptors: (): ConnectorDescriptor[] => [ + { + name: 'billing', + label: 'Billing', + type: 'api', + origin: 'declarative', + state: 'ready', + actions: [{ key: 'request', label: 'Request', inputSchema: { type: 'object' } }], + }, + { + name: 'gh_mcp', + label: 'GitHub MCP', + type: 'api', + origin: 'declarative', + state: 'degraded', + degradedReason: 'MCP server unreachable at boot', + actions: [], + }, + ], + }; + + const descriptors = service.getConnectorDescriptors!(); + expect(descriptors.map((d) => d.name)).toEqual(['billing', 'gh_mcp']); + expect(descriptors[0].actions[0].key).toBe('request'); + // A degraded instance is listed rather than hidden (#3017), exposes no + // actions, and says why. + expect(descriptors[1].state).toBe('degraded'); + expect(descriptors[1].actions).toEqual([]); + expect(descriptors[1].degradedReason).toBe('MCP server unreachable at boot'); + }); + + // The registry is a flow-engine capability, not a property of every + // automation slot: a script-runner implementation legitimately omits it, and + // `GET /automation/connectors` answers an empty registry rather than 404. + it('should allow an implementation that omits getConnectorDescriptors', () => { + const service: IAutomationService = { + execute: async () => ({ success: true }), + listFlows: async () => [], + }; + + expect(service.getConnectorDescriptors).toBeUndefined(); + }); }); diff --git a/packages/spec/src/contracts/automation-service.ts b/packages/spec/src/contracts/automation-service.ts index fb3c2bd1d0..170d1858b5 100644 --- a/packages/spec/src/contracts/automation-service.ts +++ b/packages/spec/src/contracts/automation-service.ts @@ -16,6 +16,7 @@ import type { FlowParsed } from '../automation/flow.zod'; import type { ExecutionLog } from '../automation/execution.zod'; import type { ActionDescriptor } from '../automation/node-executor.zod'; +import type { ConnectorDescriptor } from '../integration/connector-descriptor'; /** * Context passed to a flow/script execution @@ -346,6 +347,25 @@ export interface IAutomationService { */ getActionDescriptors?(): ActionDescriptor[]; + /** + * The connector registry, as designer-facing descriptors (ADR-0022). + * + * [#4127] Declared because `GET /automation/connectors` already called it — + * the sibling of {@link getActionDescriptors}, which the contract HAS + * declared since ADR-0018, serving the same designer with the other half of + * the `connector_action` node's pickers (node type ← actions, connector / + * action / input ← this). Undeclared, the route had to probe for the method + * and then re-type its own result as `any` to filter on `type`. + * + * Optional for the same reason `getActionDescriptors` is: a connector + * registry is a capability of the flow-engine implementation, not of every + * automation slot — a script-runner implementation of this contract has no + * connectors to describe, and answers an empty registry rather than 404. + * + * @returns One entry per registered connector; empty when none are registered + */ + getConnectorDescriptors?(): ConnectorDescriptor[]; + /** * Per-flow deployment + binding state, for operator surfaces. * diff --git a/packages/spec/src/integration/connector-descriptor.ts b/packages/spec/src/integration/connector-descriptor.ts new file mode 100644 index 0000000000..b18e109588 --- /dev/null +++ b/packages/spec/src/integration/connector-descriptor.ts @@ -0,0 +1,90 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Connector **registry** vocabulary — how a connector reached the registry, + * whether it can dispatch, and the designer-facing view of it that + * `GET /api/v1/automation/connectors` serves (ADR-0022, ADR-0097 §4, #3017). + * + * These sit beside the provider contract (`connector-provider.ts`) for the same + * reason it lives here: they are pure types (no logic — Prime Directive #2), so + * a connector plugin, a designer client, or the HTTP dispatcher can speak about + * registered connectors depending only on `@objectstack/spec`, with no runtime + * coupling to `@objectstack/service-automation`. + * + * [#4127] They were declared in the engine, which put the return type of a + * *service-contract* method inside one implementation of that contract — so the + * contract could not name it, `IAutomationService` never declared + * `getConnectorDescriptors`, and the dispatcher route that serves it had to + * duck-type the method and then re-type the result as `any` to filter on + * `type`. The declaration moved here; the engine imports it back. + */ + +/** + * How a registered connector reached the engine (ADR-0097 §4). `plugin` — a + * connector plugin called `registerConnector` directly (ADR-0018 §Addendum). + * `declarative` — the automation service materialized a provider-bound + * `connectors:` stack entry at boot. A name registered under one origin cannot + * be re-registered under the other: that two-sources-of-truth collision is a + * hard error, not a silent replace. + */ +export type ConnectorOrigin = 'plugin' | 'declarative'; + +/** + * Whether a registered connector is dispatchable (#3017). `ready` — the normal + * state: actions and handlers are live. `degraded` — a declarative instance + * whose provider factory could not reach its upstream (e.g. an MCP server was + * unreachable at boot): it is registered so `GET /connectors` shows it honestly + * instead of it silently missing, but it exposes no actions and every dispatch + * fails with a clear error until the materializer's retry succeeds. + */ +export type ConnectorState = 'ready' | 'degraded'; + +/** + * A designer-facing view of one connector action — identity + its JSON-Schema + * input/output. The runtime handler is intentionally omitted; this is metadata. + */ +export interface ConnectorActionDescriptor { + readonly key: string; + readonly label: string; + readonly description?: string; + readonly inputSchema?: Record; + readonly outputSchema?: Record; +} + +/** + * A designer-facing descriptor for a registered connector: its identity plus + * the actions it exposes. Served by `GET /api/v1/automation/connectors` so the + * flow designer can populate the `connector_action` node's connector → action + * → input pickers (ADR-0018 §Addendum, ADR-0022). Mirrors `ActionDescriptor`'s + * role for node types, but for the connector registry. + */ +export interface ConnectorDescriptor { + readonly name: string; + readonly label: string; + /** + * The connector's category (`saas`, `database`, `rest`, …) — the same `type` + * the authorable {@link Connector} declares. `GET /connectors?type=` filters + * on it. + */ + readonly type: string; + readonly description?: string; + readonly icon?: string; + readonly actions: ConnectorActionDescriptor[]; + /** + * How the connector reached the registry (ADR-0097 §4): `plugin` — registered + * by a connector plugin via `registerConnector`; `declarative` — materialized + * from a provider-bound `connectors:` stack entry at boot. Lets a designer + * distinguish a live declarative instance from a plugin connector (and both + * from an inert catalog descriptor, which never reaches this list). + */ + readonly origin: ConnectorOrigin; + /** + * Dispatchability (#3017): `ready` — actions are live; `degraded` — the + * instance's upstream was unreachable when the provider factory ran, so it + * currently exposes no actions and cannot dispatch. The platform retries + * degraded instances automatically; `degradedReason` says what failed. + */ + readonly state: ConnectorState; + /** Why the connector is degraded — present only when `state` is `degraded`. */ + readonly degradedReason?: string; +} diff --git a/packages/spec/src/integration/index.ts b/packages/spec/src/integration/index.ts index e0a83a0efe..e6d75ceaf3 100644 --- a/packages/spec/src/integration/index.ts +++ b/packages/spec/src/integration/index.ts @@ -19,6 +19,10 @@ export * from './connector.zod'; export * from './connector-provider'; export * from './connector-provider-errors'; +// Connector registry vocabulary — origin/state and the descriptor +// `GET /automation/connectors` serves (ADR-0022, ADR-0097 §4, #3017) +export * from './connector-descriptor'; + // Connector Templates export * from './connector/saas.zod'; export * from './connector/database.zod';