blume 1.4.3 → 1.5.1

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 (204) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -12
  3. package/dist/cli/index.js +1784 -633
  4. package/dist/cli/index.js.map +111 -106
  5. package/dist/types/ai/component-markdown.d.ts +14 -4
  6. package/dist/types/core/config-input.d.ts +80 -28
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +19 -3
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +112 -15
  12. package/dist/types/core/sources/types.d.ts +3 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +6 -5
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/dist/types/theme/fonts.d.ts +11 -2
  19. package/docs/advanced/api-reference.mdx +8 -6
  20. package/docs/advanced/custom-pages.mdx +5 -1
  21. package/docs/configuration/index.mdx +1 -1
  22. package/docs/configuration/search.mdx +2 -0
  23. package/docs/configuration/seo.mdx +1 -1
  24. package/docs/configuration/theming.mdx +4 -2
  25. package/docs/content/i18n.mdx +1 -1
  26. package/docs/content/meta.mdx +2 -1
  27. package/docs/content/meta.ts +1 -0
  28. package/docs/content/navigation.mdx +35 -1
  29. package/docs/content/versioning.mdx +106 -0
  30. package/docs/reference/cli.mdx +2 -1
  31. package/docs/reference/frontmatter.mdx +3 -0
  32. package/package.json +3 -1
  33. package/skills/blume-migrate/SKILL.md +2 -2
  34. package/skills/blume-migrate/references/docusaurus.md +1 -1
  35. package/skills/blume-migrate/references/fumadocs.md +1 -1
  36. package/skills/blume-migrate/references/mintlify.md +1 -1
  37. package/src/ai/agent-readability.ts +37 -10
  38. package/src/ai/ask-context.ts +5 -1
  39. package/src/ai/ask.ts +10 -1
  40. package/src/ai/component-markdown.ts +80 -43
  41. package/src/ai/llms.ts +40 -16
  42. package/src/ai/mcp/data.ts +48 -12
  43. package/src/ai/mcp/discovery.ts +28 -11
  44. package/src/ai/mcp/server.ts +183 -38
  45. package/src/ai/mcp/tools.ts +3 -3
  46. package/src/ai/skills.ts +32 -9
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +6 -2
  50. package/src/astro/generate.ts +64 -34
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +16 -9
  53. package/src/astro/templates.ts +181 -40
  54. package/src/audit/agent.ts +2 -2
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +37 -9
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +5 -2
  64. package/src/audit/snapshot.ts +2 -4
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +9 -4
  68. package/src/cli/commands/build.ts +15 -9
  69. package/src/cli/commands/dev.ts +2 -0
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eval.ts +7 -3
  72. package/src/cli/commands/init.ts +9 -9
  73. package/src/cli/commands/mcp-stdio.ts +3 -0
  74. package/src/cli/commands/translate.ts +14 -3
  75. package/src/cli/commands/version.ts +85 -0
  76. package/src/cli/dev-lock.ts +31 -10
  77. package/src/cli/eject-scripts.ts +17 -2
  78. package/src/cli/index.ts +2 -0
  79. package/src/cli/init/questions.ts +1 -1
  80. package/src/cli/init/scaffold.ts +22 -15
  81. package/src/cli/internal-error.ts +1 -0
  82. package/src/components/content/auto-type-table.ts +3 -0
  83. package/src/components/content/diff.ts +9 -5
  84. package/src/components/content/github-info.ts +2 -0
  85. package/src/components/islands/ask-ai.tsx +33 -25
  86. package/src/components/islands/hooks.ts +5 -1
  87. package/src/components/islands/webmcp.ts +49 -12
  88. package/src/components/layout/Fonts.astro +23 -3
  89. package/src/components/layout/Header.astro +25 -1
  90. package/src/components/layout/NavSelector.astro +11 -2
  91. package/src/components/layout/NavTree.astro +4 -2
  92. package/src/components/layout/PageLayout.astro +72 -3
  93. package/src/components/layout/ReferenceLayout.astro +2 -1
  94. package/src/components/layout/RootLayout.astro +20 -1
  95. package/src/components/layout/Search.astro +77 -13
  96. package/src/components/layout/VersionBanner.astro +39 -0
  97. package/src/components/layout/analytics-client.ts +8 -5
  98. package/src/components/layout/hydration-hint.ts +1 -1
  99. package/src/components/layout/nav-utils.ts +1 -4
  100. package/src/components/layout/overrides.ts +25 -12
  101. package/src/components/layout/search/algolia.ts +18 -5
  102. package/src/components/layout/search/endpoint.ts +3 -0
  103. package/src/components/layout/search/flexsearch.ts +23 -7
  104. package/src/components/layout/search/orama-cloud.ts +1 -1
  105. package/src/components/layout/search/orama.ts +4 -1
  106. package/src/components/layout/search/pagefind.ts +2 -0
  107. package/src/components/layout/search/types.ts +13 -1
  108. package/src/components/layout/search/typesense.ts +19 -3
  109. package/src/components/openapi/ApiOverview.astro +32 -6
  110. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  111. package/src/components/openapi/Bindings.astro +89 -0
  112. package/src/components/openapi/MethodBadge.astro +3 -0
  113. package/src/components/openapi/Operation.astro +7 -2
  114. package/src/components/openapi/PanelTabs.astro +131 -0
  115. package/src/components/openapi/ParametersTable.astro +2 -0
  116. package/src/components/openapi/RequestPanel.astro +12 -119
  117. package/src/components/openapi/async-snippets.ts +174 -0
  118. package/src/components/openapi/async.ts +348 -0
  119. package/src/components/openapi/helpers.ts +52 -20
  120. package/src/components/openapi/security.ts +102 -29
  121. package/src/components/openapi/snippets.ts +11 -11
  122. package/src/core/component-overrides.ts +28 -23
  123. package/src/core/config-input.ts +89 -28
  124. package/src/core/config.ts +20 -7
  125. package/src/core/content.ts +3 -1
  126. package/src/core/data.ts +19 -3
  127. package/src/core/define-components.ts +5 -0
  128. package/src/core/diagnostics.ts +46 -38
  129. package/src/core/frontmatter.ts +33 -7
  130. package/src/core/graph.ts +137 -53
  131. package/src/core/i18n-ui.ts +15 -0
  132. package/src/core/i18n.ts +16 -8
  133. package/src/core/last-modified.ts +49 -0
  134. package/src/core/load-module.ts +1 -0
  135. package/src/core/manifest.ts +92 -3
  136. package/src/core/meta.ts +44 -14
  137. package/src/core/nav-diagnostics.ts +3 -3
  138. package/src/core/navigation.ts +247 -67
  139. package/src/core/project-graph.ts +26 -3
  140. package/src/core/schema.ts +214 -68
  141. package/src/core/sources/assets.ts +2 -0
  142. package/src/core/sources/cache.ts +6 -0
  143. package/src/core/sources/github-releases.ts +39 -31
  144. package/src/core/sources/mdx-remote.ts +4 -0
  145. package/src/core/sources/normalize.ts +67 -20
  146. package/src/core/sources/notion.ts +49 -17
  147. package/src/core/sources/portable-text.ts +32 -11
  148. package/src/core/sources/sanity.ts +68 -14
  149. package/src/core/sources/types.ts +4 -0
  150. package/src/core/sources/watch.ts +1 -1
  151. package/src/core/standard-schema.ts +9 -3
  152. package/src/core/text-width.ts +26 -0
  153. package/src/core/tsconfig-aliases.ts +9 -5
  154. package/src/core/types.ts +45 -2
  155. package/src/core/ui-packs/index.ts +9 -1
  156. package/src/core/version-cut.ts +301 -0
  157. package/src/core/version.ts +2 -0
  158. package/src/core/versions.ts +170 -0
  159. package/src/deploy/adapter-output.ts +5 -2
  160. package/src/deploy/cloudflare-negotiation.ts +25 -10
  161. package/src/deploy/sitemap.ts +33 -1
  162. package/src/deploy/vercel-negotiation.ts +45 -18
  163. package/src/eval/report.ts +4 -4
  164. package/src/eval/run.ts +2 -2
  165. package/src/eval/schema.ts +1 -1
  166. package/src/markdown/base-links.ts +6 -6
  167. package/src/markdown/directives.ts +7 -1
  168. package/src/markdown/heading-anchors.ts +17 -6
  169. package/src/markdown/index.ts +73 -24
  170. package/src/markdown/inline-code.ts +14 -2
  171. package/src/markdown/language-icon.ts +6 -2
  172. package/src/markdown/mdast.ts +18 -4
  173. package/src/markdown/package-commands.ts +6 -8
  174. package/src/markdown/table-wrap.ts +4 -1
  175. package/src/markdown/twoslash.ts +2 -0
  176. package/src/og/card.ts +33 -12
  177. package/src/og/derive.ts +43 -27
  178. package/src/openapi/asyncapi.ts +366 -0
  179. package/src/openapi/model.ts +126 -57
  180. package/src/openapi/parse.ts +97 -5
  181. package/src/openapi/references.ts +12 -10
  182. package/src/openapi/render-mdx.ts +73 -34
  183. package/src/openapi/scalar.ts +6 -8
  184. package/src/openapi/source.ts +98 -28
  185. package/src/registry/eject.ts +7 -2
  186. package/src/search/documents.ts +25 -5
  187. package/src/search/facets.ts +7 -5
  188. package/src/search/orama-index.ts +66 -20
  189. package/src/search/popular.ts +10 -5
  190. package/src/search/providers.ts +2 -2
  191. package/src/search/sync/index.ts +2 -0
  192. package/src/search/sync/typesense.ts +4 -2
  193. package/src/seo/jsonld.ts +24 -6
  194. package/src/seo/x-handle.ts +8 -3
  195. package/src/theme/chrome-icons.ts +7 -2
  196. package/src/theme/entry.ts +24 -2
  197. package/src/theme/fonts.ts +83 -7
  198. package/src/theme/icons.ts +4 -2
  199. package/src/theme/palette.ts +22 -14
  200. package/src/translate/meta.ts +15 -6
  201. package/src/translate/report.ts +9 -5
  202. package/src/translate/run.ts +10 -4
  203. package/src/translate/validate.ts +52 -17
  204. package/src/translate/work-list.ts +0 -0
@@ -0,0 +1,348 @@
1
+ import type {
2
+ AsyncApiChannelObject,
3
+ AsyncApiDocument,
4
+ AsyncApiOperationObject,
5
+ AsyncApiRefLike,
6
+ AsyncApiServerObject,
7
+ AsyncApiSpecValue,
8
+ } from "../../openapi/asyncapi.ts";
9
+ import type { ParameterLike, SchemaLike } from "./helpers.ts";
10
+ import { resolveComponentRef } from "./helpers.ts";
11
+
12
+ /**
13
+ * Runtime helpers for the AsyncAPI components — the async counterpart of
14
+ * `helpers.ts`. These operate on the normalized 3.x document behind the
15
+ * `blume:openapi` alias, resolving the ref shapes AsyncAPI adds on top of
16
+ * `#/components/*`: operations point at channels, channel messages may `$ref`
17
+ * `#/components/messages`, and operation messages point *into* a channel
18
+ * (`#/channels/<id>/messages/<name>`). Browser-safe like the rest of the set.
19
+ */
20
+
21
+ /** A permissive view of an AsyncAPI message — only the fields we render. */
22
+ export interface AsyncApiMessageLike extends AsyncApiRefLike {
23
+ name?: string;
24
+ title?: string;
25
+ summary?: string;
26
+ description?: string;
27
+ contentType?: string;
28
+ payload?: AsyncApiSpecValue;
29
+ headers?: AsyncApiSpecValue;
30
+ examples?: {
31
+ name?: string;
32
+ summary?: string;
33
+ payload?: AsyncApiSpecValue;
34
+ headers?: AsyncApiSpecValue;
35
+ }[];
36
+ bindings?: AsyncApiChannelObject["bindings"];
37
+ }
38
+
39
+ /** A message paired with its channel-map key (the fallback display name). */
40
+ export interface NamedMessage {
41
+ key: string;
42
+ message: AsyncApiMessageLike;
43
+ }
44
+
45
+ const isObject = (
46
+ value: AsyncApiSpecValue
47
+ ): value is Record<string, AsyncApiSpecValue> =>
48
+ typeof value === "object" && value !== null && !Array.isArray(value);
49
+
50
+ /** Permissive spec nodes may lie about declared string fields; verify first. */
51
+ const isString = (value: AsyncApiSpecValue): value is string =>
52
+ typeof value === "string";
53
+
54
+ /** Decode a JSON-pointer token: `user~1signedup` -> `user/signedup`. */
55
+ const unescapePointer = (token: string): string =>
56
+ token.replaceAll("~1", "/").replaceAll("~0", "~");
57
+
58
+ const CHANNEL_MESSAGE_REF =
59
+ /^#\/channels\/(?<channel>.+)\/messages\/(?<name>[^/]+)$/u;
60
+
61
+ type Components = AsyncApiDocument["components"];
62
+
63
+ /** Resolve a channel-map message (possibly a components `$ref`) to its object. */
64
+ const channelMessage = (
65
+ raw: AsyncApiRefLike | undefined,
66
+ components: Components
67
+ ): AsyncApiMessageLike | undefined => {
68
+ if (!isObject(raw)) {
69
+ return undefined;
70
+ }
71
+ const resolved = resolveComponentRef(raw, components, "messages");
72
+ // Still a bare `$ref` after resolution means it pointed nowhere useful.
73
+ if (isString(resolved.$ref)) {
74
+ return undefined;
75
+ }
76
+ // SAFETY: `resolved` is a plain message object; the permissive message view
77
+ // only narrows the fields the components render, all of them optional.
78
+ return resolved as AsyncApiMessageLike;
79
+ };
80
+
81
+ /**
82
+ * The messages one operation carries: its own `messages` refs when declared
83
+ * (each pointing into the channel's message map or at a components message),
84
+ * else every message the channel declares. Unresolvable refs are dropped —
85
+ * the schema tables can only render an actual message object.
86
+ */
87
+ export const operationMessages = (
88
+ operation: AsyncApiOperationObject | undefined,
89
+ channel: AsyncApiChannelObject | undefined,
90
+ document: AsyncApiDocument
91
+ ): NamedMessage[] => {
92
+ const { components } = document;
93
+ const messageMap: Record<string, AsyncApiRefLike> = isObject(
94
+ channel?.messages
95
+ )
96
+ ? channel.messages
97
+ : {};
98
+ const refs = operation?.messages;
99
+ if (!Array.isArray(refs) || refs.length === 0) {
100
+ const all: NamedMessage[] = [];
101
+ for (const [key, raw] of Object.entries(messageMap)) {
102
+ const message = channelMessage(raw, components);
103
+ if (message) {
104
+ all.push({ key, message });
105
+ }
106
+ }
107
+ return all;
108
+ }
109
+ const named: NamedMessage[] = [];
110
+ for (const ref of refs) {
111
+ if (!isObject(ref)) {
112
+ continue;
113
+ }
114
+ const pointer = isString(ref.$ref)
115
+ ? CHANNEL_MESSAGE_REF.exec(ref.$ref)?.groups?.name
116
+ : undefined;
117
+ const key = pointer === undefined ? undefined : unescapePointer(pointer);
118
+ // A channel-message pointer resolves through the channel map; anything
119
+ // else (a components ref, an inline message) resolves directly.
120
+ const message =
121
+ key === undefined
122
+ ? channelMessage(ref, components)
123
+ : channelMessage(messageMap[key], components);
124
+ if (message) {
125
+ named.push({ key: key ?? message.name ?? "message", message });
126
+ }
127
+ }
128
+ return named;
129
+ };
130
+
131
+ /** A message's display name: its `name`/`title`, else its channel-map key. */
132
+ export const messageLabel = (named: NamedMessage): string =>
133
+ named.message.title ?? named.message.name ?? named.key;
134
+
135
+ /**
136
+ * The `schemaFormat` media types whose schemas render as JSON Schema: the
137
+ * AsyncAPI Schema Object (a JSON Schema superset), OpenAPI Schema Objects,
138
+ * and JSON Schema itself, in their bare and `+json`/`+yaml` spellings.
139
+ */
140
+ const JSON_SCHEMA_FORMAT =
141
+ /^application\/(?:vnd\.aai\.asyncapi|vnd\.oai\.openapi|schema)(?:\+(?:json|yaml))?$/u;
142
+
143
+ /**
144
+ * The JSON-schema view of a possibly multi-format schema value
145
+ * (`{ schemaFormat, schema }`, allowed on both message payloads and headers).
146
+ * Unwraps when the format is JSON-schema compatible and yields nothing
147
+ * otherwise (an Avro or Protobuf schema can't render as a schema table —
148
+ * callers fall back to a note).
149
+ */
150
+ export const schemaOf = (value: AsyncApiSpecValue): SchemaLike | undefined => {
151
+ if (!isObject(value)) {
152
+ return undefined;
153
+ }
154
+ if (isString(value.schemaFormat) && "schema" in value) {
155
+ // Match the media type proper (parameters like `;version=…` stripped)
156
+ // against the JSON-Schema-compatible formats AsyncAPI registers. A
157
+ // substring test would misclassify e.g. Avro's `+json` encoding, whose
158
+ // schema is JSON but not JSON Schema.
159
+ const format = (value.schemaFormat.split(";")[0] ?? "")
160
+ .trim()
161
+ .toLowerCase();
162
+ const jsonish = JSON_SCHEMA_FORMAT.test(format);
163
+ if (jsonish && isObject(value.schema)) {
164
+ // SAFETY: a JSON-Schema-format schema object; the permissive SchemaLike
165
+ // view only narrows the fields the schema tables render, all optional.
166
+ return value.schema as SchemaLike;
167
+ }
168
+ return undefined;
169
+ }
170
+ // SAFETY: a bare schema object; the permissive SchemaLike view only narrows
171
+ // the fields the schema tables render, all of them optional.
172
+ return value as SchemaLike;
173
+ };
174
+
175
+ /** The JSON-schema view of a message payload; see {@link schemaOf}. */
176
+ export const payloadSchema = (
177
+ message: AsyncApiMessageLike
178
+ ): SchemaLike | undefined => schemaOf(message.payload);
179
+
180
+ /** The channel-parameter fields {@link channelParameters} lowers. */
181
+ interface AsyncApiParameterLike extends AsyncApiRefLike {
182
+ description?: string;
183
+ default?: AsyncApiSpecValue;
184
+ enum?: AsyncApiSpecValue[];
185
+ }
186
+
187
+ /**
188
+ * Channel parameters lowered into the shared parameter-table shape. AsyncAPI
189
+ * 3.x parameters are always strings (`enum`/`default`/`examples`, no schema),
190
+ * and every one is required — an address template can't resolve without it.
191
+ */
192
+ export const channelParameters = (
193
+ channel: AsyncApiChannelObject | undefined,
194
+ document: AsyncApiDocument
195
+ ): ParameterLike[] => {
196
+ const parameters: ParameterLike[] = [];
197
+ const declared: Record<string, AsyncApiRefLike> = channel?.parameters ?? {};
198
+ for (const [name, raw] of Object.entries(declared)) {
199
+ if (!isObject(raw)) {
200
+ continue;
201
+ }
202
+ // SAFETY: the permissive parameter view only narrows the fields lowered
203
+ // below; each one is runtime-checked before use.
204
+ const parameter = resolveComponentRef(
205
+ raw as AsyncApiParameterLike,
206
+ document.components,
207
+ "parameters"
208
+ );
209
+ const schema: SchemaLike = { type: "string" };
210
+ if (Array.isArray(parameter.enum)) {
211
+ schema.enum = parameter.enum;
212
+ }
213
+ if (parameter.default !== undefined) {
214
+ schema.default = parameter.default;
215
+ }
216
+ parameters.push({
217
+ description: isString(parameter.description)
218
+ ? parameter.description
219
+ : undefined,
220
+ in: "channel",
221
+ name,
222
+ required: true,
223
+ schema,
224
+ });
225
+ }
226
+ return parameters;
227
+ };
228
+
229
+ const SERVER_REF = /^#\/servers\/(?<name>[^/]+)$/u;
230
+
231
+ /**
232
+ * The servers a channel is available on: its `servers` refs when declared,
233
+ * else every server the document declares (the spec's default).
234
+ */
235
+ export const channelServers = (
236
+ channel: AsyncApiChannelObject | undefined,
237
+ document: AsyncApiDocument
238
+ ): AsyncApiServerObject[] => {
239
+ const all = document.servers ?? {};
240
+ const refs = channel?.servers;
241
+ if (!Array.isArray(refs) || refs.length === 0) {
242
+ return Object.values(all).filter(isObject);
243
+ }
244
+ const servers: AsyncApiServerObject[] = [];
245
+ for (const ref of refs) {
246
+ const name = SERVER_REF.exec(
247
+ isObject(ref) && isString(ref.$ref) ? ref.$ref : ""
248
+ )?.groups?.name;
249
+ const server = name === undefined ? undefined : all[unescapePointer(name)];
250
+ if (isObject(server)) {
251
+ servers.push(server);
252
+ }
253
+ }
254
+ return servers;
255
+ };
256
+
257
+ /** Normalize protocol spellings onto the binding key they document. */
258
+ const PROTOCOL_ALIASES = {
259
+ "kafka-secure": "kafka",
260
+ mqtt5: "mqtt",
261
+ mqtts: "mqtt",
262
+ "secure-mqtt": "mqtt",
263
+ wss: "ws",
264
+ };
265
+
266
+ /**
267
+ * The protocol an operation speaks, for binding-aware code samples: the first
268
+ * operation/channel binding key, else the first relevant server's `protocol`.
269
+ */
270
+ export const protocolOf = (
271
+ operation: AsyncApiOperationObject | undefined,
272
+ channel: AsyncApiChannelObject | undefined,
273
+ servers: AsyncApiServerObject[]
274
+ ): string | undefined => {
275
+ const declared =
276
+ Object.keys(operation?.bindings ?? {})[0] ??
277
+ Object.keys(channel?.bindings ?? {})[0] ??
278
+ servers.find((server) => isString(server.protocol))?.protocol;
279
+ if (!isString(declared) || declared === "") {
280
+ return undefined;
281
+ }
282
+ const lower = declared.toLowerCase();
283
+ // SAFETY: guarded by the `in` check, `lower` is one of the alias keys.
284
+ return lower in PROTOCOL_ALIASES
285
+ ? PROTOCOL_ALIASES[lower as keyof typeof PROTOCOL_ALIASES]
286
+ : lower;
287
+ };
288
+
289
+ /**
290
+ * The security list an operation actually enforces: its own `security` when
291
+ * declared, else the union of its servers' — connecting already requires the
292
+ * server's schemes. Server entries dedupe by `$ref`, so two servers sharing a
293
+ * scheme render it once.
294
+ */
295
+ export const asyncApiSecurityEntries = (
296
+ operation: AsyncApiOperationObject | undefined,
297
+ servers: AsyncApiServerObject[]
298
+ ): AsyncApiRefLike[] => {
299
+ if (Array.isArray(operation?.security)) {
300
+ return operation.security;
301
+ }
302
+ const entries: AsyncApiRefLike[] = [];
303
+ const seen = new Set<string>();
304
+ for (const server of servers) {
305
+ for (const entry of server.security ?? []) {
306
+ if (!isObject(entry)) {
307
+ continue;
308
+ }
309
+ if (isString(entry.$ref)) {
310
+ if (seen.has(entry.$ref)) {
311
+ continue;
312
+ }
313
+ seen.add(entry.$ref);
314
+ }
315
+ entries.push(entry);
316
+ }
317
+ }
318
+ return entries;
319
+ };
320
+
321
+ /** One protocol's binding fields, ready for a key/value table. */
322
+ export interface BindingGroup {
323
+ protocol: string;
324
+ rows: { name: string; value: unknown }[];
325
+ }
326
+
327
+ /**
328
+ * Binding maps flattened for display, `bindingVersion` (metadata, not
329
+ * behavior) dropped. Values stay unformatted — the component renders schema-ish
330
+ * objects as nested schema tables and everything else as code.
331
+ */
332
+ export const bindingGroups = (
333
+ bindings?: AsyncApiChannelObject["bindings"]
334
+ ): BindingGroup[] => {
335
+ const groups: BindingGroup[] = [];
336
+ for (const [protocol, fields] of Object.entries(bindings ?? {})) {
337
+ if (!isObject(fields)) {
338
+ continue;
339
+ }
340
+ const rows = Object.entries(fields)
341
+ .filter(([name]) => name !== "bindingVersion")
342
+ .map(([name, value]) => ({ name, value }));
343
+ if (rows.length > 0) {
344
+ groups.push({ protocol, rows });
345
+ }
346
+ }
347
+ return groups;
348
+ };
@@ -8,6 +8,22 @@ import { sample } from "openapi-sampler";
8
8
  * values come from openapi-sampler, which is likewise browser-safe.
9
9
  */
10
10
 
11
+ /** Any value a parsed OpenAPI document can hold: JSON, nested schemas included. */
12
+ export type SpecValue =
13
+ | string
14
+ | number
15
+ | boolean
16
+ | null
17
+ | undefined
18
+ | SpecValue[]
19
+ | { [key: string]: SpecValue };
20
+
21
+ const isString = (value: SpecValue): value is string =>
22
+ typeof value === "string";
23
+
24
+ const isNumber = (value: SpecValue): value is number =>
25
+ typeof value === "number";
26
+
11
27
  /** A permissive view of an OpenAPI 3.1 schema — only the fields we render. */
12
28
  export interface SchemaLike {
13
29
  $ref?: string;
@@ -18,11 +34,11 @@ export interface SchemaLike {
18
34
  properties?: Record<string, SchemaLike>;
19
35
  required?: string[];
20
36
  items?: SchemaLike;
21
- enum?: unknown[];
22
- const?: unknown;
23
- default?: unknown;
24
- example?: unknown;
25
- examples?: unknown[];
37
+ enum?: SpecValue[];
38
+ const?: SpecValue;
39
+ default?: SpecValue;
40
+ example?: SpecValue;
41
+ examples?: SpecValue[];
26
42
  allOf?: SchemaLike[];
27
43
  oneOf?: SchemaLike[];
28
44
  anyOf?: SchemaLike[];
@@ -38,7 +54,7 @@ export interface SchemaLike {
38
54
  minItems?: number;
39
55
  maxItems?: number;
40
56
  pattern?: string;
41
- [key: string]: unknown;
57
+ [key: string]: SpecValue;
42
58
  }
43
59
 
44
60
  /** A permissive view of an operation parameter — only the fields we render. */
@@ -50,9 +66,16 @@ export interface ParameterLike {
50
66
  required?: boolean;
51
67
  deprecated?: boolean;
52
68
  schema?: SchemaLike;
53
- example?: unknown;
69
+ example?: SpecValue;
70
+ [key: string]: SpecValue;
54
71
  }
55
72
 
73
+ /** The `components` object of a parsed spec: section name → named-node table. */
74
+ export type ComponentsLike = Record<
75
+ string,
76
+ Record<string, SpecValue> | undefined
77
+ >;
78
+
56
79
  const REF_PATTERN = /#\/components\/schemas\/(?<name>[^/]+)$/u;
57
80
 
58
81
  const COMPONENT_REF = /#\/components\/(?<section>[^/]+)\/(?<name>[^/]+)$/u;
@@ -65,16 +88,18 @@ const COMPONENT_REF = /#\/components\/(?<section>[^/]+)\/(?<name>[^/]+)$/u;
65
88
  */
66
89
  export const resolveComponentRef = <T extends { $ref?: string }>(
67
90
  node: T,
68
- components: Record<string, unknown> | undefined,
91
+ components: ComponentsLike | undefined,
69
92
  section: string
70
93
  ): T => {
71
- if (typeof node.$ref !== "string") {
94
+ if (!isString(node.$ref)) {
72
95
  return node;
73
96
  }
74
97
  const groups = COMPONENT_REF.exec(node.$ref)?.groups;
75
98
  if (groups?.section !== section) {
76
99
  return node;
77
100
  }
101
+ // SAFETY: a components section table stores nodes of that section's type,
102
+ // and callers always pair `section` with the matching `T`.
78
103
  const table = components?.[section] as Record<string, T> | undefined;
79
104
  return table?.[groups.name ?? ""] ?? node;
80
105
  };
@@ -88,7 +113,7 @@ export const resolveComponentRef = <T extends { $ref?: string }>(
88
113
  export const mergeParameters = (
89
114
  pathParameters: ParameterLike[] | undefined,
90
115
  operationParameters: ParameterLike[] | undefined,
91
- components?: Record<string, unknown>
116
+ components?: ComponentsLike
92
117
  ): ParameterLike[] => {
93
118
  const merged = new Map<string, ParameterLike>();
94
119
  let position = 0;
@@ -118,7 +143,7 @@ export const resolveSchema = (
118
143
  if (!schema) {
119
144
  return {};
120
145
  }
121
- if (typeof schema.$ref === "string") {
146
+ if (isString(schema.$ref)) {
122
147
  const name = REF_PATTERN.exec(schema.$ref)?.groups?.name;
123
148
  if (name && schemas[name]) {
124
149
  return schemas[name];
@@ -140,7 +165,7 @@ const nonNullTypes = (type: string | string[] | undefined): string[] => {
140
165
  * array items can't recurse forever.
141
166
  */
142
167
  export const typeLabel = (schema: SchemaLike): string => {
143
- if (typeof schema.$ref === "string") {
168
+ if (isString(schema.$ref)) {
144
169
  return refName(schema.$ref);
145
170
  }
146
171
  if (schema.oneOf || schema.anyOf) {
@@ -177,11 +202,11 @@ export const constraints = (schema: SchemaLike): string[] => {
177
202
  ];
178
203
  for (const [key, label] of numeric) {
179
204
  const value = schema[key];
180
- if (typeof value === "number") {
205
+ if (isNumber(value)) {
181
206
  out.push(`${label} ${value}`);
182
207
  }
183
208
  }
184
- if (typeof schema.pattern === "string") {
209
+ if (isString(schema.pattern)) {
185
210
  out.push(`matches ${schema.pattern}`);
186
211
  }
187
212
  if (schema.default !== undefined) {
@@ -190,6 +215,12 @@ export const constraints = (schema: SchemaLike): string[] => {
190
215
  return out;
191
216
  };
192
217
 
218
+ /** The merged property list and required set a schema exposes. */
219
+ export interface ObjectPropertySet {
220
+ properties: [string, SchemaLike][];
221
+ required: Set<string>;
222
+ }
223
+
193
224
  /**
194
225
  * The object properties a schema exposes, merging `allOf` branches so an
195
226
  * `allOf`-composed model still lists every field. Returns the properties plus
@@ -198,7 +229,7 @@ export const constraints = (schema: SchemaLike): string[] => {
198
229
  export const objectProperties = (
199
230
  schema: SchemaLike,
200
231
  schemas: Record<string, SchemaLike>
201
- ): { properties: [string, SchemaLike][]; required: Set<string> } => {
232
+ ): ObjectPropertySet => {
202
233
  const properties = new Map<string, SchemaLike>();
203
234
  const required = new Set<string>();
204
235
  // Cycles can only enter through `$ref`s (inline JSON can't self-nest), so
@@ -206,7 +237,7 @@ export const objectProperties = (
206
237
  const seen = new Set<string>();
207
238
 
208
239
  const collect = (node: SchemaLike): void => {
209
- if (typeof node.$ref === "string") {
240
+ if (isString(node.$ref)) {
210
241
  if (seen.has(node.$ref)) {
211
242
  return;
212
243
  }
@@ -239,16 +270,18 @@ export const objectProperties = (
239
270
  export const exampleValue = (
240
271
  schema: SchemaLike | undefined,
241
272
  schemas: Record<string, SchemaLike>
242
- ): unknown => {
273
+ ): SpecValue => {
243
274
  if (!schema) {
244
275
  return null;
245
276
  }
246
277
  try {
278
+ // SAFETY: SchemaLike structurally covers the JSONSchema7 fields the
279
+ // sampler reads, and the sampler only ever assembles JSON values.
247
280
  return sample(
248
281
  schema as Parameters<typeof sample>[0],
249
282
  { quiet: true, skipReadOnly: true },
250
283
  { components: { schemas } }
251
- );
284
+ ) as SpecValue;
252
285
  } catch {
253
286
  // An unresolvable $ref or malformed schema is a spec problem the schema
254
287
  // tables already surface; a sample is best-effort.
@@ -257,5 +290,4 @@ export const exampleValue = (
257
290
  };
258
291
 
259
292
  /** Pretty-print a JSON value for an example/code block. */
260
- export const toJson = (value: unknown): string =>
261
- JSON.stringify(value, null, 2);
293
+ export const toJson = <T>(value: T): string => JSON.stringify(value, null, 2);
@@ -20,7 +20,6 @@ export interface SecuritySchemeLike {
20
20
  scheme?: string;
21
21
  /** `http` bearer: a hint at the token format, e.g. `JWT`. */
22
22
  bearerFormat?: string;
23
- [key: string]: unknown;
24
23
  }
25
24
 
26
25
  /** One security requirement: scheme name -> required scopes (empty outside OAuth). */
@@ -85,42 +84,115 @@ export const resolveSecurity = (
85
84
  return { alternatives, optional };
86
85
  };
87
86
 
87
+ /**
88
+ * One AsyncAPI 3.x security entry: a `$ref` into
89
+ * `components.securitySchemes`, or an inline scheme object (the shape the
90
+ * official 2.x converter emits for scoped requirements, `scopes` on the
91
+ * scheme itself).
92
+ */
93
+ export interface AsyncApiSecurityEntryLike {
94
+ $ref?: string;
95
+ type?: string;
96
+ scopes?: unknown;
97
+ }
98
+
99
+ const SECURITY_SCHEME_REF = /^#\/components\/securitySchemes\/(?<name>[^/]+)$/u;
100
+
101
+ /** Runtime string check for spec fields a hand-written document may corrupt. */
102
+ const isSpecString = (value: string | undefined): value is string =>
103
+ typeof value === "string";
104
+
105
+ /** Parsed YAML can put anything in a security list; keep only real objects. */
106
+ const isSecurityEntryObject = (
107
+ entry: AsyncApiSecurityEntryLike
108
+ ): entry is AsyncApiSecurityEntryLike =>
109
+ typeof entry === "object" && entry !== null;
110
+
111
+ /** Decode a JSON-pointer token: `kafka~1sasl` -> `kafka/sasl`. */
112
+ const unescapePointer = (token: string): string =>
113
+ token.replaceAll("~1", "/").replaceAll("~0", "~");
114
+
115
+ /**
116
+ * Resolve an AsyncAPI 3.x security list. Unlike OpenAPI's requirement maps,
117
+ * each entry names a single scheme and any one entry satisfies the operation
118
+ * — so every entry becomes its own one-scheme "or" alternative. A `$ref`
119
+ * pointing at an undeclared scheme is kept (with `scheme` undefined),
120
+ * matching {@link resolveSecurity}'s render-don't-drop rule.
121
+ */
122
+ export const resolveAsyncApiSecurity = (
123
+ entries: AsyncApiSecurityEntryLike[],
124
+ schemes: Record<string, SecuritySchemeLike> | undefined
125
+ ): OperationSecurity => {
126
+ const alternatives: ResolvedScheme[][] = [];
127
+ for (const entry of entries) {
128
+ if (!isSecurityEntryObject(entry)) {
129
+ continue;
130
+ }
131
+ const pointer = SECURITY_SCHEME_REF.exec(entry.$ref ?? "")?.groups?.name;
132
+ const name = pointer === undefined ? undefined : unescapePointer(pointer);
133
+ const inline = name === undefined && !isSpecString(entry.$ref);
134
+ const scheme = inline ? entry : schemes?.[name ?? ""];
135
+ alternatives.push([
136
+ {
137
+ key:
138
+ name ??
139
+ (isSpecString(entry.type) && entry.type !== ""
140
+ ? entry.type
141
+ : "security"),
142
+ scheme,
143
+ scopes:
144
+ inline && Array.isArray(entry.scopes)
145
+ ? entry.scopes.filter(
146
+ (scope): scope is string => typeof scope === "string"
147
+ )
148
+ : [],
149
+ },
150
+ ]);
151
+ }
152
+ return { alternatives, optional: false };
153
+ };
154
+
88
155
  const capitalize = (text: string): string =>
89
156
  text.charAt(0).toUpperCase() + text.slice(1);
90
157
 
158
+ /**
159
+ * Fixed labels for scheme types with no per-scheme variation. Covers both
160
+ * OpenAPI's types and the broker-auth types AsyncAPI adds; `http` is handled
161
+ * separately (its label depends on the scheme/bearerFormat fields).
162
+ */
163
+ const TYPE_LABELS = new Map<string, string>([
164
+ ["X509", "X.509 certificate"],
165
+ ["apiKey", "API key"],
166
+ ["asymmetricEncryption", "Asymmetric encryption"],
167
+ ["gssapi", "SASL/GSSAPI"],
168
+ ["httpApiKey", "API key"],
169
+ ["mutualTLS", "Mutual TLS"],
170
+ ["oauth2", "OAuth2 access token"],
171
+ ["openIdConnect", "OpenID Connect token"],
172
+ ["plain", "SASL/PLAIN"],
173
+ ["scramSha256", "SASL/SCRAM-SHA-256"],
174
+ ["scramSha512", "SASL/SCRAM-SHA-512"],
175
+ ["symmetricEncryption", "Symmetric encryption"],
176
+ ["userPassword", "Username & password"],
177
+ ]);
178
+
91
179
  /** A short human label for a scheme row, e.g. `Bearer token` or `API key`. */
92
180
  export const schemeLabel = (resolved: ResolvedScheme): string => {
93
181
  const { scheme } = resolved;
94
- switch (scheme?.type) {
95
- case "http": {
96
- const kind = (scheme.scheme ?? "").toLowerCase();
97
- if (kind === "bearer") {
98
- return scheme.bearerFormat
99
- ? `Bearer token (${scheme.bearerFormat})`
100
- : "Bearer token";
101
- }
102
- if (kind === "basic") {
103
- return "Basic auth";
104
- }
105
- return kind ? `HTTP ${kind}` : "HTTP auth";
106
- }
107
- case "apiKey": {
108
- return "API key";
182
+ if (scheme?.type === "http") {
183
+ const kind = (scheme.scheme ?? "").toLowerCase();
184
+ if (kind === "bearer") {
185
+ return scheme.bearerFormat
186
+ ? `Bearer token (${scheme.bearerFormat})`
187
+ : "Bearer token";
109
188
  }
110
- case "oauth2": {
111
- return "OAuth2 access token";
112
- }
113
- case "openIdConnect": {
114
- return "OpenID Connect token";
115
- }
116
- case "mutualTLS": {
117
- return "Mutual TLS";
118
- }
119
- default: {
120
- // Unknown scheme ref: the component name is the best label available.
121
- return resolved.key;
189
+ if (kind === "basic") {
190
+ return "Basic auth";
122
191
  }
192
+ return kind ? `HTTP ${kind}` : "HTTP auth";
123
193
  }
194
+ // Unknown scheme ref: the component name is the best label available.
195
+ return TYPE_LABELS.get(scheme?.type ?? "") ?? resolved.key;
124
196
  };
125
197
 
126
198
  /**
@@ -138,7 +210,8 @@ export const schemeCarrier = (
138
210
  case "openIdConnect": {
139
211
  return { in: "header", name: "Authorization" };
140
212
  }
141
- case "apiKey": {
213
+ case "apiKey":
214
+ case "httpApiKey": {
142
215
  return { in: scheme.in ?? "header", name: scheme.name ?? resolved.key };
143
216
  }
144
217
  default: {