blume 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -12
  3. package/dist/cli/index.js +449 -135
  4. package/dist/cli/index.js.map +24 -23
  5. package/dist/types/ai/ask-context.d.ts +78 -0
  6. package/dist/types/core/config-input.d.ts +54 -2
  7. package/dist/types/core/data.d.ts +19 -2
  8. package/dist/types/core/open-in-chat.d.ts +9 -0
  9. package/dist/types/core/schema.d.ts +48 -1
  10. package/dist/types/core/types.d.ts +10 -3
  11. package/dist/types/openapi/references.d.ts +9 -0
  12. package/dist/types/search/orama-index.d.ts +70 -0
  13. package/dist/types/theme/fonts.d.ts +11 -2
  14. package/docs/advanced/api-reference.mdx +67 -5
  15. package/docs/advanced/custom-pages.mdx +5 -1
  16. package/docs/configuration/ai.mdx +35 -0
  17. package/docs/configuration/index.mdx +14 -2
  18. package/docs/configuration/search.mdx +4 -4
  19. package/docs/configuration/theming.mdx +4 -2
  20. package/docs/reference/cli.mdx +2 -2
  21. package/package.json +1 -1
  22. package/skills/blume-migrate/SKILL.md +1 -1
  23. package/skills/blume-migrate/references/mintlify.md +1 -1
  24. package/src/ai/ask-context.ts +51 -11
  25. package/src/ai/mcp/data.ts +3 -2
  26. package/src/ai/mcp/server.ts +3 -2
  27. package/src/assets/icon-dark.png +0 -0
  28. package/src/astro/generate.ts +172 -18
  29. package/src/astro/templates.ts +89 -15
  30. package/src/components/content/AccordionItem.astro +4 -0
  31. package/src/components/content/Update.astro +3 -0
  32. package/src/components/islands/AskAI.astro +6 -0
  33. package/src/components/islands/ask-ai.tsx +39 -9
  34. package/src/components/layout/Analytics.astro +9 -1
  35. package/src/components/layout/Favicon.astro +29 -8
  36. package/src/components/layout/Fonts.astro +23 -3
  37. package/src/components/layout/Header.astro +2 -2
  38. package/src/components/layout/NavSelector.astro +1 -1
  39. package/src/components/layout/PageActions.astro +120 -78
  40. package/src/components/layout/PageFeedback.astro +12 -3
  41. package/src/components/layout/PageLayout.astro +79 -5
  42. package/src/components/layout/ReferenceLayout.astro +12 -9
  43. package/src/components/layout/RootLayout.astro +153 -121
  44. package/src/components/layout/Search.astro +41 -26
  45. package/src/components/layout/drawer-inert.ts +10 -5
  46. package/src/components/layout/head-scripts.ts +34 -16
  47. package/src/components/layout/nav-utils.ts +34 -15
  48. package/src/components/layout/search/orama.ts +3 -2
  49. package/src/components/openapi/AsyncApiOperation.astro +22 -7
  50. package/src/components/openapi/MessageComposer.astro +238 -0
  51. package/src/components/openapi/Operation.astro +26 -12
  52. package/src/components/openapi/PanelTabs.astro +7 -0
  53. package/src/components/openapi/Playground.astro +320 -0
  54. package/src/components/openapi/RequestPanel.astro +1 -0
  55. package/src/components/openapi/async-snippets.ts +20 -7
  56. package/src/components/openapi/async.ts +13 -2
  57. package/src/components/openapi/message-composer.ts +242 -0
  58. package/src/components/openapi/message-model.ts +108 -0
  59. package/src/components/openapi/message.ts +153 -0
  60. package/src/components/openapi/operation-model.ts +260 -0
  61. package/src/components/openapi/playground-client.ts +486 -0
  62. package/src/components/openapi/playground-schema.ts +109 -0
  63. package/src/components/openapi/request.ts +287 -0
  64. package/src/components/openapi/security.ts +0 -56
  65. package/src/components/openapi/snippets.ts +23 -136
  66. package/src/components/openapi/validate-json.ts +144 -0
  67. package/src/components/openapi/ws-client.ts +194 -0
  68. package/src/core/config-input.ts +67 -1
  69. package/src/core/content-assets.ts +66 -15
  70. package/src/core/data.ts +16 -2
  71. package/src/core/last-modified.ts +76 -2
  72. package/src/core/links.ts +30 -4
  73. package/src/core/navigation.ts +26 -1
  74. package/src/core/open-in-chat.ts +17 -0
  75. package/src/core/project-graph.ts +11 -0
  76. package/src/core/schema.ts +60 -1
  77. package/src/core/server-features.ts +11 -0
  78. package/src/core/sources/normalize.ts +10 -2
  79. package/src/core/types.ts +10 -3
  80. package/src/deploy/vercel-negotiation.ts +34 -14
  81. package/src/og/card.ts +3 -1
  82. package/src/openapi/model.ts +7 -0
  83. package/src/openapi/proxy.ts +217 -0
  84. package/src/openapi/references.ts +8 -0
  85. package/src/openapi/source.ts +13 -0
  86. package/src/registry/eject.ts +4 -5
  87. package/src/search/orama-index.ts +109 -36
  88. package/src/theme/entry.ts +15 -2
  89. package/src/theme/fonts.ts +75 -3
@@ -0,0 +1,108 @@
1
+ import type {
2
+ AsyncApiAction,
3
+ AsyncApiServerObject,
4
+ } from "../../openapi/asyncapi.ts";
5
+ import type { AsyncApiMessageLike, NamedMessage } from "./async.ts";
6
+ import { payloadSchema } from "./async.ts";
7
+ import { exampleValue, toJson } from "./helpers.ts";
8
+ import type { ParameterLike, SchemaLike } from "./helpers.ts";
9
+ import type {
10
+ MessageModel,
11
+ MessageParam,
12
+ MessagePayload,
13
+ MessageServerOption,
14
+ } from "./message.ts";
15
+ import { inputValue, validationSchema } from "./playground-schema.ts";
16
+
17
+ /**
18
+ * Server-side derivation of the event composer's message model — the AsyncAPI
19
+ * counterpart of `operation-model.ts`. The spec document, the sampler, and the
20
+ * channel/server resolution meet here once at build time; the resulting
21
+ * {@link MessageModel} is embedded as JSON on the operation page so the lazy
22
+ * client chunk carries none of it.
23
+ */
24
+
25
+ /** The protocols a docs page can actually open a live connection to. */
26
+ const LIVE_PROTOCOLS = { ws: true } satisfies Record<string, true>;
27
+
28
+ /**
29
+ * Channel parameters as composer inputs. `channelParameters` has already
30
+ * lowered AsyncAPI's string-only parameters (enum, default, examples) into the
31
+ * shared parameter shape, so all that is left is picking a prefill: the
32
+ * declared default, first example, or first enum member, else nothing. Never
33
+ * the sampler — its `{type:"string"}` output is the literal word `string`,
34
+ * which would replace every `{name}` template in the address samples with
35
+ * junk like `user/string/signedup`.
36
+ */
37
+ const messageParams = (parameters: ParameterLike[]): MessageParam[] =>
38
+ parameters.map((parameter) => ({
39
+ description: parameter.description,
40
+ enum: parameter.schema?.enum?.map(String),
41
+ name: parameter.name ?? "",
42
+ value: inputValue(
43
+ parameter.schema?.default ??
44
+ parameter.example ??
45
+ parameter.schema?.enum?.[0]
46
+ ),
47
+ }));
48
+
49
+ /**
50
+ * The payload editor's state for the operation's first message. Multi-message
51
+ * operations render every message's schema on the page, but the composer sends
52
+ * one payload — the first is the one the samples already show.
53
+ */
54
+ const messagePayload = (
55
+ message: AsyncApiMessageLike | undefined,
56
+ schemas: Record<string, SchemaLike>
57
+ ): MessagePayload => {
58
+ if (!message) {
59
+ return { example: "" };
60
+ }
61
+ const schema = payloadSchema(message);
62
+ // `undefined` means the message declares no example, `null` means it declares
63
+ // an empty one — only the former falls back to a schema sample. The sampler's
64
+ // own `null` is its no-schema/failure sentinel, not a value a reader typed:
65
+ // it degrades to an empty editor (which samples and sends `{}`) rather than
66
+ // prefilling the literal `null`.
67
+ const declared = message.examples?.[0]?.payload;
68
+ const example =
69
+ declared === undefined ? exampleValue(schema, schemas) : declared;
70
+ return {
71
+ contentType: message.contentType,
72
+ example:
73
+ declared === undefined && example === null ? "" : (toJson(example) ?? ""),
74
+ schema: validationSchema(schema, schemas),
75
+ };
76
+ };
77
+
78
+ /**
79
+ * A server's base URL for the picker. Falls back to the bare host when the
80
+ * spec omits `protocol` — a label is a label, and the snippet builders read
81
+ * the server object itself rather than this string.
82
+ */
83
+ const serverOption = (server: AsyncApiServerObject): MessageServerOption => {
84
+ const host = `${server.host ?? ""}${server.pathname ?? ""}`;
85
+ return {
86
+ label: server.protocol ? `${server.protocol}://${host}` : host,
87
+ server,
88
+ };
89
+ };
90
+
91
+ /** Derive the composer's message model for one event operation, at build time. */
92
+ export const messageModel = (args: {
93
+ action: AsyncApiAction;
94
+ address: string;
95
+ messages: NamedMessage[];
96
+ parameters: ParameterLike[];
97
+ protocol?: string;
98
+ schemas: Record<string, SchemaLike>;
99
+ servers: AsyncApiServerObject[];
100
+ }): MessageModel => ({
101
+ action: args.action,
102
+ address: args.address,
103
+ connectable: Object.hasOwn(LIVE_PROTOCOLS, args.protocol ?? ""),
104
+ params: messageParams(args.parameters),
105
+ payload: messagePayload(args.messages[0]?.message, args.schemas),
106
+ protocol: args.protocol,
107
+ servers: args.servers.map(serverOption),
108
+ });
@@ -0,0 +1,153 @@
1
+ import type {
2
+ AsyncApiAction,
3
+ AsyncApiServerObject,
4
+ } from "../../openapi/asyncapi.ts";
5
+ import type { MessageSample } from "./async-snippets.ts";
6
+ import type { ValidationSchema } from "./request.ts";
7
+
8
+ /**
9
+ * Framework-free message model for the event ("Try it") composer — the
10
+ * AsyncAPI counterpart of `request.ts`. The composer form, the protocol-aware
11
+ * code samples, and the live WebSocket frame all derive from one
12
+ * {@link buildMessage} call, so a copied `wscat` command and a composed send
13
+ * carry the same address and the same payload bytes by construction.
14
+ *
15
+ * Ships to the browser inside the composer's lazy chunk: no spec parser, no
16
+ * sampler, no Astro imports.
17
+ */
18
+
19
+ /** One channel parameter as a composer input. Channel params are always required. */
20
+ export interface MessageParam {
21
+ description?: string;
22
+ /** Declared `enum` members, when the parameter constrains its values. */
23
+ enum?: string[];
24
+ name: string;
25
+ /** Prefill from the parameter's `default`/`examples`; "" when none. */
26
+ value: string;
27
+ }
28
+
29
+ /** The composer's payload editor state. */
30
+ export interface MessagePayload {
31
+ /** The message's declared `contentType`, when it has one. */
32
+ contentType?: string;
33
+ /** Pretty-printed prefill: declared example, else a schema sample; "" when none. */
34
+ example: string;
35
+ /** Pruned payload schema the editor validates against, when derivable. */
36
+ schema?: ValidationSchema;
37
+ }
38
+
39
+ /** One server option in the picker: a display URL plus the spec object behind it. */
40
+ export interface MessageServerOption {
41
+ /** `protocol://host+pathname` — what the reader picks between. */
42
+ label: string;
43
+ /** The spec server, verbatim; the snippet builders read host/pathname/protocol. */
44
+ server: AsyncApiServerObject;
45
+ }
46
+
47
+ export interface MessageModel {
48
+ action: AsyncApiAction;
49
+ /** Channel address with `{param}` templates intact, e.g. `user/{id}/signedup`. */
50
+ address: string;
51
+ /**
52
+ * Whether the panel offers a live connection. Only WebSocket bindings can be
53
+ * driven from a docs page — Kafka, MQTT and friends get the composer and the
54
+ * CLI samples rather than faked connectivity.
55
+ */
56
+ connectable: boolean;
57
+ params: MessageParam[];
58
+ payload: MessagePayload;
59
+ /** Normalized binding key (`ws`, `kafka`, `mqtt`, …) when the spec implies one. */
60
+ protocol?: string;
61
+ servers: MessageServerOption[];
62
+ }
63
+
64
+ export interface MessageValues {
65
+ /** Free-text server URL; overrides the picked server when non-empty. */
66
+ customUrl: string;
67
+ /** Key: parameter name. */
68
+ params: Record<string, string>;
69
+ /** Raw payload editor text. */
70
+ payload: string;
71
+ /** Index into `model.servers`; out of range falls back to the first. */
72
+ server: number;
73
+ }
74
+
75
+ /** Values pre-filled from the model's precomputed examples. */
76
+ export const defaultMessageValues = (model: MessageModel): MessageValues => ({
77
+ customUrl: "",
78
+ params: Object.fromEntries(
79
+ model.params.map((param) => [param.name, param.value])
80
+ ),
81
+ payload: model.payload.example,
82
+ server: 0,
83
+ });
84
+
85
+ const PARAM_TEMPLATE = /\{(?<name>[^{}]+)\}/gu;
86
+
87
+ /**
88
+ * A free-text base URL lowered onto the server shape the snippet builders and
89
+ * the connect URL read. A full URL contributes its scheme, host, and path.
90
+ * Anything without an authority — a bare `host:port`, which is how broker
91
+ * tooling is usually addressed, and which `new URL` happily reads as a scheme
92
+ * plus path — is kept whole as the host, leaving the protocol to the spec.
93
+ */
94
+ const customServer = (url: string): AsyncApiServerObject => {
95
+ let parsed: URL | undefined;
96
+ try {
97
+ parsed = new URL(url);
98
+ } catch {
99
+ parsed = undefined;
100
+ }
101
+ if (!parsed || parsed.host === "") {
102
+ return { host: url };
103
+ }
104
+ return {
105
+ host: parsed.host,
106
+ pathname: parsed.pathname === "/" ? "" : parsed.pathname,
107
+ protocol: parsed.protocol.replace(":", ""),
108
+ };
109
+ };
110
+
111
+ /**
112
+ * THE one message builder: the composer's samples, its copy buttons, and the
113
+ * live WebSocket frame all consume its output. Parameter values are
114
+ * URL-encoded into the address; a parameter left blank keeps its `{name}`
115
+ * template so the sample still reads as a template rather than a broken
116
+ * address. Payload text that doesn't parse yields no payload — the editor is
117
+ * showing the reader a validation error at that moment, and neither a sample
118
+ * nor a send should invent a value.
119
+ */
120
+ export const buildMessage = (
121
+ model: MessageModel,
122
+ values: MessageValues
123
+ ): MessageSample => {
124
+ const custom = values.customUrl.trim();
125
+ const picked = model.servers[values.server] ?? model.servers[0];
126
+ let payload: unknown;
127
+ try {
128
+ // SAFETY: JSON.parse returns `any`; widening it to `unknown` claims
129
+ // nothing about the shape and forces consumers to narrow.
130
+ payload = JSON.parse(values.payload) as unknown;
131
+ } catch {
132
+ payload = undefined;
133
+ }
134
+ return {
135
+ action: model.action,
136
+ address: model.address.replaceAll(PARAM_TEMPLATE, (template, name) => {
137
+ const value = values.params[String(name)] ?? "";
138
+ return value === "" ? template : encodeURIComponent(value);
139
+ }),
140
+ payload,
141
+ server: custom === "" ? picked?.server : customServer(custom),
142
+ };
143
+ };
144
+
145
+ /**
146
+ * The exact bytes a live send transmits. Identical to the payload the `wscat`
147
+ * and browser-`WebSocket` samples embed, which is the whole point: what a
148
+ * reader copies is what Send puts on the wire. Only an absent payload
149
+ * (unparseable editor text) degrades to `{}` — a `null` payload is a value the
150
+ * reader typed, so it goes out as `null`.
151
+ */
152
+ export const messageFrame = (sample: MessageSample): string =>
153
+ JSON.stringify(sample.payload === undefined ? {} : sample.payload);
@@ -0,0 +1,260 @@
1
+ import {
2
+ exampleValue,
3
+ objectProperties,
4
+ resolveSchema,
5
+ toJson,
6
+ } from "./helpers.ts";
7
+ import type { ParameterLike, SchemaLike, SpecValue } from "./helpers.ts";
8
+ import {
9
+ declaredTypes,
10
+ inputValue,
11
+ scalarType,
12
+ validationSchema,
13
+ } from "./playground-schema.ts";
14
+ import type {
15
+ PlaygroundAuthInput,
16
+ PlaygroundBody,
17
+ PlaygroundBodyField,
18
+ PlaygroundModel,
19
+ PlaygroundParam,
20
+ } from "./request.ts";
21
+ import { schemeLabel } from "./security.ts";
22
+ import type { OperationSecurity, ResolvedScheme } from "./security.ts";
23
+
24
+ /**
25
+ * Server-side derivation of the playground's request model. This is where the
26
+ * spec document, openapi-sampler, and the security resolution meet — once, at
27
+ * build time. The resulting `PlaygroundModel` is embedded as JSON on the page,
28
+ * so the client (`request.ts`) never needs any of those dependencies.
29
+ */
30
+
31
+ interface MediaTypeLike {
32
+ schema?: SchemaLike;
33
+ example?: unknown;
34
+ }
35
+
36
+ /** Primitive types the flat-body fields UI can edit directly. */
37
+ const PRIMITIVE_TYPES = {
38
+ boolean: true,
39
+ integer: true,
40
+ number: true,
41
+ string: true,
42
+ } as const;
43
+
44
+ /** Whether a spec example is a plain object usable for per-field defaults. */
45
+ const isExampleObject = (
46
+ value: SpecValue
47
+ ): value is Record<string, SpecValue> =>
48
+ typeof value === "object" && value !== null && !Array.isArray(value);
49
+
50
+ /**
51
+ * Playground inputs for the operation's parameters. Cookie params are skipped
52
+ * (not supported in v1 — browsers won't let a page set arbitrary cookies).
53
+ * Path params are always required per spec, even when a lax document omits the
54
+ * flag. Only REQUIRED params get a precomputed example; optional ones default
55
+ * to "" so the default samples carry required params only — parity with the
56
+ * old static samples.
57
+ */
58
+ const modelParams = (
59
+ parameters: ParameterLike[],
60
+ schemas: Record<string, SchemaLike>
61
+ ): PlaygroundParam[] => {
62
+ const params: PlaygroundParam[] = [];
63
+ for (const param of parameters) {
64
+ const where = param.in;
65
+ if (
66
+ !param.name ||
67
+ (where !== "path" && where !== "query" && where !== "header")
68
+ ) {
69
+ continue;
70
+ }
71
+ const required = where === "path" ? true : param.required === true;
72
+ const schema = resolveSchema(schemas, param.schema);
73
+ params.push({
74
+ description: param.description,
75
+ enum: schema.enum?.map(String),
76
+ in: where,
77
+ name: param.name,
78
+ required,
79
+ type: scalarType(param.schema, schemas),
80
+ value: required
81
+ ? inputValue(param.example ?? exampleValue(param.schema, schemas))
82
+ : "",
83
+ });
84
+ }
85
+ return params;
86
+ };
87
+
88
+ /** The JSON-ish media entry: first whose type mentions json, else the first. */
89
+ const jsonContentType = (
90
+ content: Record<string, MediaTypeLike> | undefined
91
+ ): [string, MediaTypeLike] | undefined => {
92
+ const entries = Object.entries(content ?? {});
93
+ return entries.find(([type]) => type.includes("json")) ?? entries[0];
94
+ };
95
+
96
+ /**
97
+ * Typed field inputs when the body schema is a flat object of primitives —
98
+ * anything nested (object/array properties) falls back to the raw JSON editor,
99
+ * where structure is easier to edit than in exploded form fields.
100
+ */
101
+ const bodyFields = (
102
+ schema: SchemaLike | undefined,
103
+ schemas: Record<string, SchemaLike>,
104
+ example: SpecValue
105
+ ): PlaygroundBodyField[] | undefined => {
106
+ const resolved = resolveSchema(schemas, schema);
107
+ if (declaredTypes(resolved.type).some((type) => type !== "object")) {
108
+ return undefined;
109
+ }
110
+ const { properties, required } = objectProperties(resolved, schemas);
111
+ if (properties.length === 0) {
112
+ return undefined;
113
+ }
114
+ const defaults = isExampleObject(example) ? example : undefined;
115
+ const fields: PlaygroundBodyField[] = [];
116
+ for (const [name, property] of properties) {
117
+ const propertySchema = resolveSchema(schemas, property);
118
+ const type = scalarType(property, schemas);
119
+ if (
120
+ !(type in PRIMITIVE_TYPES) ||
121
+ propertySchema.properties ||
122
+ propertySchema.items
123
+ ) {
124
+ return undefined;
125
+ }
126
+ fields.push({
127
+ description: propertySchema.description,
128
+ enum: propertySchema.enum?.map(String),
129
+ name,
130
+ required: required.has(name),
131
+ type,
132
+ value: inputValue(defaults?.[name]),
133
+ });
134
+ }
135
+ return fields;
136
+ };
137
+
138
+ /** The playground's body editor state, when the operation takes a request body. */
139
+ const modelBody = (
140
+ requestBody: { content?: Record<string, MediaTypeLike> } | undefined,
141
+ schemas: Record<string, SchemaLike>
142
+ ): PlaygroundBody | undefined => {
143
+ const media = jsonContentType(requestBody?.content);
144
+ if (!media) {
145
+ return undefined;
146
+ }
147
+ const [contentType, mediaType] = media;
148
+ // SAFETY: `example` comes from the parsed spec document (YAML/JSON), whose
149
+ // values are exactly the JSON-shaped tree `SpecValue` models.
150
+ const exampleData =
151
+ (mediaType.example as SpecValue) ?? exampleValue(mediaType.schema, schemas);
152
+ return {
153
+ contentType,
154
+ example: toJson(exampleData) ?? "",
155
+ fields: bodyFields(mediaType.schema, schemas, exampleData),
156
+ schema: validationSchema(mediaType.schema, schemas),
157
+ };
158
+ };
159
+
160
+ const AUTHORIZATION_HEADER = { in: "header", name: "Authorization" } as const;
161
+
162
+ /**
163
+ * One resolved security scheme -> the playground input that collects its
164
+ * credential. Mutual TLS travels outside the request and an unknown ref can't
165
+ * be guessed — both contribute nothing.
166
+ */
167
+ const authInput = (
168
+ resolved: ResolvedScheme
169
+ ): PlaygroundAuthInput | undefined => {
170
+ const { scheme } = resolved;
171
+ const label = schemeLabel(resolved);
172
+ switch (scheme?.type) {
173
+ case "http": {
174
+ const kind = (scheme.scheme ?? "bearer").toLowerCase();
175
+ if (kind === "basic") {
176
+ return {
177
+ carrier: AUTHORIZATION_HEADER,
178
+ id: resolved.key,
179
+ kind: "basic",
180
+ label,
181
+ placeholder: "YOUR_CREDENTIALS",
182
+ prefix: "Basic ",
183
+ };
184
+ }
185
+ if (kind === "bearer") {
186
+ return {
187
+ carrier: AUTHORIZATION_HEADER,
188
+ id: resolved.key,
189
+ kind: "bearer",
190
+ label,
191
+ placeholder: "YOUR_TOKEN",
192
+ prefix: "Bearer ",
193
+ };
194
+ }
195
+ // Digest and friends: a paste field like bearer, scheme-name prefix.
196
+ return {
197
+ carrier: AUTHORIZATION_HEADER,
198
+ id: resolved.key,
199
+ kind: "bearer",
200
+ label,
201
+ placeholder: "YOUR_CREDENTIALS",
202
+ prefix: `${kind.charAt(0).toUpperCase() + kind.slice(1)} `,
203
+ };
204
+ }
205
+ case "oauth2":
206
+ case "openIdConnect": {
207
+ // Token paste, no flow — the playground doesn't run OAuth dances.
208
+ return {
209
+ carrier: AUTHORIZATION_HEADER,
210
+ id: resolved.key,
211
+ kind: "oauth2",
212
+ label,
213
+ placeholder: "YOUR_ACCESS_TOKEN",
214
+ prefix: "Bearer ",
215
+ };
216
+ }
217
+ case "apiKey": {
218
+ const where = scheme.in;
219
+ return {
220
+ carrier: {
221
+ in: where === "query" || where === "cookie" ? where : "header",
222
+ name: scheme.name ?? resolved.key,
223
+ },
224
+ id: resolved.key,
225
+ kind: "apiKey",
226
+ label,
227
+ placeholder: "YOUR_API_KEY",
228
+ prefix: "",
229
+ };
230
+ }
231
+ default: {
232
+ return undefined;
233
+ }
234
+ }
235
+ };
236
+
237
+ /** Derive the playground request model for one operation, at build time. */
238
+ export const operationModel = (args: {
239
+ method: string;
240
+ path: string;
241
+ /** Pre-merged/resolved (`mergeParameters` output). */
242
+ parameters: ParameterLike[];
243
+ requestBody?: { content?: Record<string, MediaTypeLike> };
244
+ servers: { url?: string }[];
245
+ schemas: Record<string, SchemaLike>;
246
+ security: OperationSecurity;
247
+ }): PlaygroundModel => ({
248
+ // First alternative only — the spec's preferred way to authorize, matching
249
+ // what the static samples always showed.
250
+ auth: (args.security.alternatives[0] ?? []).flatMap((resolved) => {
251
+ const input = authInput(resolved);
252
+ return input ? [input] : [];
253
+ }),
254
+ authOptional: args.security.optional,
255
+ body: modelBody(args.requestBody, args.schemas),
256
+ method: args.method.toUpperCase(),
257
+ params: modelParams(args.parameters, args.schemas),
258
+ path: args.path,
259
+ servers: args.servers.map((server) => server.url ?? ""),
260
+ });