blume 1.4.3 → 1.5.0

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 (194) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +1621 -576
  3. package/dist/cli/index.js.map +109 -104
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/config-input.d.ts +79 -27
  6. package/dist/types/core/config.d.ts +2 -1
  7. package/dist/types/core/data.d.ts +16 -1
  8. package/dist/types/core/diagnostics.d.ts +5 -1
  9. package/dist/types/core/i18n-ui.d.ts +12 -0
  10. package/dist/types/core/schema.d.ts +112 -15
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +7 -3
  13. package/dist/types/core/types.d.ts +43 -2
  14. package/dist/types/core/ui-packs/index.d.ts +9 -1
  15. package/dist/types/openapi/references.d.ts +6 -5
  16. package/dist/types/seo/x-handle.d.ts +3 -2
  17. package/docs/advanced/api-reference.mdx +8 -6
  18. package/docs/configuration/search.mdx +2 -0
  19. package/docs/configuration/seo.mdx +1 -1
  20. package/docs/content/i18n.mdx +1 -1
  21. package/docs/content/meta.mdx +2 -1
  22. package/docs/content/meta.ts +1 -0
  23. package/docs/content/navigation.mdx +35 -1
  24. package/docs/content/versioning.mdx +106 -0
  25. package/docs/reference/cli.mdx +1 -0
  26. package/docs/reference/frontmatter.mdx +3 -0
  27. package/package.json +3 -1
  28. package/skills/blume-migrate/SKILL.md +2 -2
  29. package/skills/blume-migrate/references/docusaurus.md +1 -1
  30. package/skills/blume-migrate/references/fumadocs.md +1 -1
  31. package/skills/blume-migrate/references/mintlify.md +1 -1
  32. package/src/ai/agent-readability.ts +37 -10
  33. package/src/ai/ask-context.ts +5 -1
  34. package/src/ai/ask.ts +10 -1
  35. package/src/ai/component-markdown.ts +80 -43
  36. package/src/ai/llms.ts +40 -16
  37. package/src/ai/mcp/data.ts +48 -12
  38. package/src/ai/mcp/discovery.ts +28 -11
  39. package/src/ai/mcp/server.ts +183 -38
  40. package/src/ai/mcp/tools.ts +3 -3
  41. package/src/ai/skills.ts +32 -9
  42. package/src/ai/visibility.ts +2 -2
  43. package/src/astro/component-slots.ts +2 -0
  44. package/src/astro/examples.ts +6 -2
  45. package/src/astro/generate.ts +54 -29
  46. package/src/astro/integration.ts +13 -2
  47. package/src/astro/islands.ts +16 -9
  48. package/src/astro/templates.ts +152 -33
  49. package/src/audit/agent.ts +2 -2
  50. package/src/audit/checks/content.ts +26 -11
  51. package/src/audit/checks/dns-aid.ts +3 -0
  52. package/src/audit/checks/indexability.ts +24 -6
  53. package/src/audit/checks/llms.ts +9 -4
  54. package/src/audit/checks/network.ts +2 -0
  55. package/src/audit/checks/social.ts +18 -10
  56. package/src/audit/crawl.ts +37 -9
  57. package/src/audit/report.ts +20 -19
  58. package/src/audit/run.ts +5 -2
  59. package/src/audit/snapshot.ts +2 -4
  60. package/src/audit/types.ts +25 -3
  61. package/src/blume-modules.d.ts +5 -1
  62. package/src/cli/commands/audit.ts +9 -4
  63. package/src/cli/commands/build.ts +15 -9
  64. package/src/cli/commands/dev.ts +2 -0
  65. package/src/cli/commands/doctor.ts +2 -0
  66. package/src/cli/commands/eval.ts +7 -3
  67. package/src/cli/commands/init.ts +9 -9
  68. package/src/cli/commands/mcp-stdio.ts +3 -0
  69. package/src/cli/commands/translate.ts +14 -3
  70. package/src/cli/commands/version.ts +85 -0
  71. package/src/cli/dev-lock.ts +31 -10
  72. package/src/cli/eject-scripts.ts +17 -2
  73. package/src/cli/index.ts +2 -0
  74. package/src/cli/init/questions.ts +1 -1
  75. package/src/cli/init/scaffold.ts +22 -15
  76. package/src/cli/internal-error.ts +1 -0
  77. package/src/components/content/auto-type-table.ts +3 -0
  78. package/src/components/content/diff.ts +9 -5
  79. package/src/components/content/github-info.ts +2 -0
  80. package/src/components/islands/ask-ai.tsx +33 -25
  81. package/src/components/islands/hooks.ts +5 -1
  82. package/src/components/islands/webmcp.ts +49 -12
  83. package/src/components/layout/Header.astro +25 -1
  84. package/src/components/layout/NavSelector.astro +11 -2
  85. package/src/components/layout/NavTree.astro +4 -2
  86. package/src/components/layout/RootLayout.astro +18 -0
  87. package/src/components/layout/Search.astro +77 -13
  88. package/src/components/layout/VersionBanner.astro +39 -0
  89. package/src/components/layout/analytics-client.ts +8 -5
  90. package/src/components/layout/hydration-hint.ts +1 -1
  91. package/src/components/layout/nav-utils.ts +1 -4
  92. package/src/components/layout/overrides.ts +25 -12
  93. package/src/components/layout/search/algolia.ts +18 -5
  94. package/src/components/layout/search/endpoint.ts +3 -0
  95. package/src/components/layout/search/flexsearch.ts +23 -7
  96. package/src/components/layout/search/orama-cloud.ts +1 -1
  97. package/src/components/layout/search/orama.ts +4 -1
  98. package/src/components/layout/search/pagefind.ts +2 -0
  99. package/src/components/layout/search/types.ts +13 -1
  100. package/src/components/layout/search/typesense.ts +19 -3
  101. package/src/components/openapi/ApiOverview.astro +32 -6
  102. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  103. package/src/components/openapi/Bindings.astro +89 -0
  104. package/src/components/openapi/MethodBadge.astro +3 -0
  105. package/src/components/openapi/Operation.astro +7 -2
  106. package/src/components/openapi/PanelTabs.astro +131 -0
  107. package/src/components/openapi/ParametersTable.astro +2 -0
  108. package/src/components/openapi/RequestPanel.astro +12 -119
  109. package/src/components/openapi/async-snippets.ts +174 -0
  110. package/src/components/openapi/async.ts +348 -0
  111. package/src/components/openapi/helpers.ts +52 -20
  112. package/src/components/openapi/security.ts +102 -29
  113. package/src/components/openapi/snippets.ts +11 -11
  114. package/src/core/component-overrides.ts +28 -23
  115. package/src/core/config-input.ts +88 -27
  116. package/src/core/config.ts +20 -7
  117. package/src/core/content.ts +3 -1
  118. package/src/core/data.ts +16 -1
  119. package/src/core/define-components.ts +5 -0
  120. package/src/core/diagnostics.ts +46 -38
  121. package/src/core/frontmatter.ts +33 -7
  122. package/src/core/graph.ts +137 -53
  123. package/src/core/i18n-ui.ts +15 -0
  124. package/src/core/i18n.ts +16 -8
  125. package/src/core/load-module.ts +1 -0
  126. package/src/core/manifest.ts +92 -3
  127. package/src/core/meta.ts +44 -14
  128. package/src/core/nav-diagnostics.ts +3 -3
  129. package/src/core/navigation.ts +247 -67
  130. package/src/core/project-graph.ts +15 -3
  131. package/src/core/schema.ts +213 -67
  132. package/src/core/sources/assets.ts +2 -0
  133. package/src/core/sources/cache.ts +6 -0
  134. package/src/core/sources/github-releases.ts +39 -31
  135. package/src/core/sources/mdx-remote.ts +4 -0
  136. package/src/core/sources/normalize.ts +67 -20
  137. package/src/core/sources/notion.ts +49 -17
  138. package/src/core/sources/portable-text.ts +32 -11
  139. package/src/core/sources/sanity.ts +68 -14
  140. package/src/core/sources/types.ts +4 -0
  141. package/src/core/sources/watch.ts +1 -1
  142. package/src/core/standard-schema.ts +9 -3
  143. package/src/core/text-width.ts +26 -0
  144. package/src/core/tsconfig-aliases.ts +9 -5
  145. package/src/core/types.ts +45 -2
  146. package/src/core/ui-packs/index.ts +9 -1
  147. package/src/core/version-cut.ts +301 -0
  148. package/src/core/version.ts +2 -0
  149. package/src/core/versions.ts +170 -0
  150. package/src/deploy/adapter-output.ts +5 -2
  151. package/src/deploy/cloudflare-negotiation.ts +25 -10
  152. package/src/deploy/sitemap.ts +33 -1
  153. package/src/deploy/vercel-negotiation.ts +11 -4
  154. package/src/eval/report.ts +4 -4
  155. package/src/eval/run.ts +2 -2
  156. package/src/eval/schema.ts +1 -1
  157. package/src/markdown/base-links.ts +6 -6
  158. package/src/markdown/directives.ts +7 -1
  159. package/src/markdown/heading-anchors.ts +17 -6
  160. package/src/markdown/index.ts +73 -24
  161. package/src/markdown/inline-code.ts +14 -2
  162. package/src/markdown/language-icon.ts +6 -2
  163. package/src/markdown/mdast.ts +18 -4
  164. package/src/markdown/package-commands.ts +6 -8
  165. package/src/markdown/table-wrap.ts +4 -1
  166. package/src/markdown/twoslash.ts +2 -0
  167. package/src/og/card.ts +30 -11
  168. package/src/og/derive.ts +43 -27
  169. package/src/openapi/asyncapi.ts +366 -0
  170. package/src/openapi/model.ts +126 -57
  171. package/src/openapi/parse.ts +97 -5
  172. package/src/openapi/references.ts +12 -10
  173. package/src/openapi/render-mdx.ts +73 -34
  174. package/src/openapi/scalar.ts +6 -8
  175. package/src/openapi/source.ts +98 -28
  176. package/src/registry/eject.ts +7 -2
  177. package/src/search/documents.ts +25 -5
  178. package/src/search/facets.ts +7 -5
  179. package/src/search/orama-index.ts +66 -20
  180. package/src/search/popular.ts +10 -5
  181. package/src/search/providers.ts +2 -2
  182. package/src/search/sync/index.ts +2 -0
  183. package/src/search/sync/typesense.ts +4 -2
  184. package/src/seo/jsonld.ts +24 -6
  185. package/src/seo/x-handle.ts +8 -3
  186. package/src/theme/chrome-icons.ts +7 -2
  187. package/src/theme/fonts.ts +8 -4
  188. package/src/theme/icons.ts +4 -2
  189. package/src/theme/palette.ts +22 -14
  190. package/src/translate/meta.ts +15 -6
  191. package/src/translate/report.ts +9 -5
  192. package/src/translate/run.ts +10 -4
  193. package/src/translate/validate.ts +52 -17
  194. package/src/translate/work-list.ts +0 -0
@@ -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: {
@@ -87,8 +87,8 @@ const headerValues = (
87
87
  params: ParamLike[],
88
88
  schemas: Record<string, SchemaLike>,
89
89
  auth: SampleAuth | undefined
90
- ): Record<string, string> => {
91
- const headers: Record<string, string> = { ...auth?.headers };
90
+ ) => {
91
+ const headers = { ...auth?.headers };
92
92
  for (const param of params) {
93
93
  if (param.in === "header" && param.required && param.name) {
94
94
  headers[param.name] = String(
@@ -230,14 +230,14 @@ const LANGUAGES: SampleLanguage[] = [
230
230
  { build: pythonSnippet, id: "python", label: "Python", lang: "python" },
231
231
  ];
232
232
 
233
- const ALIASES: Record<string, string> = {
234
- bash: "curl",
235
- javascript: "js",
236
- node: "js",
237
- py: "python",
238
- shell: "curl",
239
- typescript: "js",
240
- };
233
+ const ALIASES = new Map([
234
+ ["bash", "curl"],
235
+ ["javascript", "js"],
236
+ ["node", "js"],
237
+ ["py", "python"],
238
+ ["shell", "curl"],
239
+ ["typescript", "js"],
240
+ ]);
241
241
 
242
242
  /** The sample languages to render, resolved from config ids (unknown ids dropped). */
243
243
  export const sampleLanguages = (ids: string[]): SampleLanguage[] => {
@@ -246,7 +246,7 @@ export const sampleLanguages = (ids: string[]): SampleLanguage[] => {
246
246
  const out: SampleLanguage[] = [];
247
247
  const seen = new Set<string>();
248
248
  for (const raw of wanted) {
249
- const id = ALIASES[raw.toLowerCase()] ?? raw.toLowerCase();
249
+ const id = ALIASES.get(raw.toLowerCase()) ?? raw.toLowerCase();
250
250
  const language = byId.get(id);
251
251
  if (language && !seen.has(id)) {
252
252
  seen.add(id);
@@ -65,18 +65,20 @@ const GROUPS = ["mdx", "layout", "islands"] as const;
65
65
  const GROUP_SET = new Set<string>(GROUPS);
66
66
  type Group = (typeof GROUPS)[number];
67
67
 
68
- const FRAMEWORK_BY_EXT: Record<string, OverrideFramework> = {
69
- jsx: "react",
70
- svelte: "svelte",
71
- tsx: "react",
72
- vue: "vue",
73
- };
68
+ const isGroup = (name: string): name is Group => GROUP_SET.has(name);
69
+
70
+ const FRAMEWORK_BY_EXT = new Map<string, OverrideFramework>([
71
+ ["jsx", "react"],
72
+ ["svelte", "svelte"],
73
+ ["tsx", "react"],
74
+ ["vue", "vue"],
75
+ ]);
74
76
 
75
- const FRAMEWORK_LABEL: Record<OverrideFramework, string> = {
77
+ const FRAMEWORK_LABEL = {
76
78
  react: "React",
77
79
  svelte: "Svelte",
78
80
  vue: "Vue",
79
- };
81
+ } satisfies Record<OverrideFramework, string>;
80
82
 
81
83
  /** Extensions probed (in order) when a specifier omits one. */
82
84
  const COMPONENT_EXTS = [
@@ -90,7 +92,7 @@ const COMPONENT_EXTS = [
90
92
  "svelte",
91
93
  ];
92
94
 
93
- const HYDRATION_MODES = new Set<HydrationMode>([
95
+ const HYDRATION_MODES: ReadonlySet<string> = new Set<HydrationMode>([
94
96
  "idle",
95
97
  "load",
96
98
  "media",
@@ -98,6 +100,9 @@ const HYDRATION_MODES = new Set<HydrationMode>([
98
100
  "visible",
99
101
  ]);
100
102
 
103
+ const isHydrationMode = (value: string): value is HydrationMode =>
104
+ HYDRATION_MODES.has(value);
105
+
101
106
  interface ImportBinding {
102
107
  /** Exported name: `"default"` or a named export. */
103
108
  imported: string;
@@ -226,7 +231,7 @@ const toImport = (
226
231
  }
227
232
  }
228
233
  return {
229
- framework: FRAMEWORK_BY_EXT[extension] ?? null,
234
+ framework: FRAMEWORK_BY_EXT.get(extension) ?? null,
230
235
  name: imported,
231
236
  path,
232
237
  };
@@ -270,9 +275,9 @@ const applyDescriptorProperty = (
270
275
  } else if (
271
276
  name === "client" &&
272
277
  ts.isStringLiteral(init) &&
273
- HYDRATION_MODES.has(init.text as HydrationMode)
278
+ isHydrationMode(init.text)
274
279
  ) {
275
- descriptor.client = init.text as HydrationMode;
280
+ descriptor.client = init.text;
276
281
  } else if (name === "media" && ts.isStringLiteral(init)) {
277
282
  descriptor.media = init.text;
278
283
  }
@@ -334,13 +339,14 @@ const finalize = (
334
339
  );
335
340
  }
336
341
 
337
- return {
338
- identifier,
339
- key,
340
- ...(client ? { client } : {}),
341
- ...(media ? { media } : {}),
342
- source,
343
- };
342
+ const normalized: NormalizedOverride = { identifier, key, source };
343
+ if (client) {
344
+ normalized.client = client;
345
+ }
346
+ if (media) {
347
+ normalized.media = media;
348
+ }
349
+ return normalized;
344
350
  };
345
351
 
346
352
  const normalizeEntry = (
@@ -446,22 +452,21 @@ const collectGroupOverrides = (
446
452
  }
447
453
  const name = propName(property.name);
448
454
  if (
449
- !(name && GROUP_SET.has(name)) ||
455
+ !(name && isGroup(name)) ||
450
456
  !ts.isObjectLiteralExpression(property.initializer)
451
457
  ) {
452
458
  return;
453
459
  }
454
- const group = name as Group;
455
460
  for (const entry of property.initializer.properties) {
456
461
  const normalized = normalizeEntry(
457
462
  entry,
458
- group,
463
+ name,
459
464
  imports,
460
465
  dir,
461
466
  result.warnings
462
467
  );
463
468
  if (normalized) {
464
- result[group].push(normalized);
469
+ result[name].push(normalized);
465
470
  }
466
471
  }
467
472
  };
@@ -761,6 +761,7 @@ export interface AiConfig {
761
761
  /** Web Bot Auth signature directory. Off until at least one key is listed. */
762
762
  export interface WebBotAuthConfig {
763
763
  /** Public JWKs to publish (e.g. an Ed25519 key: `kty: "OKP"`, `crv: "Ed25519"`, `x: …`). */
764
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- mirrors the schema's `z.record(z.unknown())` (the drift guard requires it); JWK parameters are validated at parse time, not typed.
764
765
  keys?: Record<string, unknown>[];
765
766
  }
766
767
 
@@ -838,6 +839,59 @@ export interface I18nConfig {
838
839
  ui?: Record<string, Record<string, Record<string, string>>>;
839
840
  }
840
841
 
842
+ // ---------------------------------------------------------------------------
843
+ // Versions
844
+ // ---------------------------------------------------------------------------
845
+
846
+ /** A frozen documentation snapshot: a directory under the content root. */
847
+ export interface ArchivedVersionInput {
848
+ /**
849
+ * The "you're viewing an old version" notice: `true` (default) for the
850
+ * built-in message, a string for custom copy, `false` to hide it.
851
+ */
852
+ banner?: boolean | string;
853
+ /**
854
+ * Where this version's pages point their canonical URL. `latest` (default)
855
+ * targets the same page in the current docs when it still exists (self
856
+ * otherwise); `self` keeps every page authoritative.
857
+ */
858
+ canonical?: "latest" | "self";
859
+ /**
860
+ * Directory name under the content root, and the URL segment. Must start
861
+ * with a letter (e.g. `v1.0`).
862
+ */
863
+ id: string;
864
+ /** Switcher label; defaults to the id. */
865
+ label?: string;
866
+ /** Emit `noindex` on every page of this version. Defaults to `false`. */
867
+ noindex?: boolean;
868
+ }
869
+
870
+ /**
871
+ * Docs versioning. Opt-in: the latest docs live at the content root with
872
+ * unprefixed URLs, and each archived version is a frozen snapshot directory
873
+ * (`content/docs/<id>/`) cut with `blume version <id>`. Archived means frozen:
874
+ * snapshots carry their own translations and are never retranslated.
875
+ */
876
+ export interface VersionsConfig {
877
+ /** Frozen snapshots, newest first — this order is the switcher order. */
878
+ archived?: ArchivedVersionInput[];
879
+ /** Labels the unprefixed tree (the latest docs) in the switcher. */
880
+ current: {
881
+ /** Small tag rendered next to the label (e.g. `Latest`). */
882
+ badge?: string;
883
+ label: string;
884
+ };
885
+ switcher?: {
886
+ /**
887
+ * Where switching lands when the page has no equivalent in the target
888
+ * version: `same-page` (default) goes to the equivalent when it exists
889
+ * (version root otherwise); `root` always goes to the version root.
890
+ */
891
+ redirect?: "same-page" | "root";
892
+ };
893
+ }
894
+
841
895
  // ---------------------------------------------------------------------------
842
896
  // Deployment & redirects
843
897
  // ---------------------------------------------------------------------------
@@ -1112,28 +1166,24 @@ export interface ReactConfig {
1112
1166
  // ---------------------------------------------------------------------------
1113
1167
 
1114
1168
  /**
1115
- * OpenAPI reference. By default (`renderer: "blume"`) Blume renders its own UI:
1116
- * one real page per operation, grouped by tag in the sidebar and included in
1117
- * search, llms.txt, and OG. Set `renderer: "scalar"` for the embedded Scalar
1118
- * SPA (a single self-contained route).
1169
+ * The shared shape of both API-reference blocks (`openapi`, `asyncapi`). Only
1170
+ * the per-block defaults differ; those are documented on the extending
1171
+ * interfaces.
1119
1172
  */
1120
- export interface OpenApiConfig {
1121
- /** Code-sample languages shown per operation (Blume renderer). */
1122
- codeSamples?: string[];
1173
+ interface ReferenceConfig {
1123
1174
  /** Turn the reference on. Defaults to `false`. */
1124
1175
  enabled?: boolean;
1125
1176
  /** Start nested schema rows expanded (Blume renderer). Defaults to `false`. */
1126
1177
  expandSchemas?: boolean;
1127
1178
  /** Who renders the reference. Defaults to `blume`. */
1128
1179
  renderer?: "blume" | "scalar";
1129
- /** Where the reference mounts. Defaults to `/reference`. */
1130
- route?: string;
1131
1180
  /**
1132
1181
  * Extra Scalar options forwarded verbatim to the embedded `<ScalarComponent>`
1133
1182
  * (Scalar renderer only) — e.g. `localization`, `agent`,
1134
1183
  * `hideTestRequestButton`, `orderSchemaPropertiesBy`. These win over Blume's
1135
1184
  * derived spec/theme config, so it's a full escape hatch to Scalar's API.
1136
1185
  */
1186
+ // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- mirrors the schema's `z.record(z.unknown())` (the drift guard requires it); the values are Scalar's own API surface, deliberately unmodeled.
1137
1187
  scalar?: Record<string, unknown>;
1138
1188
  /** One or more specs; each renders on its own route by default. */
1139
1189
  sources?: OpenApiSource[];
@@ -1144,27 +1194,36 @@ export interface OpenApiConfig {
1144
1194
  }
1145
1195
 
1146
1196
  /**
1147
- * AsyncAPI reference, rendered via the embedded Scalar SPA (which auto-detects
1148
- * the document type). Same shape as {@link OpenApiConfig}; only the default
1149
- * `route` differs.
1197
+ * OpenAPI reference. By default (`renderer: "blume"`) Blume renders its own UI:
1198
+ * one real page per operation, grouped by tag in the sidebar and included in
1199
+ * search, llms.txt, and OG. Set `renderer: "scalar"` for the embedded Scalar
1200
+ * SPA (a single self-contained route).
1150
1201
  */
1151
- export interface AsyncApiConfig {
1152
- /** Turn the reference on. Defaults to `false`. */
1153
- enabled?: boolean;
1154
- /** Where the reference mounts. Defaults to `/events`. */
1202
+ export interface OpenApiConfig extends ReferenceConfig {
1203
+ /**
1204
+ * Code-sample languages shown per operation (Blume renderer). Defaults to
1205
+ * `["curl", "js", "python"]`.
1206
+ */
1207
+ codeSamples?: string[];
1208
+ /** Where the reference mounts. Defaults to `/reference`. */
1155
1209
  route?: string;
1210
+ }
1211
+
1212
+ /**
1213
+ * AsyncAPI reference. Same shape as {@link OpenApiConfig}: by default
1214
+ * (`renderer: "blume"`) Blume normalizes the spec to AsyncAPI 3.x and renders
1215
+ * its own UI — one real page per operation, grouped by tag (or channel) in the
1216
+ * sidebar and included in search, llms.txt, and OG. Set `renderer: "scalar"`
1217
+ * for the embedded Scalar SPA (a single self-contained route).
1218
+ */
1219
+ export interface AsyncApiConfig extends ReferenceConfig {
1156
1220
  /**
1157
- * Extra Scalar options forwarded verbatim to the embedded `<ScalarComponent>`.
1158
- * These win over Blume's derived spec/theme config — a full escape hatch to
1159
- * Scalar's API.
1221
+ * Code-sample tools shown per operation (Blume renderer). Defaults to every
1222
+ * tool appropriate to the operation's protocol binding.
1160
1223
  */
1161
- scalar?: Record<string, unknown>;
1162
- /** One or more specs. */
1163
- sources?: OpenApiSource[];
1164
- /** Shorthand for a single source. */
1165
- spec?: string;
1166
- /** Scalar theme name. */
1167
- theme?: string;
1224
+ codeSamples?: string[];
1225
+ /** Where the reference mounts. Defaults to `/events`. */
1226
+ route?: string;
1168
1227
  }
1169
1228
 
1170
1229
  // ---------------------------------------------------------------------------
@@ -1302,7 +1361,7 @@ export interface BlumeConfig {
1302
1361
  ai?: AiConfig;
1303
1362
  /** Analytics providers (PostHog, Vercel, or arbitrary scripts). */
1304
1363
  analytics?: AnalyticsConfig;
1305
- /** AsyncAPI reference (embedded Scalar renderer). */
1364
+ /** AsyncAPI reference (native renderer by default, Scalar opt-out). */
1306
1365
  asyncapi?: AsyncApiConfig;
1307
1366
  /** Site-wide announcement banner shown above the header. */
1308
1367
  banner?: BannerConfig;
@@ -1375,6 +1434,8 @@ export interface BlumeConfig {
1375
1434
  title?: string;
1376
1435
  /** On-page table of contents. Defaults to on (H2–H3). */
1377
1436
  toc?: TocConfig;
1437
+ /** Docs versioning (opt-in frozen snapshots with a version switcher). */
1438
+ versions?: VersionsConfig;
1378
1439
  }
1379
1440
 
1380
1441
  // ---------------------------------------------------------------------------