From aeed4b6375ec3e5f12fb33b00ade0107353b40ae Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 13 Sep 2026 20:00:46 -0400 Subject: [PATCH] fix(protocol): return missing integration error --- V2_HTTP_API_AUDIT.md | 4 +-- packages/client/src/effect/api/api.ts | 5 +-- .../client/src/promise/generated/client.ts | 2 +- .../client/src/promise/generated/types.ts | 10 +++++- packages/core/src/plugin/host.ts | 6 +++- packages/protocol/openapi.json | 36 ++++++++++++++----- packages/protocol/src/errors.ts | 9 +++++ packages/protocol/src/groups/integration.ts | 5 +-- packages/server/src/handlers/integration.ts | 10 ++++-- services/www/openapi.json | 36 ++++++++++++++----- services/www/public/openapi.json | 36 ++++++++++++++----- 11 files changed, 122 insertions(+), 37 deletions(-) diff --git a/V2_HTTP_API_AUDIT.md b/V2_HTTP_API_AUDIT.md index 41d87f221a2..56e84f82de3 100644 --- a/V2_HTTP_API_AUDIT.md +++ b/V2_HTTP_API_AUDIT.md @@ -109,8 +109,8 @@ Review endpoints in document order. For each endpoint, select one disposition an | Done | Method | Path | Operation ID | Decision | Notes | |---|---|---|---|---|---| -| [ ] 025 | `GET` | `/api/integration` | `integration.list` | | | -| [ ] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | | | +| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. | +| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. | | [ ] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | | | | [ ] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | | | | [ ] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | | | diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 636c37d8bfc..b0c16642c28 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -1465,10 +1465,7 @@ export type IntegrationGetInput = { readonly integrationID: Integration.ID readonly location?: { readonly directory?: string | undefined } | undefined } -export type IntegrationGetOutput = { - readonly location: Location.PublicRef - readonly data: Integration.Info | undefined -} +export type IntegrationGetOutput = { readonly location: Location.PublicRef; readonly data: Integration.Info } export type IntegrationGetOperation = (input: IntegrationGetInput) => Effect.Effect export type IntegrationWellknownAddInput = { diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 09b85af68b9..fedfa7e8035 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -1100,7 +1100,7 @@ export function make(options: ClientOptions) { path: `/api/integration/${encodeURIComponent(input.integrationID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [400, 401], + declaredStatuses: [400, 401, 404], empty: false, }, requestOptions, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 66a27ae5ab0..3f69b3d9e51 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -2502,6 +2502,14 @@ export type ProviderNotFoundError = { export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError" +export type IntegrationNotFoundError = { + readonly _tag: "IntegrationNotFoundError" + readonly integrationID: string + readonly message: string +} +export const isIntegrationNotFoundError = (value: unknown): value is IntegrationNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationNotFoundError" + export type McpServerNotFoundError = { readonly _tag: "McpServerNotFoundError" readonly server: string @@ -4509,7 +4517,7 @@ export type IntegrationGetInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] } -export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo | null } +export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo } export type IntegrationWellknownAddInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 0f1c4a0c534..e5908f61e7a 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -268,7 +268,11 @@ export const make = Effect.fn("PluginHost.make")(function* ( }, integration: { list: () => response(integration.list()), - get: (input) => response(integration.get(Integration.ID.make(input.integrationID))), + get: Effect.fn(function* (input) { + const item = yield* integration.get(Integration.ID.make(input.integrationID)) + if (!item) return yield* Effect.fail(new Error(`Integration not found: ${input.integrationID}`)) + return yield* response(Effect.succeed(item)) + }), connect: { key: (input) => integration.connection.key({ diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index f4197d448a6..e14d120d844 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -5164,14 +5164,7 @@ "$ref": "#/components/schemas/Location.PublicRef" }, "data": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.Info" - }, - { - "type": "null" - } - ] + "$ref": "#/components/schemas/Integration.Info" } }, "required": ["location", "data"], @@ -5199,6 +5192,16 @@ } } } + }, + "404": { + "description": "IntegrationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve one integration and its authentication methods.", @@ -15118,6 +15121,23 @@ "required": ["id", "type", "label"], "additionalProperties": false }, + "IntegrationNotFoundErrorEncoded": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["IntegrationNotFoundError"] + }, + "integrationID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "integrationID", "message"], + "additionalProperties": false + }, "InvalidCursorErrorEncoded": { "type": "object", "properties": { diff --git a/packages/protocol/src/errors.ts b/packages/protocol/src/errors.ts index fd3bffbb08a..a2ff7ab054d 100644 --- a/packages/protocol/src/errors.ts +++ b/packages/protocol/src/errors.ts @@ -82,6 +82,15 @@ export class ProviderNotFoundError extends Schema.TaggedError()( + "IntegrationNotFoundError", + { + integrationID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} + export class ProjectNotFoundError extends Schema.TaggedError()( "ProjectNotFoundError", { diff --git a/packages/protocol/src/groups/integration.ts b/packages/protocol/src/groups/integration.ts index a54a7a341dc..d26ea0f9f12 100644 --- a/packages/protocol/src/groups/integration.ts +++ b/packages/protocol/src/groups/integration.ts @@ -3,7 +3,7 @@ import { Location } from "@opencode/schema/location" import { Form } from "@opencode/schema/form" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" -import { InvalidRequestError } from "../errors.js" +import { IntegrationNotFoundError, InvalidRequestError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" export const IntegrationGroup = HttpApiGroup.make("server.integration") @@ -25,7 +25,8 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") HttpApiEndpoint.get("integration.get", "/api/integration/:integrationID", { params: { integrationID: Integration.ID }, query: LocationQuery, - success: Location.response(Schema.UndefinedOr(Integration.Info)), + success: Location.response(Integration.Info), + error: IntegrationNotFoundError, }) .annotateMerge(locationQueryOpenApi) .annotateMerge( diff --git a/packages/server/src/handlers/integration.ts b/packages/server/src/handlers/integration.ts index 150e10dbf40..e1e3e9f9620 100644 --- a/packages/server/src/handlers/integration.ts +++ b/packages/server/src/handlers/integration.ts @@ -2,7 +2,7 @@ import { Integration } from "@opencode/core/integration" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { Api } from "../api" -import { InvalidRequestError } from "@opencode/protocol/errors" +import { IntegrationNotFoundError, InvalidRequestError } from "@opencode/protocol/errors" import { response } from "../location" import { WellKnown } from "@opencode/core/wellknown" @@ -32,7 +32,13 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" "integration.get", Effect.fn(function* (ctx) { const service = yield* Integration.Service - return yield* response(service.get(ctx.params.integrationID)) + const integration = yield* service.get(ctx.params.integrationID) + if (!integration) + return yield* new IntegrationNotFoundError({ + integrationID: ctx.params.integrationID, + message: `Integration not found: ${ctx.params.integrationID}`, + }) + return yield* response(Effect.succeed(integration)) }), ) .handle( diff --git a/services/www/openapi.json b/services/www/openapi.json index f4197d448a6..e14d120d844 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -5164,14 +5164,7 @@ "$ref": "#/components/schemas/Location.PublicRef" }, "data": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.Info" - }, - { - "type": "null" - } - ] + "$ref": "#/components/schemas/Integration.Info" } }, "required": ["location", "data"], @@ -5199,6 +5192,16 @@ } } } + }, + "404": { + "description": "IntegrationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve one integration and its authentication methods.", @@ -15118,6 +15121,23 @@ "required": ["id", "type", "label"], "additionalProperties": false }, + "IntegrationNotFoundErrorEncoded": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["IntegrationNotFoundError"] + }, + "integrationID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "integrationID", "message"], + "additionalProperties": false + }, "InvalidCursorErrorEncoded": { "type": "object", "properties": { diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index f4197d448a6..e14d120d844 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -5164,14 +5164,7 @@ "$ref": "#/components/schemas/Location.PublicRef" }, "data": { - "anyOf": [ - { - "$ref": "#/components/schemas/Integration.Info" - }, - { - "type": "null" - } - ] + "$ref": "#/components/schemas/Integration.Info" } }, "required": ["location", "data"], @@ -5199,6 +5192,16 @@ } } } + }, + "404": { + "description": "IntegrationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve one integration and its authentication methods.", @@ -15118,6 +15121,23 @@ "required": ["id", "type", "label"], "additionalProperties": false }, + "IntegrationNotFoundErrorEncoded": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["IntegrationNotFoundError"] + }, + "integrationID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "integrationID", "message"], + "additionalProperties": false + }, "InvalidCursorErrorEncoded": { "type": "object", "properties": {