130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
import { Schema } from "effect"
|
|
import { OpenApi } from "effect/unstable/httpapi"
|
|
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
|
|
|
|
type OpenApiSchema = {
|
|
readonly $ref?: string
|
|
readonly items?: OpenApiSchema
|
|
readonly properties?: Record<string, OpenApiSchema>
|
|
}
|
|
|
|
type OpenApiSpec = {
|
|
readonly components?: { readonly schemas?: Record<string, OpenApiSchema> }
|
|
readonly paths: Record<
|
|
string,
|
|
{
|
|
readonly get?: {
|
|
readonly responses?: Record<string, { readonly content?: Record<string, { schema?: OpenApiSchema }> }>
|
|
}
|
|
}
|
|
>
|
|
}
|
|
|
|
function responseSchema(spec: OpenApiSpec, path: string) {
|
|
return spec.paths[path]?.get?.responses?.["200"]?.content?.["application/json"]?.schema
|
|
}
|
|
|
|
function componentName(ref: string | undefined) {
|
|
return ref?.replace("#/components/schemas/", "")
|
|
}
|
|
|
|
describe("PublicApi v2 catalog redaction", () => {
|
|
test("routes use redacted provider and model DTO schemas", () => {
|
|
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
|
const provider = responseSchema(spec, "/api/provider/{providerID}")
|
|
const providers = responseSchema(spec, "/api/provider")
|
|
const models = responseSchema(spec, "/api/model")
|
|
|
|
expect(componentName(provider?.$ref)).toBe("ProviderV2PublicInfo")
|
|
expect(componentName(providers?.items?.$ref)).toBe("ProviderV2PublicInfo")
|
|
expect(componentName(models?.items?.$ref)).toBe("ModelV2PublicInfo")
|
|
|
|
const providerProperties = spec.components?.schemas?.ProviderV2PublicInfo?.properties
|
|
const modelProperties = spec.components?.schemas?.ModelV2PublicInfo?.properties
|
|
expect(providerProperties).not.toHaveProperty("request")
|
|
expect(modelProperties).not.toHaveProperty("request")
|
|
expect(JSON.stringify(providerProperties)).not.toMatch(/settings|headers|body|data/)
|
|
expect(JSON.stringify(modelProperties)).not.toMatch(/settings|headers|body/)
|
|
})
|
|
|
|
test("DTOs sanitize provider and model API URLs", () => {
|
|
const providerID = ProviderV2.ID.make("test")
|
|
const providers = [
|
|
new ProviderV2.Info({
|
|
...ProviderV2.Info.empty(providerID),
|
|
api: {
|
|
type: "native",
|
|
url: "https://provider-user:provider-password@example.com:8443/provider/v1?api_key=provider-secret#fragment",
|
|
settings: {},
|
|
},
|
|
}),
|
|
new ProviderV2.Info({
|
|
...ProviderV2.Info.empty(providerID),
|
|
api: {
|
|
type: "aisdk",
|
|
package: "@ai-sdk/openai",
|
|
url: "https://provider-aisdk-user:provider-aisdk-password@example.com:8444/provider/aisdk?api_key=provider-aisdk-secret#fragment",
|
|
},
|
|
}),
|
|
].map((provider) => Schema.encodeSync(ProviderV2.PublicInfo)(ProviderV2.toPublic(provider)))
|
|
const models = [
|
|
new ModelV2.Info({
|
|
...ModelV2.Info.empty(providerID, ModelV2.ID.make("native")),
|
|
api: {
|
|
id: ModelV2.ID.make("native"),
|
|
type: "native",
|
|
url: "https://native-user:native-password@example.com:9443/native/v1?api_key=native-secret#fragment",
|
|
settings: {},
|
|
},
|
|
}),
|
|
new ModelV2.Info({
|
|
...ModelV2.Info.empty(providerID, ModelV2.ID.make("aisdk")),
|
|
api: {
|
|
id: ModelV2.ID.make("aisdk"),
|
|
type: "aisdk",
|
|
package: "@ai-sdk/openai",
|
|
url: "https://aisdk-user:aisdk-password@example.com:10443/aisdk/v1?api_key=aisdk-secret#fragment",
|
|
},
|
|
}),
|
|
].map((model) => Schema.encodeSync(ModelV2.PublicInfo)(ModelV2.toPublic(model)))
|
|
|
|
expect(providers.map((provider) => provider.api)).toEqual([
|
|
{ type: "native", url: "https://example.com:8443" },
|
|
{ type: "aisdk", package: "@ai-sdk/openai", url: "https://example.com:8444" },
|
|
])
|
|
expect(models.map((model) => model.api)).toEqual([
|
|
{ id: "native", type: "native", url: "https://example.com:9443" },
|
|
{ id: "aisdk", type: "aisdk", package: "@ai-sdk/openai", url: "https://example.com:10443" },
|
|
])
|
|
expect(JSON.stringify({ providers, models })).not.toMatch(/user|password|api_key|secret|fragment/)
|
|
})
|
|
|
|
test("DTOs omit malformed API URLs", () => {
|
|
const providerID = ProviderV2.ID.make("test")
|
|
const provider = Schema.encodeSync(ProviderV2.PublicInfo)(
|
|
ProviderV2.toPublic(
|
|
new ProviderV2.Info({
|
|
...ProviderV2.Info.empty(providerID),
|
|
api: { type: "native", url: "not a url?api_key=provider-secret", settings: {} },
|
|
}),
|
|
),
|
|
)
|
|
const modelID = ModelV2.ID.make("aisdk")
|
|
const model = Schema.encodeSync(ModelV2.PublicInfo)(
|
|
ModelV2.toPublic(
|
|
new ModelV2.Info({
|
|
...ModelV2.Info.empty(providerID, modelID),
|
|
api: { id: modelID, type: "aisdk", package: "@ai-sdk/openai", url: "model-secret" },
|
|
}),
|
|
),
|
|
)
|
|
|
|
expect(provider.api).toEqual({ type: "native" })
|
|
expect(model.api).toEqual({ id: "aisdk", type: "aisdk", package: "@ai-sdk/openai" })
|
|
expect(JSON.stringify({ provider, model })).not.toMatch(/secret|api_key/)
|
|
})
|
|
})
|