refactor(codemode): simplify OpenAPI adapter split

This commit is contained in:
Aiden Cline
2026-07-03 15:29:02 -05:00
parent afb42e1ee9
commit 34c65b71e6
6 changed files with 171 additions and 188 deletions
@@ -1,76 +0,0 @@
import { Effect } from "effect"
import { ToolError, toolError } from "../../tool-error.js"
import { own } from "./shared.js"
import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js"
export const resolveAuth = (plan: Plan): Effect.Effect<AppliedAuth, unknown> =>
Effect.gen(function* () {
const none: AppliedAuth = { headers: {}, query: {}, cookies: {} }
if (plan.security.length === 0) return none
const unavailable: Array<string> = []
alternatives: for (const requirement of plan.security) {
const names = Object.keys(requirement)
if (names.length === 0) return none
const credentials: Array<readonly [SecurityScheme, Credential]> = []
for (const name of names) {
const scheme = own(plan.schemes, name)
if (scheme === undefined || plan.auth === undefined) {
unavailable.push(name)
continue alternatives
}
const credential = yield* plan.auth.resolve({
schemeName: name,
scheme,
scopes: requirement[name] ?? [],
operation: plan.operation,
})
if (credential === undefined) {
unavailable.push(name)
continue alternatives
}
credentials.push([scheme, credential])
}
const applied = applyCredentials(credentials)
return applied instanceof ToolError ? yield* Effect.fail(applied) : applied
}
return yield* Effect.fail(
toolError(
`${plan.operation.method} ${plan.operation.path} requires authentication; no credential available for: ${[...new Set(unavailable)].join(", ")}.`,
),
)
})
const applyCredentials = (credentials: ReadonlyArray<readonly [SecurityScheme, Credential]>): AppliedAuth | ToolError => {
const headers: Record<string, string> = {}
const query: Record<string, string> = {}
const cookies: Record<string, string> = {}
for (const [scheme, credential] of credentials) {
if (credential.type === "bearer") {
headers["authorization"] = `Bearer ${credential.token}`
continue
}
if (credential.type === "basic") {
// Buffer instead of btoa: btoa throws on non-Latin-1 credentials.
headers["authorization"] =
`Basic ${Buffer.from(`${credential.username}:${credential.password}`, "utf8").toString("base64")}`
continue
}
if (credential.type === "header") {
headers[credential.name.toLowerCase()] = credential.value
continue
}
// apiKey: the carrier comes from the scheme declaration.
const name = scheme.parameterName
if (scheme.type !== "apiKey" || name === undefined || scheme.in === undefined) {
return toolError(
`Security scheme '${scheme.name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`,
)
}
if (scheme.in === "header") headers[name.toLowerCase()] = credential.value
if (scheme.in === "query") query[name] = credential.value
if (scheme.in === "cookie") cookies[name] = credential.value
}
return { headers, query, cookies }
}
@@ -1,10 +1,12 @@
import { HttpClient } from "effect/unstable/http"
import { Tool, type Definition } from "../../tool.js"
import { invoke } from "./invoke.js"
import { componentDefinitions } from "./schema.js"
import { isRecord, methods, nonEmptyString } from "./shared.js"
import { invoke } from "./runtime.js"
import {
componentDefinitions,
inputSchema,
isRecord,
methods,
nonEmptyString,
operationName,
operationParameters,
outputSchema,
@@ -1,9 +1,8 @@
import { Effect, Option, Schema } from "effect"
import { HttpClient, HttpClientRequest, type HttpMethod } from "effect/unstable/http"
import { ToolError, toolError } from "../../tool-error.js"
import { resolveAuth } from "./auth.js"
import { isRecord, maxErrorBodyChars, own } from "./shared.js"
import type { Plan } from "./types.js"
import { isRecord, maxErrorBodyChars, own } from "./spec.js"
import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js"
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
@@ -79,6 +78,78 @@ export const invoke = (plan: Plan, input: unknown): Effect.Effect<unknown, unkno
return parsed
})
const resolveAuth = (plan: Plan): Effect.Effect<AppliedAuth, unknown> =>
Effect.gen(function* () {
const none: AppliedAuth = { headers: {}, query: {}, cookies: {} }
if (plan.security.length === 0) return none
const unavailable: Array<string> = []
alternatives: for (const requirement of plan.security) {
const names = Object.keys(requirement)
if (names.length === 0) return none
const credentials: Array<readonly [SecurityScheme, Credential]> = []
for (const name of names) {
const scheme = own(plan.schemes, name)
if (scheme === undefined || plan.auth === undefined) {
unavailable.push(name)
continue alternatives
}
const credential = yield* plan.auth.resolve({
schemeName: name,
scheme,
scopes: requirement[name] ?? [],
operation: plan.operation,
})
if (credential === undefined) {
unavailable.push(name)
continue alternatives
}
credentials.push([scheme, credential])
}
const applied = applyCredentials(credentials)
return applied instanceof ToolError ? yield* Effect.fail(applied) : applied
}
return yield* Effect.fail(
toolError(
`${plan.operation.method} ${plan.operation.path} requires authentication; no credential available for: ${[...new Set(unavailable)].join(", ")}.`,
),
)
})
const applyCredentials = (credentials: ReadonlyArray<readonly [SecurityScheme, Credential]>): AppliedAuth | ToolError => {
const headers: Record<string, string> = {}
const query: Record<string, string> = {}
const cookies: Record<string, string> = {}
for (const [scheme, credential] of credentials) {
if (credential.type === "bearer") {
headers["authorization"] = `Bearer ${credential.token}`
continue
}
if (credential.type === "basic") {
// Buffer instead of btoa: btoa throws on non-Latin-1 credentials.
headers["authorization"] =
`Basic ${Buffer.from(`${credential.username}:${credential.password}`, "utf8").toString("base64")}`
continue
}
if (credential.type === "header") {
headers[credential.name.toLowerCase()] = credential.value
continue
}
// apiKey: the carrier comes from the scheme declaration.
const name = scheme.parameterName
if (scheme.type !== "apiKey" || name === undefined || scheme.in === undefined) {
return toolError(
`Security scheme '${scheme.name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`,
)
}
if (scheme.in === "header") headers[name.toLowerCase()] = credential.value
if (scheme.in === "query") query[name] = credential.value
if (scheme.in === "cookie") cookies[name] = credential.value
}
return { headers, query, cookies }
}
const summarizeBody = (body: unknown): string => {
const rendered = typeof body === "string" ? body : (JSON.stringify(body) ?? "")
if (rendered === "" || rendered === "null") return "no response body"
@@ -1,75 +0,0 @@
import type { JsonSchema } from "../../tool.js"
import { isRecord, nonEmptyString } from "./shared.js"
import type { Document } from "./types.js"
export const projectSchema = (value: unknown, depth = 0): JsonSchema => {
if (depth > 24 || !isRecord(value)) return {}
const ref = nonEmptyString(value.$ref)
if (ref !== undefined) {
// `#/components/schemas/X` becomes `#/$defs/X`, the only ref form the
// signature renderer resolves. `~` is unescaped to match the `$defs` key;
// `/` must stay escaped because the renderer takes the last `/` segment.
const name = ref.match(/^#\/components\/schemas\/(.+)$/)?.[1]
return { $ref: name === undefined ? ref : `#/$defs/${name.replaceAll("~0", "~")}` }
}
const type = Array.isArray(value.type)
? value.type.filter((item): item is string => typeof item === "string")
: nonEmptyString(value.type)
const description = nonEmptyString(value.description)
const format = nonEmptyString(value.format)
const projected: JsonSchema = {
...(type === undefined ? {} : { type }),
...(Array.isArray(value.enum) ? { enum: value.enum } : {}),
...(value.const === undefined ? {} : { const: value.const }),
...(Array.isArray(value.anyOf) ? { anyOf: value.anyOf.map((item) => projectSchema(item, depth + 1)) } : {}),
...(Array.isArray(value.oneOf) ? { oneOf: value.oneOf.map((item) => projectSchema(item, depth + 1)) } : {}),
...(Array.isArray(value.allOf) ? { allOf: value.allOf.map((item) => projectSchema(item, depth + 1)) } : {}),
...(isRecord(value.properties)
? {
properties: Object.fromEntries(
Object.entries(value.properties).map(([key, item]) => [key, projectSchema(item, depth + 1)]),
),
}
: {}),
...(Array.isArray(value.required)
? { required: value.required.filter((item): item is string => typeof item === "string") }
: {}),
...(isRecord(value.items) ? { items: projectSchema(value.items, depth + 1) } : {}),
...(typeof value.additionalProperties === "boolean"
? { additionalProperties: value.additionalProperties }
: isRecord(value.additionalProperties)
? { additionalProperties: projectSchema(value.additionalProperties, depth + 1) }
: {}),
...(description === undefined ? {} : { description }),
...(value.default === undefined ? {} : { default: value.default }),
...(format === undefined ? {} : { format }),
...(value.deprecated === true ? { deprecated: true } : {}),
...(typeof value.minItems === "number" ? { minItems: value.minItems } : {}),
...(typeof value.maxItems === "number" ? { maxItems: value.maxItems } : {}),
}
// OpenAPI 3.0 nullable -> union with null, matching what 3.1 expresses via type arrays.
if (value.nullable !== true) return projected
if (Array.isArray(projected.type)) return { ...projected, type: [...projected.type, "null"] }
if (typeof projected.type === "string") return { ...projected, type: [projected.type, "null"] }
return { anyOf: [projected, { type: "null" }] }
}
export const componentDefinitions = (document: Document): Readonly<Record<string, JsonSchema>> => {
const components = isRecord(document.components) ? document.components : {}
const schemas = isRecord(components.schemas) ? components.schemas : {}
return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(value)]))
}
export const withDefinitions = (schema: JsonSchema, definitions: Readonly<Record<string, JsonSchema>>): JsonSchema =>
Object.keys(definitions).length === 0 ? schema : { ...schema, $defs: definitions }
export const isJsonMediaType = (mediaType: string): boolean => {
const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
return normalized === "application/json" || normalized.endsWith("+json")
}
export const jsonContentSchema = (content: Record<string, unknown>): unknown => {
const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType))
return entry !== undefined && isRecord(entry[1]) ? entry[1].schema : undefined
}
@@ -1,21 +0,0 @@
export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"])
export const parameterLocations = new Set(["path", "query", "header"])
// OpenAPI: header parameters with these names SHALL be ignored.
export const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"])
export const schemeTypes = new Set(["apiKey", "http", "oauth2", "openIdConnect"])
export const blockedOperationNames = new Set(["__proto__", "constructor", "prototype"])
export const maxErrorBodyChars = 1_024
export const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
export const asArray = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value) ? value : [])
export const nonEmptyString = (value: unknown): string | undefined =>
typeof value === "string" && value !== "" ? value : undefined
// Guards record lookups keyed by spec- or model-controlled names against
// prototype-inherited values (e.g. a parameter named `toString`).
export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
Object.hasOwn(record, key) ? record[key] : undefined
+92 -10
View File
@@ -1,16 +1,26 @@
import type { JsonSchema } from "../../tool.js"
import {
asArray,
blockedOperationNames,
ignoredHeaderParameters,
isRecord,
nonEmptyString,
parameterLocations,
schemeTypes,
} from "./shared.js"
import { isJsonMediaType, jsonContentSchema, projectSchema, withDefinitions } from "./schema.js"
import type { Body, Document, Parameter, ParameterLocation, SecurityRequirement, SecurityScheme, Skip } from "./types.js"
export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"])
const parameterLocations = new Set(["path", "query", "header"])
const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"])
const schemeTypes = new Set(["apiKey", "http", "oauth2", "openIdConnect"])
const blockedOperationNames = new Set(["__proto__", "constructor", "prototype"])
export const maxErrorBodyChars = 1_024
export const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
const asArray = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value) ? value : [])
export const nonEmptyString = (value: unknown): string | undefined =>
typeof value === "string" && value !== "" ? value : undefined
// Guards record lookups keyed by spec- or model-controlled names against
// prototype-inherited values (e.g. a parameter named `toString`).
export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
Object.hasOwn(record, key) ? record[key] : undefined
export const resolve = (document: Document, value: unknown): unknown => {
if (!isRecord(value)) return value
const ref = nonEmptyString(value.$ref)
@@ -23,6 +33,78 @@ export const resolve = (document: Document, value: unknown): unknown => {
return target ?? value
}
const projectSchema = (value: unknown, depth = 0): JsonSchema => {
if (depth > 24 || !isRecord(value)) return {}
const ref = nonEmptyString(value.$ref)
if (ref !== undefined) {
// `#/components/schemas/X` becomes `#/$defs/X`, the only ref form the
// signature renderer resolves. `~` is unescaped to match the `$defs` key;
// `/` must stay escaped because the renderer takes the last `/` segment.
const name = ref.match(/^#\/components\/schemas\/(.+)$/)?.[1]
return { $ref: name === undefined ? ref : `#/$defs/${name.replaceAll("~0", "~")}` }
}
const type = Array.isArray(value.type)
? value.type.filter((item): item is string => typeof item === "string")
: nonEmptyString(value.type)
const description = nonEmptyString(value.description)
const format = nonEmptyString(value.format)
const projected: JsonSchema = {
...(type === undefined ? {} : { type }),
...(Array.isArray(value.enum) ? { enum: value.enum } : {}),
...(value.const === undefined ? {} : { const: value.const }),
...(Array.isArray(value.anyOf) ? { anyOf: value.anyOf.map((item) => projectSchema(item, depth + 1)) } : {}),
...(Array.isArray(value.oneOf) ? { oneOf: value.oneOf.map((item) => projectSchema(item, depth + 1)) } : {}),
...(Array.isArray(value.allOf) ? { allOf: value.allOf.map((item) => projectSchema(item, depth + 1)) } : {}),
...(isRecord(value.properties)
? {
properties: Object.fromEntries(
Object.entries(value.properties).map(([key, item]) => [key, projectSchema(item, depth + 1)]),
),
}
: {}),
...(Array.isArray(value.required)
? { required: value.required.filter((item): item is string => typeof item === "string") }
: {}),
...(isRecord(value.items) ? { items: projectSchema(value.items, depth + 1) } : {}),
...(typeof value.additionalProperties === "boolean"
? { additionalProperties: value.additionalProperties }
: isRecord(value.additionalProperties)
? { additionalProperties: projectSchema(value.additionalProperties, depth + 1) }
: {}),
...(description === undefined ? {} : { description }),
...(value.default === undefined ? {} : { default: value.default }),
...(format === undefined ? {} : { format }),
...(value.deprecated === true ? { deprecated: true } : {}),
...(typeof value.minItems === "number" ? { minItems: value.minItems } : {}),
...(typeof value.maxItems === "number" ? { maxItems: value.maxItems } : {}),
}
// OpenAPI 3.0 nullable -> union with null, matching what 3.1 expresses via type arrays.
if (value.nullable !== true) return projected
if (Array.isArray(projected.type)) return { ...projected, type: [...projected.type, "null"] }
if (typeof projected.type === "string") return { ...projected, type: [projected.type, "null"] }
return { anyOf: [projected, { type: "null" }] }
}
export const componentDefinitions = (document: Document): Readonly<Record<string, JsonSchema>> => {
const components = isRecord(document.components) ? document.components : {}
const schemas = isRecord(components.schemas) ? components.schemas : {}
return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(value)]))
}
const withDefinitions = (schema: JsonSchema, definitions: Readonly<Record<string, JsonSchema>>): JsonSchema =>
Object.keys(definitions).length === 0 ? schema : { ...schema, $defs: definitions }
const isJsonMediaType = (mediaType: string): boolean => {
const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
return normalized === "application/json" || normalized.endsWith("+json")
}
const jsonContentSchema = (content: Record<string, unknown>): unknown => {
const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType))
return entry !== undefined && isRecord(entry[1]) ? entry[1].schema : undefined
}
export const operationParameters = (
document: Document,
pathItem: Record<string, unknown>,