import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; import { Schema } from "effect"; import { ScopeId, SecretBackedValue } from "@executor-js/sdk/core"; import { InternalError } from "@executor-js/api"; import { OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError } from "../sdk/errors"; import { SpecPreview } from "../sdk/preview"; import { StoredSourceSchema } from "../sdk/store"; import { OAuth2Auth, OAuth2SourceConfig, OpenApiSourceBindingInputSchema, OpenApiSourceBindingRef, } from "../sdk/types"; // --------------------------------------------------------------------------- // Params // --------------------------------------------------------------------------- const DomainErrors = [ InternalError, OpenApiParseError, OpenApiExtractionError, OpenApiOAuthError, ] as const; const ScopeIdParam = { scopeId: ScopeId, }; const SourceParams = { scopeId: ScopeId, namespace: Schema.String, }; const SourceBindingParams = { scopeId: ScopeId, namespace: Schema.String, sourceScopeId: ScopeId, }; const SpecFetchCredentialsPayload = Schema.Struct({ headers: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)), queryParams: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)), }); // --------------------------------------------------------------------------- // Payloads // --------------------------------------------------------------------------- const AddSpecPayload = Schema.Struct({ spec: Schema.String, specFetchCredentials: Schema.optional(SpecFetchCredentialsPayload), name: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), namespace: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), queryParams: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)), oauth2: Schema.optional(Schema.Union([OAuth2Auth, OAuth2SourceConfig])), }); const PreviewSpecPayload = Schema.Struct({ spec: Schema.String, specFetchCredentials: Schema.optional(SpecFetchCredentialsPayload), }); const UpdateSourcePayload = Schema.Struct({ name: Schema.optional(Schema.String), baseUrl: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), queryParams: Schema.optional(Schema.Record(Schema.String, SecretBackedValue)), // Set after a successful re-authenticate to refresh the source's // stored OAuth2 metadata. oauth2: Schema.optional(Schema.Union([OAuth2Auth, OAuth2SourceConfig])), }); const UpdateSourceResponse = Schema.Struct({ updated: Schema.Boolean, }); const RemoveBindingPayload = Schema.Struct({ sourceId: Schema.String, sourceScope: ScopeId, slot: Schema.String, scope: ScopeId, }); // --------------------------------------------------------------------------- // 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.patch("updateSource", "/scopes/:scopeId/openapi/sources/:namespace", { params: SourceParams, payload: UpdateSourcePayload, success: UpdateSourceResponse, error: DomainErrors, }), ) .add( HttpApiEndpoint.get( "listSourceBindings", "/scopes/:scopeId/openapi/sources/:namespace/base/:sourceScopeId/bindings", { params: SourceBindingParams, success: Schema.Array(OpenApiSourceBindingRef), error: DomainErrors, }, ), ) .add( HttpApiEndpoint.post("setSourceBinding", "/scopes/:scopeId/openapi/source-bindings", { params: ScopeIdParam, payload: OpenApiSourceBindingInputSchema, success: OpenApiSourceBindingRef, error: DomainErrors, }), ) .add( HttpApiEndpoint.post( "removeSourceBinding", "/scopes/:scopeId/openapi/source-bindings/remove", { params: ScopeIdParam, payload: RemoveBindingPayload, success: Schema.Struct({ removed: Schema.Boolean }), error: DomainErrors, }, ), ) ;