import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; import { Schema } from "effect"; import { ConnectionId, CredentialBindingRef, InternalError, ScopeId, SecretBackedValue, } from "@executor-js/sdk/shared"; import { OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError } from "../sdk/errors"; import { SpecPreview } from "../sdk/preview"; import { StoredSourceSchema } from "../sdk/source-contracts"; import { OAuth2SourceConfig } from "../sdk/types"; // --------------------------------------------------------------------------- // Params // --------------------------------------------------------------------------- const DomainErrors = [ InternalError, OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, ] as const; const ScopeIdParam = { scopeId: ScopeId, }; const SourceParams = { scopeId: ScopeId, namespace: Schema.String, }; const OpenApiSpecInputPayload = Schema.Union([ Schema.Struct({ kind: Schema.Literal("url"), url: Schema.String }), Schema.Struct({ kind: Schema.Literal("blob"), value: Schema.String }), ]); const PreviewSpecFetchCredentialsPayload = Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)), queryParams: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)), }); const OpenApiSecretShapePayload = Schema.Struct({ kind: Schema.Literal("secret"), prefix: Schema.optional(Schema.String), }); const OpenApiConfiguredValuePayload = Schema.Union([Schema.String, OpenApiSecretShapePayload]); const SpecFetchCredentialsPayload = Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, OpenApiConfiguredValuePayload)), queryParams: Schema.optional(Schema.Record(Schema.String, OpenApiConfiguredValuePayload)), }); // --------------------------------------------------------------------------- // Payloads // --------------------------------------------------------------------------- const AddSpecPayload = Schema.Struct({ spec: OpenApiSpecInputPayload, specFetchCredentials: Schema.optional(SpecFetchCredentialsPayload), name: Schema.String, baseUrl: Schema.String, namespace: Schema.String, headers: Schema.optional(Schema.Record(Schema.String, OpenApiConfiguredValuePayload)), queryParams: Schema.optional(Schema.Record(Schema.String, OpenApiConfiguredValuePayload)), oauth2: Schema.optional(OAuth2SourceConfig), }); const PreviewSpecPayload = Schema.Struct({ spec: Schema.String, specFetchCredentials: Schema.optional(PreviewSpecFetchCredentialsPayload), }); const ConfigureCredentialPayload = Schema.Union([ Schema.String, Schema.Struct({ kind: Schema.Literal("text"), text: Schema.String, prefix: Schema.optional(Schema.String), }), Schema.Struct({ kind: Schema.Literal("secret"), secretId: Schema.String, secretScope: Schema.optional(ScopeId), prefix: Schema.optional(Schema.String), }), Schema.Struct({ kind: Schema.Literal("connection"), connectionId: ConnectionId, }), ]); const ConfigureCredentialMapPayload = Schema.Record(Schema.String, ConfigureCredentialPayload); const ConfigurePayload = Schema.Struct({ source: Schema.Struct({ id: Schema.String, scope: ScopeId, }), scope: ScopeId, headers: Schema.optional(ConfigureCredentialMapPayload), queryParams: Schema.optional(ConfigureCredentialMapPayload), specFetchCredentials: Schema.optional( Schema.Struct({ headers: Schema.optional(ConfigureCredentialMapPayload), queryParams: Schema.optional(ConfigureCredentialMapPayload), }), ), oauth2: Schema.optional( Schema.Struct({ clientId: Schema.optional(ConfigureCredentialPayload), clientSecret: Schema.optional(ConfigureCredentialPayload), connection: Schema.optional(ConfigureCredentialPayload), }), ), oauth2Source: Schema.optional(OAuth2SourceConfig), }); // --------------------------------------------------------------------------- // Responses // --------------------------------------------------------------------------- const AddSpecResponse = Schema.Struct({ toolCount: Schema.Number, namespace: Schema.String, }); // HTTP status on the three domain errors lives on their class // declarations in `../sdk/errors.ts` — see the comment there. // --------------------------------------------------------------------------- // Group // // Plugin SDK errors (OpenApiParseError, OpenApiExtractionError, // OpenApiOAuthError) are declared once at the group level via // `.addError(...)` — every endpoint inherits them. The errors themselves // carry their HTTP status via `HttpApiSchema.annotations` above, so // handlers just `return yield* ext.foo(...)` and the schema encodes // whatever comes out. // // 5xx is handled at the API level: `.addError(InternalError)` adds the // shared opaque 500 surface. Defects are captured + downgraded to it by // an HttpApiBuilder middleware (see apps/cloud/src/observability.ts). // StorageError → InternalError translation happens at service wiring // time via `withCapture(executor)`. // --------------------------------------------------------------------------- export const OpenApiGroup = HttpApiGroup.make("openapi") .add( HttpApiEndpoint.post("previewSpec", "/scopes/:scopeId/openapi/preview", { params: ScopeIdParam, payload: PreviewSpecPayload, success: SpecPreview, error: DomainErrors, }), ) .add( HttpApiEndpoint.post("addSpec", "/scopes/:scopeId/openapi/specs", { params: ScopeIdParam, payload: AddSpecPayload, success: AddSpecResponse, error: DomainErrors, }), ) .add( HttpApiEndpoint.get("getSource", "/scopes/:scopeId/openapi/sources/:namespace", { params: SourceParams, success: Schema.NullOr(StoredSourceSchema), error: DomainErrors, }), ) .add( HttpApiEndpoint.post("configure", "/scopes/:scopeId/openapi/configure", { params: ScopeIdParam, payload: ConfigurePayload, success: Schema.Array(CredentialBindingRef), error: DomainErrors, }), );