Replace AuthConfig union (NoAuth/BearerAuth/ApiKeyAuth) with plain headers on InvocationConfig. Auth is just headers now — simpler and covers all cases. New previewSpec(): - Reuses parse() + extract() for heavy lifting - Extracts security schemes from components.securitySchemes - Derives auth strategies from top-level security array - Builds header presets for the frontend (which headers to show, which are secrets) - Returns metadata: title, version, servers, operation count, tags Tested against Cloudflare spec: 4 security schemes, 3 auth strategies, correct header presets (Bearer Token, API Key + Email, etc.) 19 tests passing, all typechecks clean.
Unified · Split
| @@ -10,6 +10,13 @@ export { · 9 unmodified lines | |||
| 10 | makeInMemoryOperationStore, | 10 | makeInMemoryOperationStore, |
| 11 | type OpenApiOperationStore, | 11 | type OpenApiOperationStore, |
| 12 | } from "./operation-store"; | 12 | } from "./operation-store"; |
| 13 | export { | ||
| 14 | previewSpec, | ||
| 15 | SecurityScheme, | ||
| 16 | AuthStrategy, | ||
| 17 | HeaderPreset, | ||
| 18 | SpecPreview, | ||
| 19 | } from "./preview"; | ||
| 13 | export { DocResolver, resolveBaseUrl, preferredContent } from "./openapi-utils"; | 20 | export { DocResolver, resolveBaseUrl, preferredContent } from "./openapi-utils"; |
| 14 | 21 | ||
| 15 | export { | 22 | export { |
| @@ -19,10 +26,6 @@ export { · 3 unmodified lines | |||
| 19 | } from "./errors"; | 26 | } from "./errors"; |
| 20 | 27 | ||
| 21 | export { | 28 | export { |
| 22 | AuthConfig, | ||
| 23 | NoAuth, | ||
| 24 | BearerAuth, | ||
| 25 | ApiKeyAuth, | ||
| 26 | ExtractedOperation, | 29 | ExtractedOperation, |
| 27 | ExtractionResult, | 30 | ExtractionResult, |
| 28 | InvocationConfig, | 31 | InvocationConfig, |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { Effect, Layer, Match, Option } from "effect"; | 1 | import { Effect, Layer, Option } from "effect"; |
| 2 | import { | 2 | import { |
| 3 | HttpClient, | 3 | HttpClient, |
| 4 | HttpClientRequest, | 4 | HttpClientRequest, |
| @@ -14,7 +14,6 @@ import { · 9 unmodified lines | |||
| 14 | import { OpenApiInvocationError } from "./errors"; | 14 | import { OpenApiInvocationError } from "./errors"; |
| 15 | import type { OpenApiOperationStore } from "./operation-store"; | 15 | import type { OpenApiOperationStore } from "./operation-store"; |
| 16 | import { | 16 | import { |
| 17 | type AuthConfig, | ||
| 18 | type OperationBinding, | 17 | type OperationBinding, |
| 19 | InvocationConfig, | 18 | InvocationConfig, |
| 20 | InvocationResult, | 19 | InvocationResult, |
| @@ -117,35 +116,19 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( · 96 unmodified lines | |||
| 117 | }); | 116 | }); |
| 118 | 117 | ||
| 119 | // --------------------------------------------------------------------------- | 118 | // --------------------------------------------------------------------------- |
| 120 | // Auth application | 119 | // Header application |
| 121 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
| 122 | 121 | ||
| 123 | const applyAuth = ( | 122 | const applyHeaders = ( |
| 124 | request: HttpClientRequest.HttpClientRequest, | 123 | request: HttpClientRequest.HttpClientRequest, |
| 125 | auth: AuthConfig, | 124 | headers: Record<string, string>, |
| 126 | ): HttpClientRequest.HttpClientRequest => | 125 | ): HttpClientRequest.HttpClientRequest => { |
| 127 | Match.valueTags(auth, { | 126 | let req = request; |
| 128 | NoAuth: () => request, | 127 | for (const [name, value] of Object.entries(headers)) { |
| 129 | BearerAuth: ({ token, headerName, prefix }) => | 128 | req = HttpClientRequest.setHeader(req, name, value); |
| 130 | HttpClientRequest.setHeader(request, headerName, `${prefix}${token}`), | 129 | } |
| 131 | ApiKeyAuth: ({ name, value, in: location }) => { | 130 | return req; |
| 132 | if (location === "header") { | 131 | }; |
| 133 | return HttpClientRequest.setHeader(request, name, value); | ||
| 134 | } | ||
| 135 | if (location === "query") { | ||
| 136 | return HttpClientRequest.setUrlParam(request, name, value); | ||
| 137 | } | ||
| 138 | // cookie | ||
| 139 | const existing = | ||
| 140 | request.headers["cookie"] ?? ""; | ||
| 141 | const cookie = `${name}=${encodeURIComponent(value)}`; | ||
| 142 | return HttpClientRequest.setHeader( | ||
| 143 | request, | ||
| 144 | "cookie", | ||
| 145 | existing ? `${existing}; ${cookie}` : cookie, | ||
| 146 | ); | ||
| 147 | }, | ||
| 148 | }); | ||
| 149 | 132 | ||
| 150 | // --------------------------------------------------------------------------- | 133 | // --------------------------------------------------------------------------- |
| 151 | // Response helpers | 134 | // Response helpers |
| @@ -221,8 +204,8 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 69 unmodified lines | |||
| 221 | } | 204 | } |
| 222 | } | 205 | } |
| 223 | 206 | ||
| 224 | // Auth | 207 | // Static headers (auth, custom headers, etc.) |
| 225 | request = applyAuth(request, config.auth); | 208 | request = applyHeaders(request, config.headers); |
| 226 | 209 | ||
| 227 | // Execute | 210 | // Execute |
| 228 | const response = yield* client.execute(request).pipe( | 211 | const response = yield* client.execute(request).pipe( |
| @@ -78,6 +78,26 @@ const TestLayer = HttpApiBuilder.serve().pipe( · 77 unmodified lines | |||
| 78 | // --------------------------------------------------------------------------- | 78 | // --------------------------------------------------------------------------- |
| 79 | 79 | ||
| 80 | layer(TestLayer)("OpenAPI Plugin", (it) => { | 80 | layer(TestLayer)("OpenAPI Plugin", (it) => { |
| 81 | it.effect("previewSpec returns metadata and header presets", () => | ||
| 82 | Effect.gen(function* () { | ||
| 83 | const httpClient = yield* HttpClient.HttpClient; | ||
| 84 | const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); | ||
| 85 | |||
| 86 | const executor = yield* createExecutor( | ||
| 87 | makeTestConfig({ | ||
| 88 | plugins: [ | ||
| 89 | openApiPlugin({ httpClientLayer: clientLayer }), | ||
| 90 | ] as const, | ||
| 91 | }), | ||
| 92 | ); | ||
| 93 | |||
| 94 | const preview = yield* executor.openapi.previewSpec(specJson); | ||
| 95 | |||
| 96 | expect(preview.operationCount).toBeGreaterThanOrEqual(2); | ||
| 97 | expect(preview.servers).toBeDefined(); | ||
| 98 | }), | ||
| 99 | ); | ||
| 100 | |||
| 81 | it.effect("registers tools from an OpenAPI spec", () => | 101 | it.effect("registers tools from an OpenAPI spec", () => |
| 82 | Effect.gen(function* () { | 102 | Effect.gen(function* () { |
| 83 | const httpClient = yield* HttpClient.HttpClient; | 103 | const httpClient = yield* HttpClient.HttpClient; |
| @@ -18,11 +18,10 @@ import { · 17 unmodified lines | |||
| 18 | makeInMemoryOperationStore, | 18 | makeInMemoryOperationStore, |
| 19 | type OpenApiOperationStore, | 19 | type OpenApiOperationStore, |
| 20 | } from "./operation-store"; | 20 | } from "./operation-store"; |
| 21 | import { previewSpec, type SpecPreview } from "./preview"; | ||
| 21 | import { | 22 | import { |
| 22 | type AuthConfig, | ||
| 23 | type ExtractedOperation, | 23 | type ExtractedOperation, |
| 24 | InvocationConfig, | 24 | InvocationConfig, |
| 25 | NoAuth, | ||
| 26 | OperationBinding, | 25 | OperationBinding, |
| 27 | } from "./types"; | 26 | } from "./types"; |
| 28 | 27 | ||
| @@ -34,7 +33,8 @@ export interface OpenApiSpecConfig { · 5 unmodified lines | |||
| 34 | readonly spec: string; | 33 | readonly spec: string; |
| 35 | readonly baseUrl?: string; | 34 | readonly baseUrl?: string; |
| 36 | readonly namespace?: string; | 35 | readonly namespace?: string; |
| 37 | readonly auth?: AuthConfig; | 36 | /** Static headers applied to every request for this spec */ |
| 37 | readonly headers?: Record<string, string>; | ||
| 38 | } | 38 | } |
| 39 | 39 | ||
| 40 | // --------------------------------------------------------------------------- | 40 | // --------------------------------------------------------------------------- |
| @@ -42,10 +42,17 @@ export interface OpenApiSpecConfig { · 1 unmodified line | |||
| 42 | // --------------------------------------------------------------------------- | 42 | // --------------------------------------------------------------------------- |
| 43 | 43 | ||
| 44 | export interface OpenApiPluginExtension { | 44 | export interface OpenApiPluginExtension { |
| 45 | /** Preview a spec without registering — returns metadata, auth strategies, header presets */ | ||
| 46 | readonly previewSpec: ( | ||
| 47 | specText: string, | ||
| 48 | ) => Effect.Effect<SpecPreview, Error>; | ||
| 49 | |||
| 50 | /** Add an OpenAPI spec and register its operations as tools */ | ||
| 45 | readonly addSpec: ( | 51 | readonly addSpec: ( |
| 46 | config: OpenApiSpecConfig, | 52 | config: OpenApiSpecConfig, |
| 47 | ) => Effect.Effect<{ readonly toolCount: number }, Error>; | 53 | ) => Effect.Effect<{ readonly toolCount: number }, Error>; |
| 48 | 54 | ||
| 55 | /** Remove all tools from a previously added spec by namespace */ | ||
| 49 | readonly removeSpec: (namespace: string) => Effect.Effect<void>; | 56 | readonly removeSpec: (namespace: string) => Effect.Effect<void>; |
| 50 | } | 57 | } |
| 51 | 58 | ||
| @@ -103,6 +110,8 @@ export const openApiPlugin = (options?: { · 51 unmodified lines | |||
| 103 | 110 | ||
| 104 | return { | 111 | return { |
| 105 | extension: { | 112 | extension: { |
| 113 | previewSpec: (specText: string) => previewSpec(specText), | ||
| 114 | |||
| 106 | addSpec: (config: OpenApiSpecConfig) => | 115 | addSpec: (config: OpenApiSpecConfig) => |
| 107 | Effect.gen(function* () { | 116 | Effect.gen(function* () { |
| 108 | const doc = yield* parse(config.spec); | 117 | const doc = yield* parse(config.spec); |
| @@ -121,7 +130,7 @@ export const openApiPlugin = (options?: { · 12 unmodified lines | |||
| 121 | const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers); | 130 | const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers); |
| 122 | const invocationConfig = new InvocationConfig({ | 131 | const invocationConfig = new InvocationConfig({ |
| 123 | baseUrl, | 132 | baseUrl, |
| 124 | auth: config.auth ?? new NoAuth(), | 133 | headers: config.headers ?? {}, |
| 125 | }); | 134 | }); |
| 126 | 135 | ||
| 127 | const registrations = result.operations.map((op) => | 136 | const registrations = result.operations.map((op) => |
| @@ -0,0 +1,192 @@ | |||
| 1 | import { Effect, Option } from "effect"; | ||
| 2 | import { Schema } from "effect"; | ||
| 3 | |||
| 4 | import { parse } from "./parse"; | ||
| 5 | import { extract } from "./extract"; | ||
| 6 | import type { ExtractionResult } from "./types"; | ||
| 7 | |||
| 8 | // --------------------------------------------------------------------------- | ||
| 9 | // Security scheme — what the spec declares it needs | ||
| 10 | // --------------------------------------------------------------------------- | ||
| 11 | |||
| 12 | export class SecurityScheme extends Schema.Class<SecurityScheme>("SecurityScheme")({ | ||
| 13 | /** Key name in components.securitySchemes (e.g. "api_token") */ | ||
| 14 | name: Schema.String, | ||
| 15 | /** OpenAPI security scheme type */ | ||
| 16 | type: Schema.Literal("http", "apiKey", "oauth2", "openIdConnect"), | ||
| 17 | /** For type: "http" — e.g. "bearer", "basic" */ | ||
| 18 | scheme: Schema.optionalWith(Schema.String, { as: "Option" }), | ||
| 19 | /** For type: "apiKey" — where the key goes */ | ||
| 20 | in: Schema.optionalWith(Schema.Literal("header", "query", "cookie"), { as: "Option" }), | ||
| 21 | /** For type: "apiKey" — the header/query/cookie name */ | ||
| 22 | headerName: Schema.optionalWith(Schema.String, { as: "Option" }), | ||
| 23 | description: Schema.optionalWith(Schema.String, { as: "Option" }), | ||
| 24 | }) {} | ||
| 25 | |||
| 26 | // --------------------------------------------------------------------------- | ||
| 27 | // Auth strategy — a valid combination of security schemes | ||
| 28 | // --------------------------------------------------------------------------- | ||
| 29 | |||
| 30 | export class AuthStrategy extends Schema.Class<AuthStrategy>("AuthStrategy")({ | ||
| 31 | /** The security schemes required together for this strategy */ | ||
| 32 | schemes: Schema.Array(Schema.String), | ||
| 33 | }) {} | ||
| 34 | |||
| 35 | // --------------------------------------------------------------------------- | ||
| 36 | // Header preset — derived from an auth strategy | ||
| 37 | // --------------------------------------------------------------------------- | ||
| 38 | |||
| 39 | export class HeaderPreset extends Schema.Class<HeaderPreset>("HeaderPreset")({ | ||
| 40 | /** Human-readable label for the UI (e.g. "Bearer Token", "API Key + Email") */ | ||
| 41 | label: Schema.String, | ||
| 42 | /** Headers this strategy needs. Value is null when the user must provide it. */ | ||
| 43 | headers: Schema.Record({ key: Schema.String, value: Schema.NullOr(Schema.String) }), | ||
| 44 | /** Which headers should be stored as secrets */ | ||
| 45 | secretHeaders: Schema.Array(Schema.String), | ||
| 46 | }) {} | ||
| 47 | |||
| 48 | // --------------------------------------------------------------------------- | ||
| 49 | // Spec preview — everything the frontend needs | ||
| 50 | // --------------------------------------------------------------------------- | ||
| 51 | |||
| 52 | export class SpecPreview extends Schema.Class<SpecPreview>("SpecPreview")({ | ||
| 53 | title: Schema.optionalWith(Schema.String, { as: "Option" }), | ||
| 54 | version: Schema.optionalWith(Schema.String, { as: "Option" }), | ||
| 55 | /** Reuses ServerInfo from extraction */ | ||
| 56 | servers: Schema.Array(Schema.Unknown), | ||
| 57 | operationCount: Schema.Number, | ||
| 58 | tags: Schema.Array(Schema.String), | ||
| 59 | securitySchemes: Schema.Array(SecurityScheme), | ||
| 60 | /** Valid auth strategies (each is a set of schemes used together) */ | ||
| 61 | authStrategies: Schema.Array(AuthStrategy), | ||
| 62 | /** Pre-built header presets derived from auth strategies */ | ||
| 63 | headerPresets: Schema.Array(HeaderPreset), | ||
| 64 | }) {} | ||
| 65 | |||
| 66 | // --------------------------------------------------------------------------- | ||
| 67 | // Security scheme extraction | ||
| 68 | // --------------------------------------------------------------------------- | ||
| 69 | |||
| 70 | const extractSecuritySchemes = ( | ||
| 71 | rawSchemes: Record<string, unknown>, | ||
| 72 | ): SecurityScheme[] => | ||
| 73 | Object.entries(rawSchemes).flatMap(([name, schemeOrRef]) => { | ||
| 74 | if (!schemeOrRef || typeof schemeOrRef !== "object") return []; | ||
| 75 | const scheme = schemeOrRef as Record<string, unknown>; | ||
| 76 | if ("$ref" in scheme) return []; | ||
| 77 | |||
| 78 | const type = scheme.type as string; | ||
| 79 | if (!["http", "apiKey", "oauth2", "openIdConnect"].includes(type)) return []; | ||
| 80 | |||
| 81 | return [ | ||
| 82 | new SecurityScheme({ | ||
| 83 | name, | ||
| 84 | type: type as "http" | "apiKey" | "oauth2" | "openIdConnect", | ||
| 85 | scheme: Option.fromNullable(scheme.scheme as string | undefined), | ||
| 86 | in: Option.fromNullable( | ||
| 87 | scheme.in as "header" | "query" | "cookie" | undefined, | ||
| 88 | ), | ||
| 89 | headerName: Option.fromNullable(scheme.name as string | undefined), | ||
| 90 | description: Option.fromNullable(scheme.description as string | undefined), | ||
| 91 | }), | ||
| 92 | ]; | ||
| 93 | }); | ||
| 94 | |||
| 95 | // --------------------------------------------------------------------------- | ||
| 96 | // Header preset builder | ||
| 97 | // --------------------------------------------------------------------------- | ||
| 98 | |||
| 99 | const buildHeaderPresets = ( | ||
| 100 | schemes: readonly SecurityScheme[], | ||
| 101 | strategies: readonly AuthStrategy[], | ||
| 102 | ): HeaderPreset[] => { | ||
| 103 | const schemeMap = new Map(schemes.map((s) => [s.name, s])); | ||
| 104 | |||
| 105 | return strategies.flatMap((strategy) => { | ||
| 106 | const resolved = strategy.schemes | ||
| 107 | .map((name) => schemeMap.get(name)) | ||
| 108 | .filter((s): s is SecurityScheme => s !== undefined); | ||
| 109 | |||
| 110 | if (resolved.length === 0) return []; | ||
| 111 | |||
| 112 | const headers: Record<string, string | null> = {}; | ||
| 113 | const secretHeaders: string[] = []; | ||
| 114 | const labelParts: string[] = []; | ||
| 115 | |||
| 116 | for (const scheme of resolved) { | ||
| 117 | if (scheme.type === "http" && Option.getOrElse(scheme.scheme, () => "") === "bearer") { | ||
| 118 | headers["Authorization"] = null; | ||
| 119 | secretHeaders.push("Authorization"); | ||
| 120 | labelParts.push("Bearer Token"); | ||
| 121 | } else if (scheme.type === "http" && Option.getOrElse(scheme.scheme, () => "") === "basic") { | ||
| 122 | headers["Authorization"] = null; | ||
| 123 | secretHeaders.push("Authorization"); | ||
| 124 | labelParts.push("Basic Auth"); | ||
| 125 | } else if (scheme.type === "apiKey" && Option.getOrElse(scheme.in, () => "") === "header") { | ||
| 126 | const headerName = Option.getOrElse(scheme.headerName, () => scheme.name); | ||
| 127 | headers[headerName] = null; | ||
| 128 | secretHeaders.push(headerName); | ||
| 129 | labelParts.push(scheme.name); | ||
| 130 | } else if (scheme.type === "apiKey") { | ||
| 131 | labelParts.push(`${scheme.name} (${Option.getOrElse(scheme.in, () => "unknown")})`); | ||
| 132 | } else { | ||
| 133 | labelParts.push(scheme.name); | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | if (Object.keys(headers).length === 0 && resolved.length > 0) { | ||
| 138 | return [ | ||
| 139 | new HeaderPreset({ label: labelParts.join(" + "), headers: {}, secretHeaders: [] }), | ||
| 140 | ]; | ||
| 141 | } | ||
| 142 | |||
| 143 | return [ | ||
| 144 | new HeaderPreset({ label: labelParts.join(" + "), headers, secretHeaders }), | ||
| 145 | ]; | ||
| 146 | }); | ||
| 147 | }; | ||
| 148 | |||
| 149 | // --------------------------------------------------------------------------- | ||
| 150 | // Collect unique tags from extraction result | ||
| 151 | // --------------------------------------------------------------------------- | ||
| 152 | |||
| 153 | const collectTags = (result: ExtractionResult): string[] => { | ||
| 154 | const tagSet = new Set<string>(); | ||
| 155 | for (const op of result.operations) { | ||
| 156 | for (const tag of op.tags) tagSet.add(tag); | ||
| 157 | } | ||
| 158 | return [...tagSet].sort(); | ||
| 159 | }; | ||
| 160 | |||
| 161 | // --------------------------------------------------------------------------- | ||
| 162 | // Public API | ||
| 163 | // --------------------------------------------------------------------------- | ||
| 164 | |||
| 165 | /** Preview an OpenAPI spec — extract metadata without registering anything. | ||
| 166 | * Reuses parse() + extract() for the heavy lifting. */ | ||
| 167 | export const previewSpec = Effect.fn("OpenApi.previewSpec")(function* ( | ||
| 168 | specText: string, | ||
| 169 | ) { | ||
| 170 | const doc = yield* parse(specText); | ||
| 171 | const result = yield* extract(doc); | ||
| 172 | |||
| 173 | const securitySchemes = extractSecuritySchemes( | ||
| 174 | doc.components?.securitySchemes ?? {}, | ||
| 175 | ); | ||
| 176 | |||
| 177 | const rawSecurity = (doc.security ?? []) as Array<Record<string, unknown>>; | ||
| 178 | const authStrategies = rawSecurity.map( | ||
| 179 | (entry) => new AuthStrategy({ schemes: Object.keys(entry) }), | ||
| 180 | ); | ||
| 181 | |||
| 182 | return new SpecPreview({ | ||
| 183 | title: result.title, | ||
| 184 | version: result.version, | ||
| 185 | servers: result.servers as unknown as readonly unknown[], | ||
| 186 | operationCount: result.operations.length, | ||
| 187 | tags: collectTags(result), | ||
| 188 | securitySchemes, | ||
| 189 | authStrategies, | ||
| 190 | headerPresets: buildHeaderPresets(securitySchemes, authStrategies), | ||
| 191 | }); | ||
| 192 | }); | ||
| @@ -6,6 +6,7 @@ import { resolve } from "node:path"; · 5 unmodified lines | |||
| 6 | import type { ParsedDocument } from "./parse"; | 6 | import type { ParsedDocument } from "./parse"; |
| 7 | import { parse } from "./parse"; | 7 | import { parse } from "./parse"; |
| 8 | import { extract } from "./extract"; | 8 | import { extract } from "./extract"; |
| 9 | import { previewSpec } from "./preview"; | ||
| 9 | import type { ExtractionResult } from "./types"; | 10 | import type { ExtractionResult } from "./types"; |
| 10 | import { createExecutor, makeTestConfig } from "@executor/sdk"; | 11 | import { createExecutor, makeTestConfig } from "@executor/sdk"; |
| 11 | import { openApiPlugin } from "./plugin"; | 12 | import { openApiPlugin } from "./plugin"; |
| @@ -158,6 +159,42 @@ describe("Real specs: Cloudflare API", () => { · 146 unmodified lines | |||
| 158 | }), | 159 | }), |
| 159 | ); | 160 | ); |
| 160 | 161 | ||
| 162 | it.effect("previewSpec returns security schemes and header presets", () => | ||
| 163 | Effect.gen(function* () { | ||
| 164 | const preview = yield* previewSpec(specText); | ||
| 165 | |||
| 166 | expect(preview.operationCount).toBeGreaterThan(1000); | ||
| 167 | expect(Option.isSome(preview.title)).toBe(true); | ||
| 168 | expect(preview.servers.length).toBeGreaterThan(0); | ||
| 169 | |||
| 170 | // Cloudflare has 4 security schemes | ||
| 171 | expect(preview.securitySchemes.length).toBe(4); | ||
| 172 | const schemeNames = preview.securitySchemes.map((s) => s.name); | ||
| 173 | expect(schemeNames).toContain("api_token"); | ||
| 174 | expect(schemeNames).toContain("api_key"); | ||
| 175 | expect(schemeNames).toContain("api_email"); | ||
| 176 | |||
| 177 | // Should have header presets derived from security strategies | ||
| 178 | expect(preview.headerPresets.length).toBeGreaterThan(0); | ||
| 179 | |||
| 180 | // Bearer token preset should include Authorization header | ||
| 181 | const bearerPreset = preview.headerPresets.find((p) => | ||
| 182 | p.label.includes("Bearer"), | ||
| 183 | ); | ||
| 184 | expect(bearerPreset).toBeDefined(); | ||
| 185 | expect(bearerPreset!.headers["Authorization"]).toBeNull(); // user must provide | ||
| 186 | expect(bearerPreset!.secretHeaders).toContain("Authorization"); | ||
| 187 | |||
| 188 | // API key + email preset | ||
| 189 | const keyEmailPreset = preview.headerPresets.find((p) => | ||
| 190 | p.label.includes("api_email"), | ||
| 191 | ); | ||
| 192 | expect(keyEmailPreset).toBeDefined(); | ||
| 193 | expect(keyEmailPreset!.headers["X-Auth-Email"]).toBeNull(); | ||
| 194 | expect(keyEmailPreset!.headers["X-Auth-Key"]).toBeNull(); | ||
| 195 | }), | ||
| 196 | ); | ||
| 197 | |||
| 161 | it.effect("removeSpec cleans up all Cloudflare tools", () => | 198 | it.effect("removeSpec cleans up all Cloudflare tools", () => |
| 162 | Effect.gen(function* () { | 199 | Effect.gen(function* () { |
| 163 | const executor = yield* createExecutor( | 200 | const executor = yield* createExecutor( |
| @@ -7,33 +7,6 @@ import { Schema } from "effect"; · 6 unmodified lines | |||
| 7 | export const OperationId = Schema.String.pipe(Schema.brand("OperationId")); | 7 | export const OperationId = Schema.String.pipe(Schema.brand("OperationId")); |
| 8 | export type OperationId = typeof OperationId.Type; | 8 | export type OperationId = typeof OperationId.Type; |
| 9 | 9 | ||
| 10 | // --------------------------------------------------------------------------- | ||
| 11 | // Auth | ||
| 12 | // --------------------------------------------------------------------------- | ||
| 13 | |||
| 14 | export class NoAuth extends Schema.TaggedClass<NoAuth>()("NoAuth", {}) {} | ||
| 15 | |||
| 16 | export class BearerAuth extends Schema.TaggedClass<BearerAuth>()( | ||
| 17 | "BearerAuth", | ||
| 18 | { | ||
| 19 | token: Schema.String, | ||
| 20 | headerName: Schema.optionalWith(Schema.String, { default: () => "Authorization" }), | ||
| 21 | prefix: Schema.optionalWith(Schema.String, { default: () => "Bearer " }), | ||
| 22 | }, | ||
| 23 | ) {} | ||
| 24 | |||
| 25 | export class ApiKeyAuth extends Schema.TaggedClass<ApiKeyAuth>()( | ||
| 26 | "ApiKeyAuth", | ||
| 27 | { | ||
| 28 | name: Schema.String, | ||
| 29 | value: Schema.String, | ||
| 30 | in: Schema.Literal("header", "query", "cookie"), | ||
| 31 | }, | ||
| 32 | ) {} | ||
| 33 | |||
| 34 | export const AuthConfig = Schema.Union(NoAuth, BearerAuth, ApiKeyAuth); | ||
| 35 | export type AuthConfig = typeof AuthConfig.Type; | ||
| 36 | |||
| 37 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
| 38 | // HTTP | 11 | // HTTP |
| 39 | // --------------------------------------------------------------------------- | 12 | // --------------------------------------------------------------------------- |
| @@ -125,7 +98,11 @@ export class InvocationConfig extends Schema.Class<InvocationConfig>( · 85 unmodified lines | |||
| 125 | "InvocationConfig", | 98 | "InvocationConfig", |
| 126 | )({ | 99 | )({ |
| 127 | baseUrl: Schema.String, | 100 | baseUrl: Schema.String, |
| 128 | auth: AuthConfig, | 101 | /** Static headers applied to every request (auth, custom headers, etc.) */ |
| 102 | headers: Schema.optionalWith( | ||
| 103 | Schema.Record({ key: Schema.String, value: Schema.String }), | ||
| 104 | { default: () => ({}) }, | ||
| 105 | ), | ||
| 129 | }) {} | 106 | }) {} |
| 130 | 107 | ||
| 131 | export class InvocationResult extends Schema.Class<InvocationResult>( | 108 | export class InvocationResult extends Schema.Class<InvocationResult>( |