import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { Schema } from "effect";import { ApiKeyAuthMethod, ApiKeyAuthTemplate } from "@executor-js/sdk/http-auth";import { HealthCheckSpec, InternalError, IntegrationAlreadyExistsError, IntegrationNotFoundError, IntegrationSlug, OrgWriteDeniedError,} from "@executor-js/sdk/shared"; import { OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, OpenApiSpecOverrideError,} from "../sdk/errors";import { SpecPreviewSummary } from "../sdk/preview";import { SpecOverridesSchema } from "../sdk/spec-overrides"; // ---------------------------------------------------------------------------// Errors — the plugin-domain tagged errors flow directly to clients// (4xx, each carrying its own `httpApiStatus`). `InternalError` is the shared// opaque 500 surface; `StorageError` → `InternalError` translation happens at// service wiring time. `IntegrationAlreadyExistsError` (409) blocks re-adding// an existing slug — see addSpec.// --------------------------------------------------------------------------- const DomainErrors = [ InternalError, OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, OpenApiSpecOverrideError, IntegrationAlreadyExistsError, OrgWriteDeniedError,] as const; const IntegrationNotFound = IntegrationNotFoundError.annotate({ httpApiStatus: 404 }); const UpdateSpecErrors = [ InternalError, OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, OpenApiSpecOverrideError, IntegrationNotFound, OrgWriteDeniedError,] as const; const SlugParams = { slug: Schema.String,}; // ---------------------------------------------------------------------------// Payloads// --------------------------------------------------------------------------- const OpenApiSpecInputPayload = Schema.Union([ Schema.Struct({ kind: Schema.Literal("url"), url: Schema.String }), Schema.Struct({ kind: Schema.Literal("blob"), value: Schema.String }),]); const OAuthTemplatePayload = Schema.Struct({ slug: Schema.String, kind: Schema.Literal("oauth2"), label: Schema.optional(Schema.String), authorizationUrl: Schema.String, tokenUrl: Schema.String, resource: Schema.optional(Schema.NullOr(Schema.String)), scopes: Schema.Array(Schema.String), supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),}); /** Auth INPUTS: oauth templates + the request-shaped apikey dialect. */const AuthenticationPayload = Schema.Union([OAuthTemplatePayload, ApiKeyAuthTemplate]); /** Auth in RESPONSES: the canonical stored shapes (placements). */const AuthenticationResponse = Schema.Union([OAuthTemplatePayload, ApiKeyAuthMethod]); const AddSpecPayload = Schema.Struct({ spec: OpenApiSpecInputPayload, slug: Schema.optional(Schema.String), name: Schema.optional(Schema.String), description: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), /** The product's domain when the caller knew it (a registry row names * notion.com) — display identity when the spec lives on a code host. */ displayDomain: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), specFormat: Schema.optional(Schema.String), family: Schema.optional(Schema.String), healthCheck: Schema.optional(HealthCheckSpec), authenticationTemplate: Schema.optional(Schema.Array(AuthenticationPayload)), specOverrides: Schema.optional(SpecOverridesSchema),}); const PreviewSpecPayload = Schema.Struct({ spec: Schema.String, specFormat: Schema.optional(Schema.String), specOverrides: Schema.optional(SpecOverridesSchema),}); // The `configure` payload — the new/updated auth methods to merge onto the// integration's `authenticationTemplate`, plus request routing metadata.// Reuses the same `AuthenticationPayload` schema as `addSpec` so a custom apiKey// method round-trips identically.const ConfigurePayload = Schema.Struct({ authenticationTemplate: Schema.optional(Schema.Array(AuthenticationPayload)), mode: Schema.optional(Schema.Literals(["merge", "replace"])), baseUrl: Schema.optional(Schema.String),}); // ---------------------------------------------------------------------------// Responses// --------------------------------------------------------------------------- const AddSpecResponse = Schema.Struct({ slug: IntegrationSlug, toolCount: Schema.Number,}); // Update the spec in place. Body optional fields only: an empty payload means// "re-fetch from the stored source URL".const UpdateSpecPayload = Schema.Struct({ spec: Schema.optional(OpenApiSpecInputPayload), specOverrides: Schema.optional(SpecOverridesSchema),}); const UpdateSpecResponse = Schema.Struct({ slug: IntegrationSlug, toolCount: Schema.Number, /** Tool names new in this spec version (same diff for every connection). */ addedTools: Schema.Array(Schema.String), /** Tool names the new spec no longer defines. */ removedTools: Schema.Array(Schema.String),}); const IntegrationView = Schema.Struct({ slug: IntegrationSlug, description: Schema.String, kind: Schema.String, canRemove: Schema.Boolean, canRefresh: Schema.Boolean,}); // The integration config surfaced for the configure UX. Unlike// `IntegrationView` (catalog identity only), this carries the// `authenticationTemplate` the configure flow reads/writes. The spec text is// deliberately NOT served: it's a multi-MB build artifact in the plugin blob// store, and no client reads it (the configure UI only touches the template).const OpenApiConfigView = Schema.Struct({ specUrl: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), specOverrides: Schema.optional(SpecOverridesSchema), authenticationTemplate: Schema.optional(Schema.Array(AuthenticationResponse)),}); // The configure result — the merged `authenticationTemplate` after the new// custom methods were appended/replaced.const ConfigureResponse = Schema.Struct({ authenticationTemplate: Schema.Array(AuthenticationResponse),}); // ---------------------------------------------------------------------------// Group — addSpec/preview/get/remove over the integration catalog.// --------------------------------------------------------------------------- export const OpenApiGroup = HttpApiGroup.make("openapi") .add( HttpApiEndpoint.post("previewSpec", "/openapi/preview", { payload: PreviewSpecPayload, success: SpecPreviewSummary, error: DomainErrors, }), ) .add( HttpApiEndpoint.post("addSpec", "/openapi/specs", { payload: AddSpecPayload, success: AddSpecResponse, error: DomainErrors, }), ) .add( HttpApiEndpoint.get("getIntegration", "/openapi/integrations/:slug", { params: SlugParams, success: Schema.NullOr(IntegrationView), error: DomainErrors, }), ) .add( HttpApiEndpoint.get("getConfig", "/openapi/integrations/:slug/config", { params: SlugParams, success: Schema.NullOr(OpenApiConfigView), error: DomainErrors, }), ) .add( HttpApiEndpoint.post("configure", "/openapi/integrations/:slug/config", { params: SlugParams, payload: ConfigurePayload, success: ConfigureResponse, error: DomainErrors, }), ) .add( HttpApiEndpoint.post("updateSpec", "/openapi/integrations/:slug/spec", { params: SlugParams, payload: UpdateSpecPayload, success: UpdateSpecResponse, error: UpdateSpecErrors, }), ) .add( HttpApiEndpoint.delete("removeSpec", "/openapi/integrations/:slug", { params: SlugParams, success: Schema.Void, error: DomainErrors, }), );
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
import { Schema } from "effect";
import { ApiKeyAuthMethod, ApiKeyAuthTemplate } from "@executor-js/sdk/http-auth";
import {
HealthCheckSpec,
InternalError,
IntegrationAlreadyExistsError,
IntegrationNotFoundError,
IntegrationSlug,
OrgWriteDeniedError,
} from "@executor-js/sdk/shared";
import {
OpenApiParseError,
OpenApiExtractionError,
OpenApiOAuthError,
OpenApiSpecOverrideError,
} from "../sdk/errors";
import { SpecPreviewSummary } from "../sdk/preview";
import { SpecOverridesSchema } from "../sdk/spec-overrides";
// ---------------------------------------------------------------------------
// Errors — the plugin-domain tagged errors flow directly to clients
// (4xx, each carrying its own `httpApiStatus`). `InternalError` is the shared
// opaque 500 surface; `StorageError` → `InternalError` translation happens at
// service wiring time. `IntegrationAlreadyExistsError` (409) blocks re-adding
// an existing slug — see addSpec.
// ---------------------------------------------------------------------------
const DomainErrors = [
InternalError,
OpenApiParseError,
OpenApiExtractionError,
OpenApiOAuthError,
OpenApiSpecOverrideError,
IntegrationAlreadyExistsError,
OrgWriteDeniedError,
] as const;
const IntegrationNotFound = IntegrationNotFoundError.annotate({ httpApiStatus: 404 });
const UpdateSpecErrors = [
InternalError,
OpenApiParseError,
OpenApiExtractionError,
OpenApiOAuthError,
OpenApiSpecOverrideError,
IntegrationNotFound,
OrgWriteDeniedError,
] as const;
const SlugParams = {
slug: Schema.String,
};
// ---------------------------------------------------------------------------
// Payloads
// ---------------------------------------------------------------------------
const OpenApiSpecInputPayload = Schema.Union([
Schema.Struct({ kind: Schema.Literal("url"), url: Schema.String }),
Schema.Struct({ kind: Schema.Literal("blob"), value: Schema.String }),
]);
const OAuthTemplatePayload = Schema.Struct({
slug: Schema.String,
kind: Schema.Literal("oauth2"),
label: Schema.optional(Schema.String),
authorizationUrl: Schema.String,
tokenUrl: Schema.String,
resource: Schema.optional(Schema.NullOr(Schema.String)),
scopes: Schema.Array(Schema.String),
supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),
});
/** Auth INPUTS: oauth templates + the request-shaped apikey dialect. */
const AuthenticationPayload = Schema.Union([OAuthTemplatePayload, ApiKeyAuthTemplate]);
/** Auth in RESPONSES: the canonical stored shapes (placements). */
const AuthenticationResponse = Schema.Union([OAuthTemplatePayload, ApiKeyAuthMethod]);
const AddSpecPayload = Schema.Struct({
spec: OpenApiSpecInputPayload,
slug: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
description: Schema.optional(Schema.String),
baseUrl: Schema.optional(Schema.String),
/** The product's domain when the caller knew it (a registry row names
* notion.com) — display identity when the spec lives on a code host. */
displayDomain: Schema.optional(Schema.String),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
specFormat: Schema.optional(Schema.String),
family: Schema.optional(Schema.String),
healthCheck: Schema.optional(HealthCheckSpec),
authenticationTemplate: Schema.optional(Schema.Array(AuthenticationPayload)),
specOverrides: Schema.optional(SpecOverridesSchema),
});
const PreviewSpecPayload = Schema.Struct({
spec: Schema.String,
specFormat: Schema.optional(Schema.String),
specOverrides: Schema.optional(SpecOverridesSchema),
});
// The `configure` payload — the new/updated auth methods to merge onto the
// integration's `authenticationTemplate`, plus request routing metadata.
// Reuses the same `AuthenticationPayload` schema as `addSpec` so a custom apiKey
// method round-trips identically.
const ConfigurePayload = Schema.Struct({
authenticationTemplate: Schema.optional(Schema.Array(AuthenticationPayload)),
mode: Schema.optional(Schema.Literals(["merge", "replace"])),
baseUrl: Schema.optional(Schema.String),
});
// ---------------------------------------------------------------------------
// Responses
// ---------------------------------------------------------------------------
const AddSpecResponse = Schema.Struct({
slug: IntegrationSlug,
toolCount: Schema.Number,
});
// Update the spec in place. Body optional fields only: an empty payload means
// "re-fetch from the stored source URL".
const UpdateSpecPayload = Schema.Struct({
spec: Schema.optional(OpenApiSpecInputPayload),
specOverrides: Schema.optional(SpecOverridesSchema),
});
const UpdateSpecResponse = Schema.Struct({
slug: IntegrationSlug,
toolCount: Schema.Number,
/** Tool names new in this spec version (same diff for every connection). */
addedTools: Schema.Array(Schema.String),
/** Tool names the new spec no longer defines. */
removedTools: Schema.Array(Schema.String),
});
const IntegrationView = Schema.Struct({
slug: IntegrationSlug,
description: Schema.String,
kind: Schema.String,
canRemove: Schema.Boolean,
canRefresh: Schema.Boolean,
});
// The integration config surfaced for the configure UX. Unlike
// `IntegrationView` (catalog identity only), this carries the
// `authenticationTemplate` the configure flow reads/writes. The spec text is
// deliberately NOT served: it's a multi-MB build artifact in the plugin blob
// store, and no client reads it (the configure UI only touches the template).
const OpenApiConfigView = Schema.Struct({
specUrl: Schema.optional(Schema.String),
baseUrl: Schema.optional(Schema.String),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
specOverrides: Schema.optional(SpecOverridesSchema),
authenticationTemplate: Schema.optional(Schema.Array(AuthenticationResponse)),
});
// The configure result — the merged `authenticationTemplate` after the new
// custom methods were appended/replaced.
const ConfigureResponse = Schema.Struct({
authenticationTemplate: Schema.Array(AuthenticationResponse),
});
// ---------------------------------------------------------------------------
// Group — addSpec/preview/get/remove over the integration catalog.
// ---------------------------------------------------------------------------
export const OpenApiGroup = HttpApiGroup.make("openapi")
.add(
HttpApiEndpoint.post("previewSpec", "/openapi/preview", {
payload: PreviewSpecPayload,
success: SpecPreviewSummary,
error: DomainErrors,
}),
)
.add(
HttpApiEndpoint.post("addSpec", "/openapi/specs", {
payload: AddSpecPayload,
success: AddSpecResponse,
error: DomainErrors,
}),
)
.add(
HttpApiEndpoint.get("getIntegration", "/openapi/integrations/:slug", {
params: SlugParams,
success: Schema.NullOr(IntegrationView),
error: DomainErrors,
}),
)
.add(
HttpApiEndpoint.get("getConfig", "/openapi/integrations/:slug/config", {
params: SlugParams,
success: Schema.NullOr(OpenApiConfigView),
error: DomainErrors,
}),
)
.add(
HttpApiEndpoint.post("configure", "/openapi/integrations/:slug/config", {
params: SlugParams,
payload: ConfigurePayload,
success: ConfigureResponse,
error: DomainErrors,
}),
)
.add(
HttpApiEndpoint.post("updateSpec", "/openapi/integrations/:slug/spec", {
params: SlugParams,
payload: UpdateSpecPayload,
success: UpdateSpecResponse,
error: UpdateSpecErrors,
}),
)
.add(
HttpApiEndpoint.delete("removeSpec", "/openapi/integrations/:slug", {
params: SlugParams,
success: Schema.Void,
error: DomainErrors,
}),
);