blume 2.1.0 → 2.1.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 (397) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
  3. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
  4. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  5. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  6. package/dist/cli/chunk-150jk7km.js +52 -0
  7. package/dist/cli/chunk-150jk7km.js.map +10 -0
  8. package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
  9. package/dist/cli/chunk-313vnn40.js.map +10 -0
  10. package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
  11. package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
  12. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  13. package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
  14. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  15. package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
  16. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  17. package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
  18. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
  19. package/dist/cli/chunk-66w9j7p6.js +322 -0
  20. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  21. package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
  22. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  23. package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
  24. package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
  25. package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
  26. package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
  27. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  28. package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
  29. package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
  30. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
  31. package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
  32. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
  33. package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
  34. package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
  35. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  36. package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
  37. package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
  38. package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
  39. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
  40. package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
  41. package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
  42. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  43. package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
  44. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  45. package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
  46. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  47. package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
  48. package/dist/cli/chunk-j134vrp2.js +65 -0
  49. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  50. package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
  51. package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
  52. package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
  53. package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
  54. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  55. package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
  56. package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
  57. package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
  58. package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
  59. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
  60. package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
  61. package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
  62. package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
  63. package/dist/cli/index.js +24 -19
  64. package/dist/cli/index.js.map +3 -3
  65. package/dist/types/ai/ask-context.d.ts +6 -2
  66. package/dist/types/ai/component-markdown.d.ts +2 -0
  67. package/dist/types/ai/openapi-components.d.ts +4 -1
  68. package/dist/types/astro/integration.d.ts +5 -4
  69. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  70. package/dist/types/astro/pages.d.ts +10 -0
  71. package/dist/types/cli/init/scaffold.d.ts +1 -1
  72. package/dist/types/components/content/api-field.d.ts +4 -1
  73. package/dist/types/components/content/api-page.d.ts +6 -1
  74. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  75. package/dist/types/components/openapi/helpers.d.ts +22 -1
  76. package/dist/types/components/openapi/security.d.ts +10 -0
  77. package/dist/types/components/openapi/snippets.d.ts +60 -0
  78. package/dist/types/core/changelog-index.d.ts +8 -0
  79. package/dist/types/core/config-input.d.ts +30 -3
  80. package/dist/types/core/data.d.ts +4 -0
  81. package/dist/types/core/i18n-ui.d.ts +2 -0
  82. package/dist/types/core/links.d.ts +8 -0
  83. package/dist/types/core/probe.d.ts +2 -0
  84. package/dist/types/core/project-graph.d.ts +6 -0
  85. package/dist/types/core/redirect-patterns.d.ts +11 -3
  86. package/dist/types/core/schema.d.ts +119 -0
  87. package/dist/types/core/sources/cache.d.ts +3 -0
  88. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  89. package/dist/types/core/sources/contentful.d.ts +17 -1
  90. package/dist/types/core/sources/lexical.d.ts +5 -0
  91. package/dist/types/core/sources/normalize.d.ts +20 -1
  92. package/dist/types/core/sources/remote.d.ts +37 -10
  93. package/dist/types/core/sources/sanity.d.ts +1 -1
  94. package/dist/types/core/types.d.ts +17 -0
  95. package/dist/types/deploy/adapter-output.d.ts +2 -1
  96. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  97. package/dist/types/deploy/adapters/node.d.ts +58 -23
  98. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  99. package/dist/types/deploy/adapters/types.d.ts +3 -2
  100. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  101. package/dist/types/deploy/platforms/node.d.ts +3 -1
  102. package/dist/types/deploy/platforms/types.d.ts +18 -7
  103. package/dist/types/deploy/platforms/vercel.d.ts +20 -1
  104. package/dist/types/deploy/redirects.d.ts +17 -1
  105. package/dist/types/deploy/sitemap.d.ts +2 -1
  106. package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
  107. package/dist/types/openapi/checks.d.ts +27 -0
  108. package/dist/types/openapi/graphql.d.ts +6 -0
  109. package/dist/types/openapi/model.d.ts +15 -4
  110. package/dist/types/openapi/parse.d.ts +3 -0
  111. package/dist/types/openapi/references.d.ts +16 -0
  112. package/dist/types/ratelimit/index.d.ts +1 -1
  113. package/dist/types/ratelimit/schema.d.ts +8 -0
  114. package/dist/types/ratelimit/upstash.d.ts +39 -14
  115. package/dist/types/reference/graphql.d.ts +76 -1
  116. package/dist/types/reference/index.d.ts +1 -1
  117. package/dist/types/reference/openapi.d.ts +2 -2
  118. package/dist/types/reference/options.d.ts +37 -1
  119. package/dist/types/reference/schema.d.ts +84 -0
  120. package/dist/types/search/adapters/registry.d.ts +3 -0
  121. package/dist/types/search/adapters/typesense.d.ts +14 -4
  122. package/dist/types/search/build.d.ts +3 -1
  123. package/dist/types/search/orama-index.d.ts +9 -9
  124. package/dist/types/search/sync/algolia.d.ts +5 -2
  125. package/dist/types/search/sync/index.d.ts +3 -1
  126. package/dist/types/search/sync/typesense.d.ts +11 -7
  127. package/dist/types/sources/contentful.d.ts +16 -0
  128. package/dist/types/sources/github-releases.d.ts +8 -1
  129. package/dist/types/sources/payload.d.ts +2 -0
  130. package/dist/types/sources/registry.d.ts +54 -0
  131. package/dist/types/sources/shared.d.ts +3 -0
  132. package/dist/types/sources/strapi.d.ts +2 -0
  133. package/docs/01-quickstart.mdx +4 -2
  134. package/docs/02-deployment.mdx +31 -13
  135. package/docs/advanced/changelog.mdx +11 -0
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +30 -7
  138. package/docs/cli/doctor.mdx +5 -1
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/translate.mdx +1 -0
  141. package/docs/cli/validate.mdx +4 -3
  142. package/docs/configuration/analytics.mdx +1 -1
  143. package/docs/configuration/assistant.mdx +6 -2
  144. package/docs/configuration/customization.mdx +4 -2
  145. package/docs/configuration/export.mdx +5 -3
  146. package/docs/configuration/index.mdx +3 -2
  147. package/docs/configuration/rate-limiting.mdx +23 -2
  148. package/docs/configuration/search.mdx +37 -14
  149. package/docs/content/components.mdx +2 -2
  150. package/docs/content/frontmatter.mdx +5 -3
  151. package/docs/content/i18n.mdx +2 -2
  152. package/docs/content/includes.mdx +1 -1
  153. package/docs/content/navigation.mdx +4 -4
  154. package/docs/content/sources/contentful.mdx +7 -4
  155. package/docs/content/sources/custom.mdx +7 -3
  156. package/docs/content/sources/github-releases.mdx +10 -2
  157. package/docs/content/sources/index.mdx +3 -3
  158. package/docs/content/sources/notion.mdx +1 -1
  159. package/docs/content/sources/obsidian.mdx +1 -1
  160. package/docs/content/sources/payload.mdx +3 -2
  161. package/docs/content/sources/remote-mdx.mdx +4 -0
  162. package/docs/content/sources/sanity.mdx +2 -2
  163. package/docs/content/sources/strapi.mdx +3 -2
  164. package/docs/content/syntax.mdx +2 -2
  165. package/docs/discoverability/agent-discovery.mdx +1 -1
  166. package/docs/discoverability/index.mdx +1 -1
  167. package/docs/discoverability/json-api.mdx +2 -0
  168. package/docs/discoverability/llms-txt.mdx +1 -1
  169. package/docs/discoverability/markdown.mdx +3 -1
  170. package/docs/discoverability/mcp.mdx +4 -0
  171. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  172. package/docs/references/api-pages.mdx +1 -1
  173. package/docs/references/asyncapi.mdx +3 -1
  174. package/docs/references/graphql.mdx +13 -1
  175. package/docs/references/openapi.mdx +50 -8
  176. package/package.json +9 -9
  177. package/skills/blume-migrate/SKILL.md +1 -1
  178. package/skills/blume-migrate/references/docusaurus.md +4 -4
  179. package/skills/blume-migrate/references/fumadocs.md +17 -13
  180. package/skills/blume-migrate/references/nextra.md +8 -5
  181. package/skills/blume-migrate/references/starlight.md +9 -7
  182. package/src/ai/ask-context.ts +33 -3
  183. package/src/ai/ask-limits.ts +13 -0
  184. package/src/ai/changelog-markdown.ts +11 -6
  185. package/src/ai/component-markdown.ts +1 -1
  186. package/src/ai/cors.ts +12 -3
  187. package/src/ai/mcp/query.ts +14 -6
  188. package/src/ai/openapi-components.ts +28 -4
  189. package/src/ai/relative-links.ts +4 -38
  190. package/src/astro/generate.ts +78 -48
  191. package/src/astro/integration.ts +16 -13
  192. package/src/astro/markdown-negotiation.ts +3 -0
  193. package/src/astro/module-types.ts +3 -1
  194. package/src/astro/pages.ts +23 -0
  195. package/src/astro/templates.ts +125 -80
  196. package/src/audit/catalog.ts +19 -3
  197. package/src/audit/checks/links.ts +14 -1
  198. package/src/audit/checks/network.ts +110 -6
  199. package/src/audit/checks/redirects.ts +6 -4
  200. package/src/audit/checks/robots.ts +58 -2
  201. package/src/audit/run.ts +3 -0
  202. package/src/audit/types.ts +2 -0
  203. package/src/audit/url.ts +14 -5
  204. package/src/blume-modules.d.ts +5 -1
  205. package/src/cli/args.ts +51 -0
  206. package/src/cli/build-failure.ts +23 -0
  207. package/src/cli/commands/audit.ts +10 -1
  208. package/src/cli/commands/dev.ts +18 -2
  209. package/src/cli/commands/doctor.ts +45 -12
  210. package/src/cli/commands/eval.ts +36 -4
  211. package/src/cli/commands/init.ts +6 -4
  212. package/src/cli/commands/validate.ts +10 -1
  213. package/src/cli/init/scaffold.ts +3 -3
  214. package/src/cli/prepare.ts +6 -3
  215. package/src/cli/required-secrets.ts +1 -1
  216. package/src/components/content/ApiPlayground.astro +6 -2
  217. package/src/components/content/api-field.ts +4 -1
  218. package/src/components/content/api-page.ts +31 -7
  219. package/src/components/content/mermaid-element.ts +15 -1
  220. package/src/components/islands/hooks.ts +27 -1
  221. package/src/components/layout/PageActions.astro +139 -24
  222. package/src/components/layout/RootLayout.astro +10 -2
  223. package/src/components/layout/Search.astro +17 -3
  224. package/src/components/layout/TableOfContents.astro +24 -17
  225. package/src/components/layout/head-scripts.ts +6 -1
  226. package/src/components/layout/search/algolia.ts +10 -1
  227. package/src/components/layout/search/analytics.ts +42 -14
  228. package/src/components/layout/search/endpoint.ts +48 -5
  229. package/src/components/layout/search/pagefind.ts +114 -5
  230. package/src/components/layout/search/types.ts +12 -0
  231. package/src/components/layout/search/typesense.ts +18 -6
  232. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  233. package/src/components/openapi/Authorization.astro +10 -0
  234. package/src/components/openapi/GraphqlOperation.astro +5 -0
  235. package/src/components/openapi/Operation.astro +24 -1
  236. package/src/components/openapi/ParametersTable.astro +8 -4
  237. package/src/components/openapi/Playground.astro +7 -1
  238. package/src/components/openapi/RequestBody.astro +19 -2
  239. package/src/components/openapi/Responses.astro +1 -0
  240. package/src/components/openapi/SchemaTable.astro +15 -3
  241. package/src/components/openapi/async-snippets.ts +61 -5
  242. package/src/components/openapi/async.ts +123 -29
  243. package/src/components/openapi/graphql-helpers.ts +35 -12
  244. package/src/components/openapi/helpers.ts +36 -2
  245. package/src/components/openapi/message-model.ts +54 -11
  246. package/src/components/openapi/message.ts +14 -4
  247. package/src/components/openapi/operation-model.ts +16 -7
  248. package/src/components/openapi/playground-client.ts +24 -5
  249. package/src/components/openapi/playground-schema.ts +1 -1
  250. package/src/components/openapi/schema-tree.ts +44 -14
  251. package/src/components/openapi/security.ts +10 -0
  252. package/src/components/openapi/snippets.ts +5 -3
  253. package/src/components/openapi/validate-json.ts +4 -3
  254. package/src/core/changelog-index.ts +31 -1
  255. package/src/core/component-diagnostics.ts +53 -0
  256. package/src/core/component-overrides.ts +32 -0
  257. package/src/core/config-input.ts +31 -3
  258. package/src/core/data.ts +2 -0
  259. package/src/core/diagnostics.ts +5 -0
  260. package/src/core/i18n-ui.ts +2 -0
  261. package/src/core/includes.ts +1 -1
  262. package/src/core/links.ts +47 -17
  263. package/src/core/probe.ts +26 -6
  264. package/src/core/project-graph.ts +59 -18
  265. package/src/core/redirect-patterns.ts +36 -7
  266. package/src/core/schema.ts +21 -2
  267. package/src/core/sources/cache.ts +6 -0
  268. package/src/core/sources/contentful-rich-text.ts +29 -6
  269. package/src/core/sources/contentful.ts +135 -25
  270. package/src/core/sources/filesystem.ts +26 -4
  271. package/src/core/sources/github-releases.ts +12 -1
  272. package/src/core/sources/lexical.ts +15 -10
  273. package/src/core/sources/mdx-remote.ts +62 -10
  274. package/src/core/sources/normalize.ts +227 -41
  275. package/src/core/sources/notion.ts +7 -1
  276. package/src/core/sources/obsidian.ts +16 -3
  277. package/src/core/sources/payload.ts +38 -15
  278. package/src/core/sources/remote.ts +72 -15
  279. package/src/core/sources/resolve.ts +11 -1
  280. package/src/core/sources/sanity.ts +43 -25
  281. package/src/core/sources/strapi.ts +2 -2
  282. package/src/core/types.ts +18 -0
  283. package/src/core/ui-packs/ar.ts +1 -0
  284. package/src/core/ui-packs/bg.ts +1 -0
  285. package/src/core/ui-packs/bn.ts +1 -0
  286. package/src/core/ui-packs/ca.ts +1 -0
  287. package/src/core/ui-packs/cs.ts +1 -0
  288. package/src/core/ui-packs/da.ts +1 -0
  289. package/src/core/ui-packs/de.ts +1 -0
  290. package/src/core/ui-packs/el.ts +1 -0
  291. package/src/core/ui-packs/es.ts +1 -0
  292. package/src/core/ui-packs/fa.ts +1 -0
  293. package/src/core/ui-packs/fi.ts +1 -0
  294. package/src/core/ui-packs/fr.ts +1 -0
  295. package/src/core/ui-packs/he.ts +1 -0
  296. package/src/core/ui-packs/hi.ts +1 -0
  297. package/src/core/ui-packs/hr.ts +1 -0
  298. package/src/core/ui-packs/hu.ts +1 -0
  299. package/src/core/ui-packs/id.ts +1 -0
  300. package/src/core/ui-packs/it.ts +1 -0
  301. package/src/core/ui-packs/ja.ts +1 -0
  302. package/src/core/ui-packs/ko.ts +1 -0
  303. package/src/core/ui-packs/nl.ts +1 -0
  304. package/src/core/ui-packs/no.ts +1 -0
  305. package/src/core/ui-packs/pl.ts +1 -0
  306. package/src/core/ui-packs/pt-br.ts +1 -0
  307. package/src/core/ui-packs/pt.ts +1 -0
  308. package/src/core/ui-packs/ro.ts +1 -0
  309. package/src/core/ui-packs/ru.ts +1 -0
  310. package/src/core/ui-packs/sk.ts +1 -0
  311. package/src/core/ui-packs/sr.ts +1 -0
  312. package/src/core/ui-packs/sv.ts +1 -0
  313. package/src/core/ui-packs/th.ts +1 -0
  314. package/src/core/ui-packs/tr.ts +1 -0
  315. package/src/core/ui-packs/uk.ts +1 -0
  316. package/src/core/ui-packs/vi.ts +1 -0
  317. package/src/core/ui-packs/zh-tw.ts +1 -0
  318. package/src/core/ui-packs/zh.ts +1 -0
  319. package/src/deploy/adapter-output.ts +7 -2
  320. package/src/deploy/adapters/cloudflare.ts +7 -5
  321. package/src/deploy/adapters/node.ts +47 -9
  322. package/src/deploy/adapters/types.ts +7 -3
  323. package/src/deploy/artifacts.ts +2 -1
  324. package/src/deploy/platforms/cloudflare.ts +13 -9
  325. package/src/deploy/platforms/netlify.ts +2 -1
  326. package/src/deploy/platforms/node.ts +12 -2
  327. package/src/deploy/platforms/types.ts +21 -7
  328. package/src/deploy/platforms/vercel.ts +113 -16
  329. package/src/deploy/redirects.ts +66 -6
  330. package/src/deploy/rss.ts +3 -1
  331. package/src/deploy/sitemap.ts +39 -5
  332. package/src/deploy/vercel-negotiation.ts +290 -102
  333. package/src/eval/agents.ts +18 -1
  334. package/src/eval/prompts.ts +6 -3
  335. package/src/eval/report.ts +34 -13
  336. package/src/eval/run.ts +5 -0
  337. package/src/markdown/relative-links.ts +3 -2
  338. package/src/openapi/checks.ts +273 -0
  339. package/src/openapi/graphql-build.ts +21 -0
  340. package/src/openapi/graphql.ts +11 -0
  341. package/src/openapi/model.ts +15 -3
  342. package/src/openapi/parse.ts +127 -2
  343. package/src/openapi/references.ts +43 -3
  344. package/src/openapi/source.ts +39 -14
  345. package/src/ratelimit/index.ts +1 -1
  346. package/src/ratelimit/runtime.ts +37 -19
  347. package/src/ratelimit/upstash.ts +40 -10
  348. package/src/reference/graphql.ts +15 -1
  349. package/src/reference/index.ts +1 -0
  350. package/src/reference/openapi.ts +2 -2
  351. package/src/reference/options.ts +29 -1
  352. package/src/registry/eject.ts +3 -1
  353. package/src/search/adapters/locale-scope.ts +30 -0
  354. package/src/search/adapters/typesense.ts +14 -4
  355. package/src/search/build.ts +21 -3
  356. package/src/search/orama-index.ts +163 -31
  357. package/src/search/source-pages.ts +29 -0
  358. package/src/search/sync/algolia.ts +99 -9
  359. package/src/search/sync/index.ts +52 -13
  360. package/src/search/sync/typesense.ts +72 -27
  361. package/src/sources/contentful.ts +12 -0
  362. package/src/sources/github-releases.ts +7 -1
  363. package/src/sources/shared.ts +3 -0
  364. package/src/theme/entry.ts +29 -3
  365. package/src/translate/validate.ts +23 -1
  366. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  367. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  368. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  369. package/dist/cli/chunk-4ejxbzee.js +0 -39
  370. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  371. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  372. package/dist/cli/chunk-713mngdq.js.map +0 -14
  373. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  374. package/dist/cli/chunk-amrb6hgh.js +0 -145
  375. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  376. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  377. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  378. package/dist/cli/chunk-ta27feev.js.map +0 -10
  379. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  380. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  381. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
  382. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
  383. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
  384. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
  385. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
  386. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
  387. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
  388. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
  389. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
  390. /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
  391. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
  392. /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
  393. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
  394. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
  395. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
  396. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
  397. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
@@ -4,7 +4,7 @@ import { setTimeout as sleep } from "node:timers/promises";
4
4
  import type * as ConverterModule from "@asyncapi/converter";
5
5
  import { normalize, upgrade } from "@scalar/openapi-parser";
6
6
  import pRetry, { AbortError } from "p-retry";
7
- import { isAbsolute, join } from "pathe";
7
+ import { dirname, isAbsolute, join, resolve } from "pathe";
8
8
  import type * as UndiciModule from "undici";
9
9
 
10
10
  import {
@@ -15,8 +15,11 @@ import { nodeRequire } from "../core/node-require.ts";
15
15
  import { hashText } from "../core/sources/cache.ts";
16
16
  import type { AsyncApiDocument } from "./asyncapi.ts";
17
17
  import { normalizeAsyncApiDocument } from "./asyncapi.ts";
18
+ import { operationLabel } from "./checks.ts";
19
+ import type { SpecIssue } from "./checks.ts";
18
20
  import { buildGraphqlDocument } from "./graphql-build.ts";
19
21
  import type { GraphqlDocument } from "./graphql.ts";
22
+ import { HTTP_METHODS } from "./model.ts";
20
23
  import type { ApiDocument } from "./model.ts";
21
24
  import { applyOverlay, isJsonObject, overlayDocument } from "./overlay.ts";
22
25
  import { SpecDependencyError } from "./spec-dependency-error.ts";
@@ -25,6 +28,8 @@ import { SpecDependencyError } from "./spec-dependency-error.ts";
25
28
  * Spec loading and normalization. Blume reuses Scalar's parser
26
29
  * (`@scalar/openapi-parser`) to read a spec (YAML or JSON), then upgrade Swagger
27
30
  * 2.0 / OpenAPI 3.0 documents to 3.1 so the renderer only handles one shape.
31
+ * OpenAPI 3.2 is backward compatible with 3.1, so a 3.2 document is read as
32
+ * written.
28
33
  * Internal `$ref`s are deliberately left in place (see `model.ts`).
29
34
  *
30
35
  * Remote (`http(s)`) specs are fetched defensively — bounded per attempt, retried
@@ -58,6 +63,8 @@ const PROXY_ENV_VARS = [
58
63
 
59
64
  export interface ParsedSpec {
60
65
  document: ApiDocument;
66
+ /** `x-codeSamples` entries whose `$ref` couldn't be inlined. */
67
+ issues: SpecIssue[];
61
68
  warnings: string[];
62
69
  }
63
70
 
@@ -360,6 +367,123 @@ export const readOverlaidSpec = async (
360
367
  return { text: JSON.stringify(document), warnings };
361
368
  };
362
369
 
370
+ /** A `$ref` in place of an `x-codeSamples` entry's `source`. */
371
+ interface SampleRef {
372
+ $ref: string;
373
+ }
374
+
375
+ /** An `x-codeSamples` entry whose `source` is a `$ref` rather than the code. */
376
+ const isRefSample = (value: unknown): value is { source: SampleRef } =>
377
+ isJsonObject(value) &&
378
+ isJsonObject(value.source) &&
379
+ typeof value.source.$ref === "string";
380
+
381
+ /**
382
+ * Where a code sample's `$ref` points, in the form `readSpecText` reads: a
383
+ * URL, or a path resolved against the spec's own location. `undefined` for
384
+ * a JSON pointer (`#/…`, or a file with a fragment), which names a spot
385
+ * inside a document rather than a file of code.
386
+ */
387
+ const sampleTarget = (
388
+ ref: string,
389
+ spec: string,
390
+ root: string
391
+ ): string | undefined => {
392
+ if (ref.includes("#")) {
393
+ return undefined;
394
+ }
395
+ if (URL_SPEC.test(ref)) {
396
+ return ref;
397
+ }
398
+ if (URL_SPEC.test(spec)) {
399
+ return new URL(ref, spec).href;
400
+ }
401
+ return resolve(dirname(isAbsolute(spec) ? spec : join(root, spec)), ref);
402
+ };
403
+
404
+ /**
405
+ * Inline each `x-codeSamples` entry whose `source` is a `$ref` to a file of
406
+ * its own, as Redocly's docs describe (`source: { $ref: ../code_samples/… }`):
407
+ * `redocly bundle` inlines those, and a spec read as written doesn't. Each
408
+ * file is read relative to the spec,
409
+ * the way the spec itself was (local, or fetched and cached), so the sample
410
+ * renders like a written-out one. An entry that can't be read is left out of
411
+ * the page, with a warning, rather than dropped without a sign.
412
+ */
413
+ const inlineSampleRefs = async (
414
+ document: ApiDocument,
415
+ spec: string,
416
+ root: string,
417
+ options: SpecFetchOptions
418
+ ): Promise<SpecIssue[]> => {
419
+ const reads: Promise<SpecIssue | undefined>[] = [];
420
+ const inline = async (
421
+ entry: { source: SampleRef | string },
422
+ ref: string,
423
+ label: string
424
+ ): Promise<SpecIssue | undefined> => {
425
+ const target = sampleTarget(ref, spec, root);
426
+ if (target === undefined) {
427
+ return {
428
+ code: "BLUME_OPENAPI_CODE_SAMPLE_REF",
429
+ message: `${label} has an \`x-codeSamples\` entry whose \`source\` is a $ref to "${ref}", a spot inside a document, which Blume doesn't resolve, so the sample is left out.`,
430
+ suggestion:
431
+ "Point the `$ref` at a file that holds only the sample's code, or write the code out in `source`.",
432
+ };
433
+ }
434
+ try {
435
+ const { text } = await readSpecText(target, root, options);
436
+ entry.source = text;
437
+ return undefined;
438
+ } catch (error) {
439
+ // SAFETY: a failed read throws a Node error, and a failed fetch an
440
+ // Error (`attemptFetch` wraps anything else); only the message is shown.
441
+ const { message } = error as Error;
442
+ return {
443
+ code: "BLUME_OPENAPI_CODE_SAMPLE_REF",
444
+ message: `${label} has an \`x-codeSamples\` entry whose \`source\` is a $ref to "${ref}", which couldn't be read (${message}), so the sample is left out.`,
445
+ suggestion:
446
+ "Check the path, which is relative to the spec, or write the code out in `source`.",
447
+ };
448
+ }
449
+ };
450
+ const visit = (
451
+ name: string,
452
+ item: NonNullable<ApiDocument["paths"]>[string] | undefined,
453
+ webhook: boolean
454
+ ): void => {
455
+ if (!isJsonObject(item) || "$ref" in item) {
456
+ return;
457
+ }
458
+ for (const method of HTTP_METHODS) {
459
+ const operation = item[method];
460
+ // The spelling `customCodeSamples` renders: the current one wins.
461
+ const samples = isJsonObject(operation)
462
+ ? (operation["x-codeSamples"] ?? operation["x-code-samples"])
463
+ : undefined;
464
+ for (const entry of Array.isArray(samples) ? samples : []) {
465
+ if (isRefSample(entry)) {
466
+ reads.push(
467
+ inline(
468
+ entry,
469
+ entry.source.$ref,
470
+ operationLabel(name, method, webhook)
471
+ )
472
+ );
473
+ }
474
+ }
475
+ }
476
+ };
477
+ for (const [path, item] of Object.entries(document.paths ?? {})) {
478
+ visit(path, item, false);
479
+ }
480
+ for (const [name, item] of Object.entries(document.webhooks ?? {})) {
481
+ visit(name, item, true);
482
+ }
483
+ const issues = await Promise.all(reads);
484
+ return issues.filter((issue) => issue !== undefined);
485
+ };
486
+
363
487
  export const parseSpec = async (
364
488
  spec: string,
365
489
  root: string,
@@ -377,7 +501,8 @@ export const parseSpec = async (
377
501
  `${spec} is not a valid OpenAPI document (expected a YAML or JSON object).`
378
502
  );
379
503
  }
380
- return { document: specification, warnings };
504
+ const issues = await inlineSampleRefs(specification, spec, root, options);
505
+ return { document: specification, issues, warnings };
381
506
  };
382
507
 
383
508
  export interface ParsedAsyncApiSpec {
@@ -1,6 +1,9 @@
1
+ import { isAbsolute, join } from "pathe";
2
+
1
3
  import { normalizeRoute, withBasePath } from "../core/base-path.ts";
2
4
  import type { ResolvedConfig } from "../core/schema.ts";
3
5
  import { trimChar } from "../core/trim.ts";
6
+ import type { GraphqlAuthOptions } from "../reference/options.ts";
4
7
  import type { ScalarOptions } from "../reference/scalar.ts";
5
8
  import type { ResolvedReferenceAdapter } from "../reference/schema.ts";
6
9
 
@@ -54,6 +57,12 @@ export interface ReferenceSource {
54
57
  */
55
58
  basePath: string;
56
59
  label: string;
60
+ /**
61
+ * The source's own `label`, when it sets one: it names the sidebar group
62
+ * of the source's route. An unlabeled source's group keeps the name its
63
+ * route gives it.
64
+ */
65
+ groupLabel?: string;
57
66
  /** Whether generated pages are included in llms.txt/llms-full.txt. */
58
67
  includeInLlms: boolean;
59
68
  /** Whether generated pages are included in site search. */
@@ -77,6 +86,8 @@ export interface ReferenceSource {
77
86
  * (GraphQL only — a schema, unlike an OpenAPI document, names no server).
78
87
  */
79
88
  endpoint?: string;
89
+ /** How the GraphQL endpoint authenticates (GraphQL only). */
90
+ auth?: GraphqlAuthOptions;
80
91
  /**
81
92
  * The `scalar()` adapter's own options (`scalar` kind only): `theme` plus
82
93
  * any Scalar config forwarded verbatim to `<ScalarComponent>`, which takes
@@ -181,6 +192,7 @@ const scalarOptionsOf = (
181
192
 
182
193
  /** A source row with every kind's fields reconciled onto one shape. */
183
194
  interface SourceRow {
195
+ auth?: GraphqlAuthOptions;
184
196
  endpoint?: string;
185
197
  includeInLlms: boolean;
186
198
  includeInSearch: boolean;
@@ -194,9 +206,9 @@ interface SourceRow {
194
206
 
195
207
  /**
196
208
  * One row per source, with the kind-specific fields already reconciled: a
197
- * GraphQL source's `endpoint` falls back to the adapter-wide default (the
198
- * common single-schema case pairs it with the `spec` shorthand); the other
199
- * kinds have no endpoint at all. A Scalar source carries only `noindex` of
209
+ * GraphQL source's `endpoint` and `auth` fall back to the adapter-wide
210
+ * defaults (the common single-schema case pairs them with the `spec`
211
+ * shorthand); the other kinds have neither. A Scalar source carries only `noindex` of
200
212
  * the per-source controls — the embed sits outside search and llms.txt, so
201
213
  * the other two read as off.
202
214
  */
@@ -204,6 +216,7 @@ const sourceRowsOf = (adapter: ResolvedReferenceAdapter): SourceRow[] => {
204
216
  if (adapter.kind === "graphql") {
205
217
  return adapter.options.sources.map((source) => ({
206
218
  ...source,
219
+ auth: source.auth ?? adapter.options.auth,
207
220
  endpoint: source.endpoint ?? adapter.options.endpoint,
208
221
  }));
209
222
  }
@@ -258,9 +271,15 @@ const referencesFor = (
258
271
  if (adapter.kind === "scalar") {
259
272
  reference.scalar = scalarOptionsOf(adapter.options);
260
273
  }
274
+ if (source.label !== undefined) {
275
+ reference.groupLabel = source.label;
276
+ }
261
277
  if (source.endpoint !== undefined) {
262
278
  reference.endpoint = source.endpoint;
263
279
  }
280
+ if (source.auth !== undefined) {
281
+ reference.auth = source.auth;
282
+ }
264
283
  if (source.overlays !== undefined) {
265
284
  reference.overlays = source.overlays;
266
285
  }
@@ -348,6 +367,27 @@ export const blumeReferences = (
348
367
  return result;
349
368
  };
350
369
 
370
+ /** A spec or overlay the parser fetches rather than reads from disk. */
371
+ const REMOTE_SPEC = /^https?:\/\//u;
372
+
373
+ /**
374
+ * The local files every reference reads, as absolute paths: each spec and
375
+ * overlay that isn't an `http(s)` URL, resolved from the project root the way
376
+ * the parser resolves them. `blume dev` watches these, so an edit to a spec
377
+ * regenerates its reference like an edit to a page does.
378
+ */
379
+ export const referenceSpecFiles = (
380
+ config: ResolvedConfig,
381
+ root: string
382
+ ): string[] => [
383
+ ...new Set(
384
+ resolveReferences(config)
385
+ .flatMap((ref) => [ref.spec, ...(ref.overlays ?? [])])
386
+ .filter((path) => !REMOTE_SPEC.test(path))
387
+ .map((path) => (isAbsolute(path) ? path : join(root, path)))
388
+ ),
389
+ ];
390
+
351
391
  /** Whether any reference is Scalar-rendered (gates the Scalar pages). */
352
392
  export const hasScalarReferences = (config: ResolvedConfig): boolean =>
353
393
  resolveReferences(config).some((ref) => ref.kind === "scalar");
@@ -11,6 +11,8 @@ import type {
11
11
  import type { Diagnostic } from "../core/types.ts";
12
12
  import { extractAsyncApiOperations } from "./asyncapi.ts";
13
13
  import type { AsyncApiDocument } from "./asyncapi.ts";
14
+ import { specIssues } from "./checks.ts";
15
+ import type { SpecIssue } from "./checks.ts";
14
16
  import { extractGraphqlOperations } from "./graphql.ts";
15
17
  import type { GraphqlDocument } from "./graphql.ts";
16
18
  import { extractOperations } from "./model.ts";
@@ -173,24 +175,34 @@ const specEntries = (
173
175
  };
174
176
 
175
177
  /**
176
- * Label each tag's sidebar group with the spec's own tag name. The group label
177
- * is otherwise re-humanized from the tag's route slug (split on hyphens,
178
+ * Label each tag's sidebar group with the spec's own tag name, and order the
179
+ * groups as the overview does: the spec's declared tag order, then first use
180
+ * (see `operationCollector`), not alphabetically. The group label is
181
+ * otherwise re-humanized from the tag's route slug (split on hyphens,
178
182
  * title-cased), which mangles authored casing and symbols — `OAuth2` →
179
- * "Oauth2", `Größe` → "Größe" only by luck of the slug. Keys are the tag
180
- * directories under the reference route, the same group paths `meta.ts` files
181
- * use, so user-authored meta still overrides these.
183
+ * "Oauth2", `Größe` → "Größe" only by luck of the slug. A source that names
184
+ * its own `label` gets it on its route's group the same way (`GitHub (v2)`,
185
+ * not "Github V2"). Keys are the directories under the reference route, the
186
+ * same group paths `meta.ts` files use, so user-authored meta still
187
+ * overrides these.
182
188
  */
183
189
  const tagFolderMeta = (
184
190
  spec: ApiSpecData,
185
- tags: { slug: string; name: string }[]
191
+ tags: { slug: string; name: string }[],
192
+ groupLabel: string | undefined
186
193
  ): Record<string, FolderMeta> => {
187
194
  const base = routeToRef(spec.route);
188
- return Object.fromEntries(
189
- tags.map((tag) => [
195
+ const meta: Record<string, FolderMeta> = Object.fromEntries(
196
+ tags.map((tag, order) => [
190
197
  base ? `${base}/${tag.slug}` : tag.slug,
191
- { title: tag.name },
198
+ { order, title: tag.name },
192
199
  ])
193
200
  );
201
+ // A root-mounted reference has no group of its own to name.
202
+ if (groupLabel !== undefined && base) {
203
+ meta[base] = { title: groupLabel };
204
+ }
205
+ return meta;
194
206
  };
195
207
 
196
208
  interface LoadedSpec {
@@ -210,6 +222,8 @@ interface ParsedReference {
210
222
  operations: ApiOperationRef[];
211
223
  tags: ApiTagRef[];
212
224
  extractWarnings: string[];
225
+ /** Spec mistakes that would render silently wrong (OpenAPI only). */
226
+ issues?: SpecIssue[];
213
227
  }
214
228
 
215
229
  const parseReference = async (
@@ -247,7 +261,7 @@ const parseReference = async (
247
261
  warnings,
248
262
  };
249
263
  }
250
- const { document, warnings } = await parseSpec(
264
+ const { document, issues, warnings } = await parseSpec(
251
265
  reference.spec,
252
266
  ctx.projectRoot,
253
267
  { ...options, overlays: reference.overlays }
@@ -256,6 +270,7 @@ const parseReference = async (
256
270
  return {
257
271
  document,
258
272
  extractWarnings: extracted.warnings,
273
+ issues: [...issues, ...specIssues(document)],
259
274
  operations: extracted.operations,
260
275
  tags: extracted.tags,
261
276
  warnings,
@@ -276,7 +291,7 @@ export const openApiSource = (
276
291
  const kindLabel = KIND_LABELS[reference.kind];
277
292
  const codePrefix = CODE_PREFIXES[reference.kind];
278
293
  try {
279
- const { document, warnings, operations, tags, extractWarnings } =
294
+ const { document, warnings, operations, tags, extractWarnings, issues } =
280
295
  await parseReference(reference, ctx);
281
296
  const info = document.info ?? { title: reference.label, version: "" };
282
297
  // The playground proxy resolves here, not client-side: `true` selects
@@ -318,11 +333,15 @@ export const openApiSource = (
318
333
  title: info.title ?? reference.label,
319
334
  version: info.version ?? "",
320
335
  };
321
- // Only GraphQL references carry a live endpoint (a schema names no
322
- // server); assigned separately so the key stays absent otherwise.
336
+ // Only GraphQL references carry a live endpoint and its auth (a schema
337
+ // names no server); assigned separately so the keys stay absent
338
+ // otherwise.
323
339
  if (reference.endpoint !== undefined) {
324
340
  spec.endpoint = reference.endpoint;
325
341
  }
342
+ if (reference.auth !== undefined) {
343
+ spec.auth = reference.auth;
344
+ }
326
345
  return {
327
346
  diagnostics: [
328
347
  ...warnings.map((message) => ({
@@ -335,6 +354,12 @@ export const openApiSource = (
335
354
  message: `In ${kindLabel} spec "${reference.spec}": ${message}`,
336
355
  severity: "warning" as const,
337
356
  })),
357
+ ...(issues ?? []).map((issue) => ({
358
+ code: issue.code,
359
+ message: `In ${kindLabel} spec "${reference.spec}": ${issue.message}`,
360
+ severity: "warning" as const,
361
+ suggestion: issue.suggestion,
362
+ })),
338
363
  // A document with no operations (say, a config file that happens to
339
364
  // parse as YAML) would otherwise build a nav tab onto an empty
340
365
  // reference with no hint why.
@@ -350,7 +375,7 @@ export const openApiSource = (
350
375
  : []),
351
376
  ],
352
377
  entries: specEntries(spec, operations, reference),
353
- folderMeta: tagFolderMeta(spec, tags),
378
+ folderMeta: tagFolderMeta(spec, tags, reference.groupLabel),
354
379
  slug: reference.slug,
355
380
  spec,
356
381
  };
@@ -27,4 +27,4 @@ export { memory } from "./memory.ts";
27
27
  export type { MemoryAdapter, RateLimitOptions } from "./memory.ts";
28
28
  export type { RateLimitAdapter } from "./schema.ts";
29
29
  export { upstash } from "./upstash.ts";
30
- export type { UpstashAdapter } from "./upstash.ts";
30
+ export type { UpstashAdapter, UpstashOptions } from "./upstash.ts";
@@ -16,7 +16,7 @@ import type { ClientContext } from "../core/client-address.ts";
16
16
  import { RATE_LIMIT_BINDING } from "./cloudflare.ts";
17
17
  import { DEFAULT_REQUESTS, DEFAULT_WINDOW } from "./memory.ts";
18
18
  import type { RateLimitAdapter } from "./schema.ts";
19
- import { UPSTASH_SECRETS } from "./upstash.ts";
19
+ import { upstashSecrets } from "./upstash.ts";
20
20
 
21
21
  /** What a limiter says about one request. */
22
22
  export interface RateLimitResult {
@@ -86,16 +86,32 @@ export const memoryLimiter = (
86
86
  };
87
87
  };
88
88
 
89
- /** One command's reply from Upstash's REST pipeline. */
89
+ /**
90
+ * Counts the request and reads what's left of the window, run by Redis as
91
+ * one step. A key without an expiry — the one `INCR` just created, or one a
92
+ * failed call left behind — gets the window as its expiry, so a count can't
93
+ * outlive its window and lock a reader out of the route for good. Separate
94
+ * commands, even pipelined, let the key expire between them, and the `INCR`
95
+ * after that created a key that never expired.
96
+ */
97
+ export const UPSTASH_COUNT_SCRIPT = `local count = redis.call("INCR", KEYS[1])
98
+ local ttl = redis.call("TTL", KEYS[1])
99
+ if ttl < 0 then
100
+ redis.call("EXPIRE", KEYS[1], ARGV[1])
101
+ ttl = tonumber(ARGV[1])
102
+ end
103
+ return {count, ttl}`;
104
+
105
+ /** Upstash's REST reply to one command. */
90
106
  interface UpstashReply {
91
107
  error?: string;
92
- result?: number | string | null;
108
+ result?: (number | null)[] | null;
93
109
  }
94
110
 
95
111
  /**
96
- * Count in Upstash Redis over its REST API: one pipeline opens the window
97
- * (`SET … EX … NX`), counts the request (`INCR`), and reads what's left of
98
- * the window (`TTL`).
112
+ * Count in Upstash Redis over its REST API: one `EVAL` of
113
+ * {@link UPSTASH_COUNT_SCRIPT} counts the request, opens the window when it's
114
+ * the first, and reads what's left of it.
99
115
  */
100
116
  export const upstashLimiter = (
101
117
  requests: number,
@@ -104,13 +120,15 @@ export const upstashLimiter = (
104
120
  token: string,
105
121
  fetchImpl: typeof fetch = fetch
106
122
  ): Limiter => {
107
- const url = `${endpoint.replace(/\/+$/u, "")}/pipeline`;
123
+ const url = endpoint.replace(/\/+$/u, "");
108
124
  return async (key) => {
109
125
  const response = await fetchImpl(url, {
110
126
  body: JSON.stringify([
111
- ["SET", key, "0", "EX", String(window), "NX"],
112
- ["INCR", key],
113
- ["TTL", key],
127
+ "EVAL",
128
+ UPSTASH_COUNT_SCRIPT,
129
+ "1",
130
+ key,
131
+ String(window),
114
132
  ]),
115
133
  headers: {
116
134
  authorization: `Bearer ${token}`,
@@ -121,11 +139,12 @@ export const upstashLimiter = (
121
139
  if (!response.ok) {
122
140
  throw new Error(`Upstash answered ${response.status}.`);
123
141
  }
124
- // SAFETY: Upstash's pipeline endpoint answers with one reply object per
125
- // command, in order; a missing or failed reply reads as `NaN` below.
126
- const replies = (await response.json()) as UpstashReply[];
127
- const count = Number(replies[1]?.result ?? Number.NaN);
128
- const ttl = Number(replies[2]?.result);
142
+ // SAFETY: Upstash's REST API answers a command with `{ result }`, here
143
+ // the script's `{count, ttl}`; a missing or failed reply reads as `NaN`
144
+ // below.
145
+ const reply = (await response.json()) as UpstashReply;
146
+ const count = Number(reply.result?.[0] ?? Number.NaN);
147
+ const ttl = Number(reply.result?.[1]);
129
148
  if (Number.isNaN(count)) {
130
149
  throw new TypeError("Upstash sent no count.");
131
150
  }
@@ -175,14 +194,13 @@ export const createLimiter = (
175
194
  );
176
195
  }
177
196
  if (adapter.kind === "upstash") {
178
- const [endpoint, token] = UPSTASH_SECRETS.map((name) =>
179
- runtime.secret?.(name)
180
- );
197
+ const secrets = upstashSecrets(adapter.options);
198
+ const [endpoint, token] = secrets.map((name) => runtime.secret?.(name));
181
199
  if (endpoint && token) {
182
200
  return upstashLimiter(requests, window, endpoint, token, runtime.fetch);
183
201
  }
184
202
  console.warn(
185
- `Rate limiting counts in memory: set ${UPSTASH_SECRETS.join(" and ")} to share the count through Upstash.`
203
+ `Rate limiting counts in memory: set ${secrets.join(" and ")} to share the count through Upstash.`
186
204
  );
187
205
  }
188
206
  return memoryLimiter(requests, window, runtime.now);
@@ -1,30 +1,60 @@
1
+ import { z } from "zod";
2
+
1
3
  import type { AdapterDescriptor } from "../core/adapter.ts";
2
4
  import { adapterDescriptorSchema } from "../core/adapter.ts";
3
5
  import { rateLimitOptionsSchema } from "./memory.ts";
4
6
  import type { RateLimitOptions } from "./memory.ts";
5
7
 
6
- /** The env vars holding the Upstash Redis REST endpoint and its token. */
7
- export const UPSTASH_SECRETS = [
8
- "UPSTASH_REDIS_REST_URL",
9
- "UPSTASH_REDIS_REST_TOKEN",
10
- ] as const;
8
+ /** The env var holding the Upstash Redis REST endpoint, unless set. */
9
+ const UPSTASH_URL_ENV = "UPSTASH_REDIS_REST_URL";
10
+
11
+ /** The env var holding the endpoint's token, unless set. */
12
+ const UPSTASH_TOKEN_ENV = "UPSTASH_REDIS_REST_TOKEN";
13
+
14
+ /** Options for {@link upstash}. */
15
+ export interface UpstashOptions extends RateLimitOptions {
16
+ /**
17
+ * Name of the env var holding the REST token. Defaults to
18
+ * `UPSTASH_REDIS_REST_TOKEN`; a database added from Vercel's Marketplace
19
+ * sets `KV_REST_API_TOKEN`.
20
+ */
21
+ tokenEnv?: string;
22
+ /**
23
+ * Name of the env var holding the REST endpoint. Defaults to
24
+ * `UPSTASH_REDIS_REST_URL`; a database added from Vercel's Marketplace
25
+ * sets `KV_REST_API_URL`.
26
+ */
27
+ urlEnv?: string;
28
+ }
11
29
 
12
- export type UpstashAdapter = AdapterDescriptor<"upstash", RateLimitOptions>;
30
+ export const upstashOptionsSchema = rateLimitOptionsSchema.extend({
31
+ tokenEnv: z.string().min(1).optional(),
32
+ urlEnv: z.string().min(1).optional(),
33
+ });
34
+
35
+ export type UpstashAdapter = AdapterDescriptor<"upstash", UpstashOptions>;
13
36
 
14
37
  export const upstashAdapterSchema = adapterDescriptorSchema(
15
38
  "upstash",
16
- rateLimitOptionsSchema
39
+ upstashOptionsSchema
17
40
  );
18
41
 
42
+ /** The env vars the endpoint and its token are read from, in that order. */
43
+ export const upstashSecrets = (options: UpstashOptions): [string, string] => [
44
+ options.urlEnv ?? UPSTASH_URL_ENV,
45
+ options.tokenEnv ?? UPSTASH_TOKEN_ENV,
46
+ ];
47
+
19
48
  /**
20
49
  * Keeps the count in Upstash Redis, which every instance on every host shares,
21
50
  * so the limit holds exactly. It talks to Upstash's REST API, so there is
22
51
  * nothing to install; the endpoint and token come from
23
- * `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`.
52
+ * `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`, or the env vars
53
+ * `urlEnv` and `tokenEnv` name.
24
54
  */
25
- export const upstash = (options: RateLimitOptions = {}): UpstashAdapter => ({
55
+ export const upstash = (options: UpstashOptions = {}): UpstashAdapter => ({
26
56
  kind: "upstash",
27
57
  options,
28
- requiredSecrets: [...UPSTASH_SECRETS],
58
+ requiredSecrets: upstashSecrets(options),
29
59
  runtimeDeps: [],
30
60
  });
@@ -3,16 +3,28 @@ import { z } from "zod";
3
3
  import type { AdapterDescriptor } from "../core/adapter.ts";
4
4
  import { adapterDescriptorSchema } from "../core/adapter.ts";
5
5
  import {
6
+ graphqlAuthSchema,
6
7
  graphqlSourceSchema,
7
8
  liftSpec,
8
9
  hasSources,
9
10
  missingSourcesIssue,
10
11
  sharedOptions,
11
12
  } from "./options.ts";
12
- import type { GraphqlSourceOptions, PlaygroundOptions } from "./options.ts";
13
+ import type {
14
+ GraphqlAuthOptions,
15
+ GraphqlSourceOptions,
16
+ PlaygroundOptions,
17
+ } from "./options.ts";
13
18
 
14
19
  /** Options for {@link graphql}. */
15
20
  export interface GraphqlOptions {
21
+ /**
22
+ * How the endpoint authenticates, like the site's `api.auth`: `bearer`,
23
+ * `basic`, or `key` (sent in the `name` header, `x-api-key` by default).
24
+ * The Try it panel gets a field for the credential and the code samples
25
+ * send a placeholder. Applies to every source; a per-source `auth` wins.
26
+ */
27
+ auth?: GraphqlAuthOptions;
16
28
  /**
17
29
  * Code-sample languages shown per operation, in order. Defaults to
18
30
  * `["curl", "js", "python"]`; `false` shows none.
@@ -52,6 +64,8 @@ export const graphqlOptionsSchema = z
52
64
  codeSamples: ["curl", "js", "python"],
53
65
  route: "/graphql",
54
66
  }),
67
+ /** Default endpoint auth for every source (per-source `auth` wins). */
68
+ auth: graphqlAuthSchema.optional(),
55
69
  /** Default live endpoint URL for every source (per-source `endpoint` wins). */
56
70
  endpoint: z.string().optional(),
57
71
  /** One or more schemas; each renders on its own route by default. */
@@ -27,6 +27,7 @@ export type { GraphqlAdapter, GraphqlOptions } from "./graphql.ts";
27
27
  export { openapi } from "./openapi.ts";
28
28
  export type { OpenApiAdapter, OpenApiOptions } from "./openapi.ts";
29
29
  export type {
30
+ GraphqlAuthOptions,
30
31
  GraphqlSourceOptions,
31
32
  PlaygroundOptions,
32
33
  ReferenceSourceOptions,
@@ -18,8 +18,8 @@ import type { OpenApiSourceOptions, PlaygroundOptions } from "./options.ts";
18
18
  export interface OpenApiOptions {
19
19
  /**
20
20
  * Code-sample languages generated per operation (Blume renderer), in
21
- * order. Defaults to `["curl", "js", "python"]`; `false` generates none, so
22
- * only the spec's own `x-codeSamples` show.
21
+ * order. Defaults to `["curl", "js", "python"]`; `false` (or an empty list)
22
+ * generates none, so only the spec's own `x-codeSamples` show.
23
23
  */
24
24
  codeSamples?: string[] | false;
25
25
  /** Start nested schema rows expanded rather than collapsed (Blume renderer). Defaults to `false`. */