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.
- package/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +1784 -633
- package/dist/cli/index.js.map +111 -106
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +80 -28
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +19 -3
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/configuration/theming.mdx +4 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +2 -1
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +64 -34
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +181 -40
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageLayout.astro +72 -3
- package/src/components/layout/ReferenceLayout.astro +2 -1
- package/src/components/layout/RootLayout.astro +20 -1
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +89 -28
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +19 -3
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +49 -0
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +26 -3
- package/src/core/schema.ts +214 -68
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +45 -18
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +33 -12
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/entry.ts +24 -2
- package/src/theme/fonts.ts +83 -7
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- 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?:
|
|
22
|
-
const?:
|
|
23
|
-
default?:
|
|
24
|
-
example?:
|
|
25
|
-
examples?:
|
|
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]:
|
|
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?:
|
|
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:
|
|
91
|
+
components: ComponentsLike | undefined,
|
|
69
92
|
section: string
|
|
70
93
|
): T => {
|
|
71
|
-
if (
|
|
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?:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
205
|
+
if (isNumber(value)) {
|
|
181
206
|
out.push(`${label} ${value}`);
|
|
182
207
|
}
|
|
183
208
|
}
|
|
184
|
-
if (
|
|
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
|
-
):
|
|
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 (
|
|
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
|
-
):
|
|
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:
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
111
|
-
return "
|
|
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: {
|