History

Extract OAuth2 flows from OpenAPI security schemes (#250)

Exposes the `flows` object on `components.securitySchemes` through the preview pipeline so the onboarding UI can offer an OAuth2 path for any spec that declares one. Previously we detected `type: oauth2` but silently dropped the flow metadata, leaving users to paste a bearer token manually. Schema additions: - `OAuth2AuthorizationCodeFlow`, `OAuth2ClientCredentialsFlow`, and `OAuth2Flows` classes capturing `authorizationUrl`, `tokenUrl`, `refreshUrl`, and `scopes` per flow. `implicit` and `password` are intentionally excluded — both are deprecated by OAuth 2.1 and adding them would only encourage users to onboard onto them. - `SecurityScheme` now carries `flows`, `bearerFormat`, and `openIdConnectUrl` (previously dropped). - `OAuth2Preset` class — one entry per (scheme × flow) pair with the label, URLs, and scope map the onboarding UI needs to render a "Connect via OAuth2" flow with a scope checklist. - `SpecPreview.oauth2Presets` carries the derived presets. Extraction: - `extractSecuritySchemes` now takes a `DocResolver` and resolves `$ref` entries instead of silently dropping them. Specs that alias their security schemes through `components.securitySchemes` refs (rare but real) now surface correctly. - `extractFlows` tolerates malformed flows (missing `tokenUrl` / `authorizationUrl`) by dropping them, so a broken flow does not prevent the rest of the spec from previewing. 5 new tests cover authorizationCode + clientCredentials extraction, the `$ref` resolution path, `bearerFormat` / `openIdConnectUrl` capture, and the malformed-flow ignore case.

Rhys Sullivan committed 6 months ago

Commit bb6dfa51 parent 1a15f723 files · +384 −6Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/sdk/index.ts +4 −0
  2. packages/plugins/openapi/src/sdk/preview-oauth2.test.ts +192 −0
  3. packages/plugins/openapi/src/sdk/preview.ts +188 −6
packages/plugins/openapi/src/sdk/index.ts+4−0
@@ -15,6 +15,10 @@ export { · 14 unmodified lines
15 SecurityScheme,15 SecurityScheme,
16 AuthStrategy,16 AuthStrategy,
17 HeaderPreset,17 HeaderPreset,
18 OAuth2Preset,
19 OAuth2Flows,
20 OAuth2AuthorizationCodeFlow,
21 OAuth2ClientCredentialsFlow,
18 PreviewOperation,22 PreviewOperation,
19 SpecPreview,23 SpecPreview,
20} from "./preview";24} from "./preview";
packages/plugins/openapi/src/sdk/preview-oauth2.test.ts+192−0
@@ -0,0 +1,192 @@
1// ---------------------------------------------------------------------------
2// Tests for OAuth2 flow extraction in previewSpec. Covers:
3// - authorizationCode flow extraction (URLs + scopes)
4// - clientCredentials flow extraction
5// - security schemes defined via $ref are no longer silently dropped
6// - bearerFormat / openIdConnectUrl are captured
7// - invalid flows (missing tokenUrl) are ignored
8// ---------------------------------------------------------------------------
9
10import { describe, expect, it } from "@effect/vitest";
11import { Effect, Option } from "effect";
12
13import { previewSpec } from "./preview";
14
15const minimalSpec = (
16 securitySchemes: Record<string, unknown>,
17 components: Record<string, unknown> = {},
18) => ({
19 openapi: "3.0.0",
20 info: { title: "Test API", version: "1.0.0" },
21 servers: [{ url: "https://api.example.com" }],
22 paths: {
23 "/ping": {
24 get: { responses: { "200": { description: "ok" } } },
25 },
26 },
27 components: { ...components, securitySchemes },
28});
29
30describe("previewSpec OAuth2 extraction", () => {
31 it.effect("extracts authorizationCode flow with URLs, scopes, refreshUrl", () =>
32 Effect.gen(function* () {
33 const spec = minimalSpec({
34 oauth_app: {
35 type: "oauth2",
36 flows: {
37 authorizationCode: {
38 authorizationUrl: "https://example.com/oauth/authorize",
39 tokenUrl: "https://example.com/oauth/token",
40 refreshUrl: "https://example.com/oauth/refresh",
41 scopes: {
42 read: "Read access",
43 write: "Write access",
44 },
45 },
46 },
47 },
48 });
49 const preview = yield* previewSpec(JSON.stringify(spec));
50
51 expect(preview.securitySchemes).toHaveLength(1);
52 const scheme = preview.securitySchemes[0]!;
53 expect(scheme.name).toBe("oauth_app");
54 expect(scheme.type).toBe("oauth2");
55 expect(Option.isSome(scheme.flows)).toBe(true);
56 const flows = Option.getOrThrow(scheme.flows);
57 expect(Option.isSome(flows.authorizationCode)).toBe(true);
58 const flow = Option.getOrThrow(flows.authorizationCode);
59 expect(flow.authorizationUrl).toBe("https://example.com/oauth/authorize");
60 expect(flow.tokenUrl).toBe("https://example.com/oauth/token");
61 expect(Option.getOrElse(flow.refreshUrl, () => "")).toBe(
62 "https://example.com/oauth/refresh",
63 );
64 expect(flow.scopes).toEqual({ read: "Read access", write: "Write access" });
65
66 // A preset should be generated for this flow.
67 expect(preview.oauth2Presets).toHaveLength(1);
68 const preset = preview.oauth2Presets[0]!;
69 expect(preset.flow).toBe("authorizationCode");
70 expect(preset.securitySchemeName).toBe("oauth_app");
71 expect(preset.tokenUrl).toBe("https://example.com/oauth/token");
72 expect(preset.label).toContain("Authorization Code");
73 expect(preset.label).toContain("oauth_app");
74 expect(preset.scopes).toEqual({ read: "Read access", write: "Write access" });
75 }),
76 );
77
78 it.effect("extracts clientCredentials flow alongside authorizationCode", () =>
79 Effect.gen(function* () {
80 const spec = minimalSpec({
81 oauth_app: {
82 type: "oauth2",
83 flows: {
84 authorizationCode: {
85 authorizationUrl: "https://example.com/auth",
86 tokenUrl: "https://example.com/token",
87 scopes: {},
88 },
89 clientCredentials: {
90 tokenUrl: "https://example.com/token",
91 scopes: { "admin:read": "Admin read" },
92 },
93 },
94 },
95 });
96 const preview = yield* previewSpec(JSON.stringify(spec));
97
98 expect(preview.oauth2Presets).toHaveLength(2);
99 const flowKinds = preview.oauth2Presets.map((p) => p.flow).sort();
100 expect(flowKinds).toEqual(["authorizationCode", "clientCredentials"]);
101
102 const cc = preview.oauth2Presets.find((p) => p.flow === "clientCredentials")!;
103 expect(Option.isNone(cc.authorizationUrl)).toBe(true);
104 expect(cc.scopes).toEqual({ "admin:read": "Admin read" });
105 }),
106 );
107
108 it.effect("skips authorizationCode flow missing tokenUrl or authorizationUrl", () =>
109 Effect.gen(function* () {
110 const spec = minimalSpec({
111 broken: {
112 type: "oauth2",
113 flows: {
114 authorizationCode: {
115 authorizationUrl: "https://example.com/auth",
116 // tokenUrl missing
117 scopes: {},
118 },
119 },
120 },
121 });
122 const preview = yield* previewSpec(JSON.stringify(spec));
123
124 expect(preview.securitySchemes).toHaveLength(1);
125 // Scheme is still captured but with no flows.
126 expect(Option.isNone(preview.securitySchemes[0]!.flows)).toBe(true);
127 expect(preview.oauth2Presets).toHaveLength(0);
128 }),
129 );
130
131 it.effect("resolves security schemes defined via $ref", () =>
132 Effect.gen(function* () {
133 const spec = minimalSpec(
134 {
135 api_token: { $ref: "#/components/securitySchemes/_api_token_impl" },
136 },
137 {
138 securitySchemes: {
139 _api_token_impl: {
140 type: "http",
141 scheme: "bearer",
142 bearerFormat: "JWT",
143 description: "Internal token scheme",
144 },
145 },
146 },
147 );
148 // Note: the outer securitySchemes at `components.securitySchemes` is
149 // what previewSpec reads; the `_api_token_impl` shim inside
150 // components.securitySchemes allows $ref resolution via the resolver.
151 // The test spec above is slightly awkward because we have to nest both
152 // under the same key — adjust by merging.
153 spec.components = {
154 securitySchemes: {
155 api_token: { $ref: "#/components/securitySchemes/_api_token_impl" },
156 _api_token_impl: {
157 type: "http",
158 scheme: "bearer",
159 bearerFormat: "JWT",
160 description: "Internal token scheme",
161 },
162 },
163 };
164
165 const preview = yield* previewSpec(JSON.stringify(spec));
166 // Both keys are present, but the `api_token` entry should resolve to
167 // the http bearer scheme (previously it was silently dropped).
168 const apiToken = preview.securitySchemes.find((s) => s.name === "api_token");
169 expect(apiToken).toBeDefined();
170 expect(apiToken!.type).toBe("http");
171 expect(Option.getOrElse(apiToken!.scheme, () => "")).toBe("bearer");
172 expect(Option.getOrElse(apiToken!.bearerFormat, () => "")).toBe("JWT");
173 }),
174 );
175
176 it.effect("captures openIdConnectUrl for openIdConnect schemes", () =>
177 Effect.gen(function* () {
178 const spec = minimalSpec({
179 oidc: {
180 type: "openIdConnect",
181 openIdConnectUrl: "https://example.com/.well-known/openid-configuration",
182 },
183 });
184 const preview = yield* previewSpec(JSON.stringify(spec));
185 const scheme = preview.securitySchemes[0]!;
186 expect(scheme.type).toBe("openIdConnect");
187 expect(Option.getOrElse(scheme.openIdConnectUrl, () => "")).toBe(
188 "https://example.com/.well-known/openid-configuration",
189 );
190 }),
191 );
192});
packages/plugins/openapi/src/sdk/preview.ts+188−6
@@ -1,10 +1,41 @@
1import { Effect, Option } from "effect";1import { Effect, Option } from "effect";
2import { Schema } from "effect";2import { Schema } from "effect";
3 3
4import { parse } from "./parse";4import { parse, type ParsedDocument } from "./parse";
5import { extract } from "./extract";5import { extract } from "./extract";
6import { DocResolver } from "./openapi-utils";
6import { HttpMethod, ServerInfo, type ExtractionResult } from "./types";7import { HttpMethod, ServerInfo, type ExtractionResult } from "./types";
7 8
9
10// ---------------------------------------------------------------------------
11// OAuth 2.0 flows — one entry per supported grant type
12// ---------------------------------------------------------------------------
13
14/** Scopes declared by a flow: `{ scopeName: description }` */
15const OAuth2Scopes = Schema.Record({ key: Schema.String, value: Schema.String });
16
17export class OAuth2AuthorizationCodeFlow extends Schema.Class<OAuth2AuthorizationCodeFlow>(
18 "OAuth2AuthorizationCodeFlow",
19)({
20 authorizationUrl: Schema.String,
21 tokenUrl: Schema.String,
22 refreshUrl: Schema.optionalWith(Schema.String, { as: "Option" }),
23 scopes: OAuth2Scopes,
24}) {}
25
26export class OAuth2ClientCredentialsFlow extends Schema.Class<OAuth2ClientCredentialsFlow>(
27 "OAuth2ClientCredentialsFlow",
28)({
29 tokenUrl: Schema.String,
30 refreshUrl: Schema.optionalWith(Schema.String, { as: "Option" }),
31 scopes: OAuth2Scopes,
32}) {}
33
34export class OAuth2Flows extends Schema.Class<OAuth2Flows>("OAuth2Flows")({
35 authorizationCode: Schema.optionalWith(OAuth2AuthorizationCodeFlow, { as: "Option" }),
36 clientCredentials: Schema.optionalWith(OAuth2ClientCredentialsFlow, { as: "Option" }),
37}) {}
38
8// ---------------------------------------------------------------------------39// ---------------------------------------------------------------------------
9// Security scheme — what the spec declares it needs40// Security scheme — what the spec declares it needs
10// ---------------------------------------------------------------------------41// ---------------------------------------------------------------------------
@@ -16,11 +47,17 @@ export class SecurityScheme extends Schema.Class<SecurityScheme>("SecurityScheme · 5 unmodified lines
16 type: Schema.Literal("http", "apiKey", "oauth2", "openIdConnect"),47 type: Schema.Literal("http", "apiKey", "oauth2", "openIdConnect"),
17 /** For type: "http" — e.g. "bearer", "basic" */48 /** For type: "http" — e.g. "bearer", "basic" */
18 scheme: Schema.optionalWith(Schema.String, { as: "Option" }),49 scheme: Schema.optionalWith(Schema.String, { as: "Option" }),
50 /** For type: "http" with scheme "bearer" — e.g. "JWT" */
51 bearerFormat: Schema.optionalWith(Schema.String, { as: "Option" }),
19 /** For type: "apiKey" — where the key goes */52 /** For type: "apiKey" — where the key goes */
20 in: Schema.optionalWith(Schema.Literal("header", "query", "cookie"), { as: "Option" }),53 in: Schema.optionalWith(Schema.Literal("header", "query", "cookie"), { as: "Option" }),
21 /** For type: "apiKey" — the header/query/cookie name */54 /** For type: "apiKey" — the header/query/cookie name */
22 headerName: Schema.optionalWith(Schema.String, { as: "Option" }),55 headerName: Schema.optionalWith(Schema.String, { as: "Option" }),
23 description: Schema.optionalWith(Schema.String, { as: "Option" }),56 description: Schema.optionalWith(Schema.String, { as: "Option" }),
57 /** For type: "oauth2" — declared flows (authorizationCode / clientCredentials only; implicit and password are deprecated). */
58 flows: Schema.optionalWith(OAuth2Flows, { as: "Option" }),
59 /** For type: "openIdConnect" — the discovery URL. */
60 openIdConnectUrl: Schema.optionalWith(Schema.String, { as: "Option" }),
24}) {}61}) {}
25 62
26// ---------------------------------------------------------------------------63// ---------------------------------------------------------------------------
@@ -45,6 +82,27 @@ export class HeaderPreset extends Schema.Class<HeaderPreset>("HeaderPreset")({ · 18 unmodified lines
45 secretHeaders: Schema.Array(Schema.String),82 secretHeaders: Schema.Array(Schema.String),
46}) {}83}) {}
47 84
85// ---------------------------------------------------------------------------
86// OAuth2 preset — derived from an oauth2 security scheme + a flow choice
87// ---------------------------------------------------------------------------
88
89export class OAuth2Preset extends Schema.Class<OAuth2Preset>("OAuth2Preset")({
90 /** Human-readable label for the UI (e.g. "OAuth2 (Authorization Code) — oauth_app") */
91 label: Schema.String,
92 /** The source security scheme this preset came from (components.securitySchemes key). */
93 securitySchemeName: Schema.String,
94 /** Which OAuth2 flow this preset uses. */
95 flow: Schema.Literal("authorizationCode", "clientCredentials"),
96 /** For authorizationCode: user-agent redirect URL (from the spec). */
97 authorizationUrl: Schema.optionalWith(Schema.String, { as: "Option" }),
98 /** Token endpoint to exchange the code / refresh. */
99 tokenUrl: Schema.String,
100 /** Optional refresh endpoint if the spec declares one separately. */
101 refreshUrl: Schema.optionalWith(Schema.String, { as: "Option" }),
102 /** Declared scopes for this flow: `{ scope: description }`. */
103 scopes: Schema.Record({ key: Schema.String, value: Schema.String }),
104}) {}
105
48// ---------------------------------------------------------------------------106// ---------------------------------------------------------------------------
49// Preview operation — lightweight shape for the add-source UI list107// Preview operation — lightweight shape for the add-source UI list
50// ---------------------------------------------------------------------------108// ---------------------------------------------------------------------------
@@ -76,17 +134,87 @@ export class SpecPreview extends Schema.Class<SpecPreview>("SpecPreview")({ · 25 unmodified lines
76 authStrategies: Schema.Array(AuthStrategy),134 authStrategies: Schema.Array(AuthStrategy),
77 /** Pre-built header presets derived from auth strategies */135 /** Pre-built header presets derived from auth strategies */
78 headerPresets: Schema.Array(HeaderPreset),136 headerPresets: Schema.Array(HeaderPreset),
137 /** OAuth2 presets — one per (oauth2 scheme × supported flow) combination */
138 oauth2Presets: Schema.Array(OAuth2Preset),
79}) {}139}) {}
80 140
81// ---------------------------------------------------------------------------141// ---------------------------------------------------------------------------
82// Security scheme extraction142// Security scheme extraction
83// ---------------------------------------------------------------------------143// ---------------------------------------------------------------------------
84 144
85const extractSecuritySchemes = (rawSchemes: Record<string, unknown>): SecurityScheme[] =>145const stringRecord = (value: unknown): Record<string, string> => {
146 if (!value || typeof value !== "object" || Array.isArray(value)) return {};
147 const out: Record<string, string> = {};
148 for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
149 if (typeof v === "string") out[k] = v;
150 }
151 return out;
152};
153
154const extractFlows = (rawFlows: unknown): Option.Option<OAuth2Flows> => {
155 if (!rawFlows || typeof rawFlows !== "object") return Option.none();
156 const flows = rawFlows as Record<string, unknown>;
157
158 const parseFlow = <K extends "authorizationCode" | "clientCredentials">(
159 key: K,
160 ): unknown => flows[key];
161
162 let authorizationCode: Option.Option<OAuth2AuthorizationCodeFlow> = Option.none();
163 const authCodeRaw = parseFlow("authorizationCode");
164 if (authCodeRaw && typeof authCodeRaw === "object") {
165 const f = authCodeRaw as Record<string, unknown>;
166 const authUrl = typeof f.authorizationUrl === "string" ? f.authorizationUrl : null;
167 const tokenUrl = typeof f.tokenUrl === "string" ? f.tokenUrl : null;
168 if (authUrl && tokenUrl) {
169 authorizationCode = Option.some(
170 new OAuth2AuthorizationCodeFlow({
171 authorizationUrl: authUrl,
172 tokenUrl,
173 refreshUrl: Option.fromNullable(
174 typeof f.refreshUrl === "string" ? f.refreshUrl : undefined,
175 ),
176 scopes: stringRecord(f.scopes),
177 }),
178 );
179 }
180 }
181
182 let clientCredentials: Option.Option<OAuth2ClientCredentialsFlow> = Option.none();
183 const ccRaw = parseFlow("clientCredentials");
184 if (ccRaw && typeof ccRaw === "object") {
185 const f = ccRaw as Record<string, unknown>;
186 const tokenUrl = typeof f.tokenUrl === "string" ? f.tokenUrl : null;
187 if (tokenUrl) {
188 clientCredentials = Option.some(
189 new OAuth2ClientCredentialsFlow({
190 tokenUrl,
191 refreshUrl: Option.fromNullable(
192 typeof f.refreshUrl === "string" ? f.refreshUrl : undefined,
193 ),
194 scopes: stringRecord(f.scopes),
195 }),
196 );
197 }
198 }
199
200 if (Option.isNone(authorizationCode) && Option.isNone(clientCredentials)) {
201 return Option.none();
202 }
203 return Option.some(new OAuth2Flows({ authorizationCode, clientCredentials }));
204};
205
206const extractSecuritySchemes = (
207 rawSchemes: Record<string, unknown>,
208 resolver: DocResolver,
209): SecurityScheme[] =>
86 Object.entries(rawSchemes).flatMap(([name, schemeOrRef]) => {210 Object.entries(rawSchemes).flatMap(([name, schemeOrRef]) => {
87 if (!schemeOrRef || typeof schemeOrRef !== "object") return [];211 if (!schemeOrRef || typeof schemeOrRef !== "object") return [];
88 const scheme = schemeOrRef as Record<string, unknown>;212 // Resolve $ref so schemes defined via `$ref` aren't silently dropped.
89 if ("$ref" in scheme) return [];213 const resolved = resolver.resolve<Record<string, unknown>>(
214 schemeOrRef as Record<string, unknown>,
215 );
216 if (!resolved || typeof resolved !== "object") return [];
217 const scheme = resolved;
90 218
91 const type = scheme.type as string;219 const type = scheme.type as string;
92 if (!["http", "apiKey", "oauth2", "openIdConnect"].includes(type)) return [];220 if (!["http", "apiKey", "oauth2", "openIdConnect"].includes(type)) return [];
@@ -96,9 +224,14 @@ const extractSecuritySchemes = (rawSchemes: Record<string, unknown>): SecuritySc · 3 unmodified lines
96 name,224 name,
97 type: type as "http" | "apiKey" | "oauth2" | "openIdConnect",225 type: type as "http" | "apiKey" | "oauth2" | "openIdConnect",
98 scheme: Option.fromNullable(scheme.scheme as string | undefined),226 scheme: Option.fromNullable(scheme.scheme as string | undefined),
227 bearerFormat: Option.fromNullable(scheme.bearerFormat as string | undefined),
99 in: Option.fromNullable(scheme.in as "header" | "query" | "cookie" | undefined),228 in: Option.fromNullable(scheme.in as "header" | "query" | "cookie" | undefined),
100 headerName: Option.fromNullable(scheme.name as string | undefined),229 headerName: Option.fromNullable(scheme.name as string | undefined),
101 description: Option.fromNullable(scheme.description as string | undefined),230 description: Option.fromNullable(scheme.description as string | undefined),
231 flows: type === "oauth2" ? extractFlows(scheme.flows) : Option.none(),
232 openIdConnectUrl: Option.fromNullable(
233 scheme.openIdConnectUrl as string | undefined,
234 ),
102 }),235 }),
103 ];236 ];
104 });237 });
@@ -153,6 +286,50 @@ const buildHeaderPresets = ( · 48 unmodified lines
153 });286 });
154};287};
155 288
289// ---------------------------------------------------------------------------
290// OAuth2 preset builder
291// ---------------------------------------------------------------------------
292
293const buildOAuth2Presets = (schemes: readonly SecurityScheme[]): OAuth2Preset[] => {
294 const presets: OAuth2Preset[] = [];
295 for (const scheme of schemes) {
296 if (scheme.type !== "oauth2") continue;
297 if (Option.isNone(scheme.flows)) continue;
298 const flows = scheme.flows.value;
299
300 if (Option.isSome(flows.authorizationCode)) {
301 const flow = flows.authorizationCode.value;
302 presets.push(
303 new OAuth2Preset({
304 label: `OAuth2 Authorization Code · ${scheme.name}`,
305 securitySchemeName: scheme.name,
306 flow: "authorizationCode",
307 authorizationUrl: Option.some(flow.authorizationUrl),
308 tokenUrl: flow.tokenUrl,
309 refreshUrl: flow.refreshUrl,
310 scopes: flow.scopes,
311 }),
312 );
313 }
314
315 if (Option.isSome(flows.clientCredentials)) {
316 const flow = flows.clientCredentials.value;
317 presets.push(
318 new OAuth2Preset({
319 label: `OAuth2 Client Credentials · ${scheme.name}`,
320 securitySchemeName: scheme.name,
321 flow: "clientCredentials",
322 authorizationUrl: Option.none(),
323 tokenUrl: flow.tokenUrl,
324 refreshUrl: flow.refreshUrl,
325 scopes: flow.scopes,
326 }),
327 );
328 }
329 }
330 return presets;
331};
332
156// ---------------------------------------------------------------------------333// ---------------------------------------------------------------------------
157// Collect unique tags from extraction result334// Collect unique tags from extraction result
158// ---------------------------------------------------------------------------335// ---------------------------------------------------------------------------
@@ -172,10 +349,14 @@ const collectTags = (result: ExtractionResult): string[] => { · 13 unmodified lines
172/** Preview an OpenAPI spec — extract metadata without registering anything.349/** Preview an OpenAPI spec — extract metadata without registering anything.
173 * Reuses parse() + extract() for the heavy lifting. */350 * Reuses parse() + extract() for the heavy lifting. */
174export const previewSpec = Effect.fn("OpenApi.previewSpec")(function* (specText: string) {351export const previewSpec = Effect.fn("OpenApi.previewSpec")(function* (specText: string) {
175 const doc = yield* parse(specText);352 const doc: ParsedDocument = yield* parse(specText);
176 const result = yield* extract(doc);353 const result = yield* extract(doc);
177 354
178 const securitySchemes = extractSecuritySchemes(doc.components?.securitySchemes ?? {});355 const resolver = new DocResolver(doc);
356 const securitySchemes = extractSecuritySchemes(
357 doc.components?.securitySchemes ?? {},
358 resolver,
359 );
179 360
180 const rawSecurity = (doc.security ?? []) as Array<Record<string, unknown>>;361 const rawSecurity = (doc.security ?? []) as Array<Record<string, unknown>>;
181 const authStrategies = rawSecurity.map(362 const authStrategies = rawSecurity.map(
@@ -202,5 +383,6 @@ export const previewSpec = Effect.fn("OpenApi.previewSpec")(function* (specText: · 20 unmodified lines
202 securitySchemes,383 securitySchemes,
203 authStrategies,384 authStrategies,
204 headerPresets: buildHeaderPresets(securitySchemes, authStrategies),385 headerPresets: buildHeaderPresets(securitySchemes, authStrategies),
386 oauth2Presets: buildOAuth2Presets(securitySchemes),
205 });387 });
206});388});