blume 2.0.3 → 2.1.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 (455) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
  12. package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
  13. package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
  14. package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
  15. package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
  16. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  17. package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
  18. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
  19. package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
  20. package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
  21. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  22. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  23. package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
  24. package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
  25. package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
  26. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
  27. package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
  28. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  29. package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
  30. package/dist/cli/chunk-cm89scdv.js +443 -0
  31. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  32. package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
  33. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  34. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  35. package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
  36. package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
  37. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  38. package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
  39. package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
  40. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  41. package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
  42. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
  43. package/dist/cli/chunk-kwrg63tm.js +31 -0
  44. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  45. package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
  46. package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
  47. package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
  48. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
  49. package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
  50. package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
  51. package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
  52. package/dist/cli/chunk-q3wxxq39.js +126 -0
  53. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  54. package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
  55. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  56. package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
  57. package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
  58. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  59. package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
  60. package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
  61. package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
  62. package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
  63. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
  64. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  65. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  66. package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
  67. package/dist/cli/chunk-zf748avv.js.map +37 -0
  68. package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
  69. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
  70. package/dist/cli/index.js +26 -20
  71. package/dist/cli/index.js.map +5 -5
  72. package/dist/types/ai/agent-surface.d.ts +8 -7
  73. package/dist/types/ai/ai-catalog.d.ts +7 -0
  74. package/dist/types/ai/ask-context.d.ts +22 -0
  75. package/dist/types/ai/ask.d.ts +271 -58
  76. package/dist/types/ai/index.d.ts +2 -2
  77. package/dist/types/ai/site-skill.d.ts +26 -0
  78. package/dist/types/ai/skills.d.ts +16 -0
  79. package/dist/types/astro/integration.d.ts +8 -0
  80. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  81. package/dist/types/captcha/index.d.ts +28 -0
  82. package/dist/types/captcha/schema.d.ts +29 -0
  83. package/dist/types/captcha/turnstile.d.ts +28 -0
  84. package/dist/types/components/content/api-field.d.ts +59 -0
  85. package/dist/types/components/content/api-page.d.ts +95 -0
  86. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  87. package/dist/types/components/openapi/helpers.d.ts +137 -0
  88. package/dist/types/components/openapi/security.d.ts +85 -0
  89. package/dist/types/consent/ethyca.d.ts +48 -0
  90. package/dist/types/consent/index.d.ts +37 -0
  91. package/dist/types/consent/native.d.ts +28 -0
  92. package/dist/types/consent/osano.d.ts +42 -0
  93. package/dist/types/consent/schema.d.ts +110 -0
  94. package/dist/types/core/config-input.d.ts +187 -20
  95. package/dist/types/core/data.d.ts +49 -4
  96. package/dist/types/core/footer.d.ts +8 -0
  97. package/dist/types/core/i18n-ui.d.ts +86 -0
  98. package/dist/types/core/includes.d.ts +8 -2
  99. package/dist/types/core/localizable.d.ts +7 -0
  100. package/dist/types/core/page-modes.d.ts +29 -0
  101. package/dist/types/core/redirect-patterns.d.ts +144 -0
  102. package/dist/types/core/related.d.ts +40 -0
  103. package/dist/types/core/schema.d.ts +551 -56
  104. package/dist/types/core/sources/read.d.ts +5 -0
  105. package/dist/types/core/types.d.ts +1 -1
  106. package/dist/types/core/variables.d.ts +42 -0
  107. package/dist/types/deploy/node-headers.d.ts +13 -2
  108. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  109. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  110. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  111. package/dist/types/deploy/redirects.d.ts +39 -11
  112. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  113. package/dist/types/markdown/code-title.d.ts +17 -0
  114. package/dist/types/markdown/features.d.ts +3 -2
  115. package/dist/types/narration/build.d.ts +70 -0
  116. package/dist/types/narration/provider.d.ts +26 -0
  117. package/dist/types/narration/script.d.ts +154 -0
  118. package/dist/types/openapi/model.d.ts +12 -6
  119. package/dist/types/openapi/overlay.d.ts +54 -0
  120. package/dist/types/openapi/parse.d.ts +13 -0
  121. package/dist/types/openapi/references.d.ts +14 -4
  122. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  123. package/dist/types/ratelimit/index.d.ts +27 -0
  124. package/dist/types/ratelimit/memory.d.ts +36 -0
  125. package/dist/types/ratelimit/schema.d.ts +111 -0
  126. package/dist/types/ratelimit/upstash.d.ts +21 -0
  127. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  128. package/dist/types/reference/asyncapi.d.ts +10 -10
  129. package/dist/types/reference/graphql.d.ts +13 -10
  130. package/dist/types/reference/openapi.d.ts +33 -12
  131. package/dist/types/reference/options.d.ts +45 -5
  132. package/dist/types/reference/scalar.d.ts +13 -0
  133. package/dist/types/reference/schema.d.ts +60 -36
  134. package/dist/types/search/documents.d.ts +12 -1
  135. package/dist/types/search/orama-index.d.ts +4 -0
  136. package/dist/types/seo/metatags.d.ts +20 -0
  137. package/docs/02-deployment.mdx +41 -5
  138. package/docs/03-upgrading.mdx +4 -4
  139. package/docs/04-migrating.mdx +1 -1
  140. package/docs/advanced/changelog.mdx +2 -2
  141. package/docs/advanced/skills.mdx +14 -0
  142. package/docs/cli/audit.mdx +21 -1
  143. package/docs/cli/index.mdx +1 -0
  144. package/docs/configuration/analytics.mdx +3 -1
  145. package/docs/configuration/assistant.mdx +188 -36
  146. package/docs/configuration/consent.mdx +100 -0
  147. package/docs/configuration/customization.mdx +4 -4
  148. package/docs/configuration/index.mdx +63 -5
  149. package/docs/configuration/meta.ts +3 -0
  150. package/docs/configuration/narration.mdx +119 -0
  151. package/docs/configuration/rate-limiting.mdx +69 -0
  152. package/docs/configuration/search.mdx +29 -1
  153. package/docs/configuration/theming.mdx +3 -1
  154. package/docs/content/components.mdx +106 -0
  155. package/docs/content/frontmatter.mdx +76 -0
  156. package/docs/content/i18n.mdx +38 -3
  157. package/docs/content/includes.mdx +16 -0
  158. package/docs/content/meta.mdx +1 -0
  159. package/docs/content/meta.ts +1 -0
  160. package/docs/content/navigation.mdx +28 -6
  161. package/docs/content/sources/contentful.mdx +32 -0
  162. package/docs/content/sources/custom.mdx +39 -0
  163. package/docs/content/sources/github-releases.mdx +31 -0
  164. package/docs/content/sources/index.mdx +91 -0
  165. package/docs/content/sources/meta.ts +16 -0
  166. package/docs/content/sources/notion.mdx +32 -0
  167. package/docs/content/sources/obsidian.mdx +37 -0
  168. package/docs/content/sources/payload.mdx +30 -0
  169. package/docs/content/sources/remote-mdx.mdx +40 -0
  170. package/docs/content/sources/sanity.mdx +29 -0
  171. package/docs/content/sources/strapi.mdx +31 -0
  172. package/docs/content/syntax.mdx +67 -2
  173. package/docs/content/variables.mdx +41 -0
  174. package/docs/discoverability/agent-discovery.mdx +29 -2
  175. package/docs/discoverability/index.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/metadata.mdx +17 -0
  178. package/docs/references/api-pages.mdx +74 -0
  179. package/docs/references/asyncapi.mdx +1 -1
  180. package/docs/references/graphql.mdx +2 -2
  181. package/docs/references/meta.ts +1 -1
  182. package/docs/references/openapi.mdx +102 -3
  183. package/docs/references/scalar.mdx +3 -1
  184. package/package.json +36 -1
  185. package/skills/blume/SKILL.md +3 -0
  186. package/skills/blume-migrate/SKILL.md +5 -5
  187. package/skills/blume-migrate/references/docusaurus.md +3 -3
  188. package/skills/blume-migrate/references/mintlify.md +37 -19
  189. package/skills/blume-migrate/references/nextra.md +2 -2
  190. package/skills/blume-migrate/references/starlight.md +2 -1
  191. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  192. package/skills/blume-write-skill/SKILL.md +82 -0
  193. package/src/ai/agent-readability.ts +2 -1
  194. package/src/ai/agent-surface.ts +9 -7
  195. package/src/ai/ai-catalog.ts +6 -3
  196. package/src/ai/ask-context.ts +73 -20
  197. package/src/ai/ask-data.ts +6 -0
  198. package/src/ai/ask-tools.ts +142 -0
  199. package/src/ai/ask.ts +354 -76
  200. package/src/ai/component-markdown.ts +75 -0
  201. package/src/ai/cors.ts +2 -0
  202. package/src/ai/index.ts +12 -0
  203. package/src/ai/llms.ts +6 -2
  204. package/src/ai/markdown.ts +2 -1
  205. package/src/ai/mcp/data.ts +6 -0
  206. package/src/ai/openapi-components.ts +9 -3
  207. package/src/ai/site-skill.ts +274 -0
  208. package/src/ai/skills.ts +35 -2
  209. package/src/ai/write-skill.ts +97 -0
  210. package/src/astro/generate.ts +132 -18
  211. package/src/astro/index.ts +1 -0
  212. package/src/astro/integration.ts +35 -0
  213. package/src/astro/module-types.ts +6 -0
  214. package/src/astro/templates.ts +283 -30
  215. package/src/astro/variables.ts +31 -0
  216. package/src/audit/catalog.ts +19 -0
  217. package/src/audit/checks/contrast.ts +183 -0
  218. package/src/audit/checks/i18n.ts +2 -3
  219. package/src/audit/checks/indexability.ts +2 -3
  220. package/src/audit/checks/links.ts +5 -12
  221. package/src/audit/checks/sitemap.ts +2 -3
  222. package/src/audit/redirects.ts +46 -15
  223. package/src/audit/run.ts +2 -0
  224. package/src/audit/types.ts +2 -1
  225. package/src/blume-modules.d.ts +7 -0
  226. package/src/captcha/client.ts +152 -0
  227. package/src/captcha/hcaptcha.ts +36 -0
  228. package/src/captcha/index.ts +28 -0
  229. package/src/captcha/schema.ts +32 -0
  230. package/src/captcha/turnstile.ts +38 -0
  231. package/src/captcha/verify.ts +71 -0
  232. package/src/cli/command-meta.ts +5 -0
  233. package/src/cli/commands/doctor.ts +22 -16
  234. package/src/cli/commands/skill.ts +96 -0
  235. package/src/cli/index.ts +5 -0
  236. package/src/cli/required-secrets.ts +71 -29
  237. package/src/components/BlumePage.astro +2 -0
  238. package/src/components/content/ApiEndpoint.astro +18 -0
  239. package/src/components/content/ApiField.astro +79 -0
  240. package/src/components/content/ApiPlayground.astro +62 -0
  241. package/src/components/content/ApiRail.astro +19 -0
  242. package/src/components/content/Color.astro +1 -0
  243. package/src/components/content/FileTree.astro +1 -0
  244. package/src/components/content/GithubInfo.astro +1 -0
  245. package/src/components/content/Panel.astro +1 -0
  246. package/src/components/content/ParamField.astro +25 -0
  247. package/src/components/content/RequestExample.astro +17 -0
  248. package/src/components/content/ResponseExample.astro +17 -0
  249. package/src/components/content/ResponseField.astro +32 -0
  250. package/src/components/content/Step.astro +1 -0
  251. package/src/components/content/Tile.astro +1 -0
  252. package/src/components/content/Tooltip.astro +1 -0
  253. package/src/components/content/Tree.astro +1 -0
  254. package/src/components/content/TypeTable.astro +4 -1
  255. package/src/components/content/View.astro +21 -0
  256. package/src/components/content/ViewSwitcher.astro +168 -0
  257. package/src/components/content/api-field.ts +100 -0
  258. package/src/components/content/api-page.ts +224 -0
  259. package/src/components/content/view-switcher.ts +71 -0
  260. package/src/components/index.ts +2 -0
  261. package/src/components/islands/Assistant.astro +10 -1
  262. package/src/components/islands/assistant.tsx +104 -14
  263. package/src/components/islands/code-question.ts +62 -0
  264. package/src/components/islands/hooks.ts +110 -21
  265. package/src/components/islands/support-link.ts +68 -0
  266. package/src/components/layout/Analytics.astro +11 -2
  267. package/src/components/layout/Banner.astro +14 -10
  268. package/src/components/layout/Breadcrumbs.astro +6 -2
  269. package/src/components/layout/ConsentBanner.astro +62 -0
  270. package/src/components/layout/ConsentHead.astro +38 -0
  271. package/src/components/layout/Directory.astro +94 -0
  272. package/src/components/layout/DrawerSwitchers.astro +51 -0
  273. package/src/components/layout/Empty.astro +2 -2
  274. package/src/components/layout/Header.astro +122 -80
  275. package/src/components/layout/LanguageSwitcher.astro +48 -15
  276. package/src/components/layout/NarrationPlayer.astro +1085 -0
  277. package/src/components/layout/NavSelector.astro +33 -7
  278. package/src/components/layout/PageFeedback.astro +54 -49
  279. package/src/components/layout/PageLayout.astro +71 -3
  280. package/src/components/layout/ReferenceLayout.astro +17 -6
  281. package/src/components/layout/RelatedPages.astro +42 -0
  282. package/src/components/layout/RootLayout.astro +308 -45
  283. package/src/components/layout/Search.astro +31 -17
  284. package/src/components/layout/SiteFooter.astro +124 -0
  285. package/src/components/layout/SiteMetatags.astro +16 -0
  286. package/src/components/layout/consent/native.ts +79 -0
  287. package/src/components/layout/consent/types.ts +37 -0
  288. package/src/components/layout/footer-links.ts +53 -0
  289. package/src/components/layout/footer-socials.ts +87 -0
  290. package/src/components/layout/locale-redirect.ts +107 -0
  291. package/src/components/layout/nav-utils.ts +83 -0
  292. package/src/components/layout/page-feedback.ts +96 -0
  293. package/src/components/layout/search/analytics.ts +98 -0
  294. package/src/components/layout/search/flexsearch.ts +17 -5
  295. package/src/components/layout/search/orama-cloud.ts +26 -14
  296. package/src/components/layout/search/types.ts +20 -0
  297. package/src/components/layout/search/typesense.ts +5 -1
  298. package/src/components/openapi/Operation.astro +161 -13
  299. package/src/components/openapi/RequestBody.astro +6 -4
  300. package/src/components/openapi/RequestPanel.astro +59 -7
  301. package/src/components/openapi/Responses.astro +6 -4
  302. package/src/components/openapi/async-snippets.ts +4 -4
  303. package/src/components/openapi/code-samples.ts +97 -0
  304. package/src/components/openapi/security.ts +6 -3
  305. package/src/components/openapi/snippets.ts +869 -8
  306. package/src/components/props.ts +6 -0
  307. package/src/consent/client.ts +95 -0
  308. package/src/consent/clients.ts +19 -0
  309. package/src/consent/ethyca.ts +71 -0
  310. package/src/consent/head.ts +24 -0
  311. package/src/consent/index.ts +37 -0
  312. package/src/consent/init.ts +29 -0
  313. package/src/consent/native.ts +36 -0
  314. package/src/consent/osano.ts +59 -0
  315. package/src/consent/schema.ts +30 -0
  316. package/src/core/builtin-tags.ts +5 -0
  317. package/src/core/client-address.ts +19 -0
  318. package/src/core/config-input.ts +194 -20
  319. package/src/core/data.ts +44 -4
  320. package/src/core/diagnostics.ts +3 -2
  321. package/src/core/footer.ts +26 -0
  322. package/src/core/graph.ts +39 -36
  323. package/src/core/i18n-ui.ts +82 -2
  324. package/src/core/includes.ts +20 -3
  325. package/src/core/links.ts +16 -2
  326. package/src/core/localizable.ts +26 -0
  327. package/src/core/navigation.ts +43 -12
  328. package/src/core/page-modes.ts +56 -0
  329. package/src/core/project-graph.ts +111 -5
  330. package/src/core/redirect-patterns.ts +491 -0
  331. package/src/core/related.ts +131 -0
  332. package/src/core/schema.ts +318 -64
  333. package/src/core/sources/normalize.ts +2 -1
  334. package/src/core/sources/read.ts +8 -3
  335. package/src/core/types.ts +1 -1
  336. package/src/core/ui-packs/ar.ts +43 -0
  337. package/src/core/ui-packs/bg.ts +45 -0
  338. package/src/core/ui-packs/bn.ts +43 -0
  339. package/src/core/ui-packs/ca.ts +46 -0
  340. package/src/core/ui-packs/cs.ts +44 -0
  341. package/src/core/ui-packs/da.ts +45 -0
  342. package/src/core/ui-packs/de.ts +46 -0
  343. package/src/core/ui-packs/el.ts +45 -0
  344. package/src/core/ui-packs/es.ts +46 -0
  345. package/src/core/ui-packs/fa.ts +45 -0
  346. package/src/core/ui-packs/fi.ts +45 -0
  347. package/src/core/ui-packs/fr.ts +46 -0
  348. package/src/core/ui-packs/he.ts +43 -0
  349. package/src/core/ui-packs/hi.ts +44 -0
  350. package/src/core/ui-packs/hr.ts +45 -0
  351. package/src/core/ui-packs/hu.ts +44 -0
  352. package/src/core/ui-packs/id.ts +45 -0
  353. package/src/core/ui-packs/it.ts +45 -0
  354. package/src/core/ui-packs/ja.ts +45 -0
  355. package/src/core/ui-packs/ko.ts +44 -0
  356. package/src/core/ui-packs/nl.ts +46 -0
  357. package/src/core/ui-packs/no.ts +44 -0
  358. package/src/core/ui-packs/pl.ts +45 -0
  359. package/src/core/ui-packs/pt-br.ts +46 -0
  360. package/src/core/ui-packs/pt.ts +45 -0
  361. package/src/core/ui-packs/ro.ts +45 -0
  362. package/src/core/ui-packs/ru.ts +45 -0
  363. package/src/core/ui-packs/sk.ts +44 -0
  364. package/src/core/ui-packs/sr.ts +45 -0
  365. package/src/core/ui-packs/sv.ts +44 -0
  366. package/src/core/ui-packs/th.ts +43 -0
  367. package/src/core/ui-packs/tr.ts +45 -0
  368. package/src/core/ui-packs/uk.ts +45 -0
  369. package/src/core/ui-packs/vi.ts +45 -0
  370. package/src/core/ui-packs/zh-tw.ts +43 -0
  371. package/src/core/ui-packs/zh.ts +43 -0
  372. package/src/core/variables.ts +103 -0
  373. package/src/deploy/artifacts.ts +77 -8
  374. package/src/deploy/cloudflare-negotiation.ts +35 -5
  375. package/src/deploy/headers.ts +2 -1
  376. package/src/deploy/node-headers.ts +63 -9
  377. package/src/deploy/platforms/cloudflare.ts +32 -0
  378. package/src/deploy/platforms/netlify.ts +33 -1
  379. package/src/deploy/platforms/vercel.ts +37 -2
  380. package/src/deploy/redirects.ts +91 -24
  381. package/src/deploy/vercel-negotiation.ts +36 -0
  382. package/src/markdown/api-rail.ts +259 -0
  383. package/src/markdown/code-title.ts +35 -9
  384. package/src/markdown/features.ts +3 -2
  385. package/src/markdown/fence-meta.ts +2 -0
  386. package/src/markdown/include.ts +11 -1
  387. package/src/markdown/index.ts +28 -5
  388. package/src/markdown/math.ts +4 -4
  389. package/src/markdown/variables.ts +56 -0
  390. package/src/markdown/views.ts +82 -0
  391. package/src/narration/build.ts +412 -0
  392. package/src/narration/player.ts +249 -0
  393. package/src/narration/provider.ts +29 -0
  394. package/src/narration/script.ts +592 -0
  395. package/src/og/card.ts +7 -6
  396. package/src/openapi/model.ts +57 -24
  397. package/src/openapi/overlay.ts +256 -0
  398. package/src/openapi/parse.ts +55 -2
  399. package/src/openapi/references.ts +21 -5
  400. package/src/openapi/render-mdx.ts +5 -1
  401. package/src/openapi/scalar.ts +40 -6
  402. package/src/openapi/source.ts +1 -1
  403. package/src/ratelimit/cloudflare.ts +54 -0
  404. package/src/ratelimit/index.ts +30 -0
  405. package/src/ratelimit/memory.ts +45 -0
  406. package/src/ratelimit/runtime.ts +229 -0
  407. package/src/ratelimit/schema.ts +28 -0
  408. package/src/ratelimit/upstash.ts +30 -0
  409. package/src/ratelimit/wrangler.ts +70 -0
  410. package/src/reference/asyncapi.ts +2 -2
  411. package/src/reference/graphql.ts +5 -2
  412. package/src/reference/openapi.ts +21 -7
  413. package/src/reference/options.ts +51 -7
  414. package/src/reference/scalar.ts +33 -8
  415. package/src/registry/eject.ts +20 -4
  416. package/src/registry/registry.ts +7 -0
  417. package/src/search/documents.ts +26 -2
  418. package/src/search/orama-index.ts +28 -3
  419. package/src/search/pagefind-ranking.ts +39 -0
  420. package/src/search/sync/algolia.ts +15 -4
  421. package/src/search/sync/orama-cloud.ts +3 -0
  422. package/src/search/sync/typesense.ts +5 -0
  423. package/src/seo/metatags.ts +67 -0
  424. package/src/theme/chrome-icons.ts +1 -0
  425. package/src/theme/color.ts +500 -0
  426. package/src/theme/entry.ts +56 -3
  427. package/src/theme/palette.ts +121 -45
  428. package/src/translate/prompts.ts +1 -0
  429. package/src/translate/validate.ts +8 -2
  430. package/src/upgrade/upgrade.ts +4 -171
  431. package/dist/cli/chunk-364znk6q.js +0 -75
  432. package/dist/cli/chunk-364znk6q.js.map +0 -11
  433. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  434. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  435. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  436. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  437. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  438. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  439. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  440. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  441. package/docs/content/sources.mdx +0 -334
  442. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
  443. /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
  444. /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
  445. /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
  446. /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
  447. /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
  448. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
  449. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
  450. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
  451. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
  452. /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
  453. /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
  454. /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
  455. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
@@ -0,0 +1,183 @@
1
+ import type { ResolvedConfig } from "../../core/schema.ts";
2
+ import type { Diagnostic } from "../../core/types.ts";
3
+ import { composite, contrastRatio, parseColor } from "../../theme/color.ts";
4
+ import type { Rgba } from "../../theme/color.ts";
5
+ import {
6
+ MIN_TEXT_CONTRAST,
7
+ labelOn,
8
+ resolveAccent,
9
+ resolveAction,
10
+ themeInk,
11
+ } from "../../theme/palette.ts";
12
+ import type { ColorMode } from "../../theme/palette.ts";
13
+ import { finding } from "../catalog.ts";
14
+ import type { CheckModule } from "../types.ts";
15
+
16
+ const MODES = ["light", "dark"] as const satisfies readonly ColorMode[];
17
+
18
+ /** The theme config key a color comes from. */
19
+ type ThemeKey = "theme.accent" | "theme.action" | "theme.background";
20
+
21
+ /** A color the theme draws text in, over the surface it sits on. */
22
+ interface TextPair {
23
+ fix: string;
24
+ key: ThemeKey;
25
+ /** What the pair is, for the message: "Accent text on the page". */
26
+ label: string;
27
+ surface: Rgba;
28
+ text: Rgba;
29
+ }
30
+
31
+ /** A theme color, resolved for one mode, and the config key it came from. */
32
+ interface ThemeColor {
33
+ key: ThemeKey;
34
+ value: string;
35
+ }
36
+
37
+ /** What one mode's check found: pairs to measure, colors it couldn't read. */
38
+ interface ModeColors {
39
+ pairs: TextPair[];
40
+ unread: ThemeColor[];
41
+ }
42
+
43
+ /** A fill and the label the build puts on it (`labelOn`, as `readableOn`). */
44
+ const fillPair = (
45
+ key: "theme.accent" | "theme.action",
46
+ fill: Rgba,
47
+ page: Rgba,
48
+ mode: ColorMode
49
+ ): TextPair => ({
50
+ fix: `Neither white nor dark text reaches ${MIN_TEXT_CONTRAST}:1 on this shade. Move \`${key}\`'s ${mode} shade lighter or darker.`,
51
+ key,
52
+ label:
53
+ key === "theme.action"
54
+ ? "The call-to-action label"
55
+ : "Button labels on the accent",
56
+ surface: composite(fill, page),
57
+ text: parseColor(labelOn(fill, mode)) ?? fill,
58
+ });
59
+
60
+ /** The accent's pairs: accent text on the page, and labels on accent fills. */
61
+ const accentPairs = (fill: Rgba, page: Rgba, mode: ColorMode): TextPair[] => [
62
+ {
63
+ fix: `${mode === "light" ? "Darken" : "Lighten"} \`theme.accent\`'s ${mode} shade (\`theme.accent: { light, dark }\`).`,
64
+ key: "theme.accent",
65
+ label: "Accent text on the page",
66
+ surface: page,
67
+ text: fill,
68
+ },
69
+ fillPair("theme.accent", fill, page, mode),
70
+ ];
71
+
72
+ /** The pairs a custom page background sets: body and secondary text on it. */
73
+ const backgroundPairs = (page: Rgba, mode: ColorMode): TextPair[] => {
74
+ const fix = `Pick a \`theme.background\` ${mode} color further from the text color.`;
75
+ return [
76
+ {
77
+ fix,
78
+ key: "theme.background",
79
+ label: "Body text on the page",
80
+ surface: page,
81
+ text: themeInk(mode, "foreground"),
82
+ },
83
+ {
84
+ fix,
85
+ key: "theme.background",
86
+ label: "Secondary text on the page",
87
+ surface: page,
88
+ text: themeInk(mode, "mutedForeground"),
89
+ },
90
+ ];
91
+ };
92
+
93
+ /** Everything one mode draws text in, from the theme config. */
94
+ const modeColors = (
95
+ theme: ResolvedConfig["theme"],
96
+ mode: ColorMode
97
+ ): ModeColors => {
98
+ const unread: ThemeColor[] = [];
99
+ const read = (color: ThemeColor): Rgba | null => {
100
+ const parsed = parseColor(color.value);
101
+ if (!parsed) {
102
+ unread.push(color);
103
+ }
104
+ return parsed;
105
+ };
106
+ const custom = theme.background?.[mode];
107
+ const customPage =
108
+ custom === undefined
109
+ ? null
110
+ : read({ key: "theme.background", value: custom });
111
+ // Text on a page color the audit can't read can't be measured.
112
+ if (custom !== undefined && !customPage) {
113
+ return { pairs: [], unread };
114
+ }
115
+ const page = customPage ?? themeInk(mode, "background");
116
+ const pairs = customPage ? backgroundPairs(customPage, mode) : [];
117
+ const accent = read({
118
+ key: "theme.accent",
119
+ value: resolveAccent(theme)[mode],
120
+ });
121
+ if (accent) {
122
+ pairs.push(...accentPairs(accent, page, mode));
123
+ }
124
+ const action = resolveAction(theme);
125
+ const actionFill =
126
+ action && read({ key: "theme.action", value: action[mode] });
127
+ if (actionFill) {
128
+ pairs.push(fillPair("theme.action", actionFill, page, mode));
129
+ }
130
+ return { pairs, unread };
131
+ };
132
+
133
+ /**
134
+ * Theme contrast: the text colors a configured theme draws — accent text,
135
+ * labels on accent and call-to-action fills, and body text on a custom
136
+ * background — measured against WCAG AA's 4.5:1 in both color modes, resolved
137
+ * the way the build resolves them (presets per mode, labels by `labelOn`).
138
+ * One finding per failing pair, pointed at blume.config.ts, since a theme
139
+ * color is a site-wide choice, not a page's.
140
+ */
141
+ export const contrastChecks: CheckModule = {
142
+ category: "accessibility",
143
+ run(context) {
144
+ const { theme } = context.project.config;
145
+ const site = {
146
+ file: context.project.context.configFile ?? undefined,
147
+ url: "/",
148
+ };
149
+ const found: Diagnostic[] = [];
150
+ const reported = new Set<string>();
151
+ for (const mode of MODES) {
152
+ const { pairs, unread } = modeColors(theme, mode);
153
+ for (const color of unread) {
154
+ const id = `${color.key}:${color.value}`;
155
+ if (!reported.has(id)) {
156
+ reported.add(id);
157
+ found.push(
158
+ finding(
159
+ "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
160
+ site,
161
+ `\`${color.key}\` is "${color.value}", which the audit can't read as a color, so its contrast went unchecked.`
162
+ )
163
+ );
164
+ }
165
+ }
166
+ for (const pair of pairs) {
167
+ const ratio = contrastRatio(pair.text, pair.surface);
168
+ if (ratio < MIN_TEXT_CONTRAST) {
169
+ found.push(
170
+ finding(
171
+ "BLUME_AUDIT_THEME_CONTRAST_LOW",
172
+ site,
173
+ `${pair.label} in ${mode} mode is ${ratio.toFixed(2)}:1, under the ${MIN_TEXT_CONTRAST}:1 WCAG AA asks of body text (\`${pair.key}\`).`,
174
+ pair.fix
175
+ )
176
+ );
177
+ }
178
+ }
179
+ }
180
+ return found;
181
+ },
182
+ tier: "static",
183
+ };
@@ -2,6 +2,7 @@ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
2
2
  import type { Diagnostic } from "../../core/types.ts";
3
3
  import { finding } from "../catalog.ts";
4
4
  import { pageSite } from "../locate.ts";
5
+ import { redirectAt } from "../redirects.ts";
5
6
  import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
6
7
  import { decodePath, normalizePath } from "../url.ts";
7
8
  import { isArchivedLatestCanonical } from "./indexability.ts";
@@ -134,9 +135,7 @@ const hreflangChecks = (
134
135
  }
135
136
 
136
137
  const target = context.byUrl.get(path);
137
- const redirect = context.redirects.find(
138
- (entry) => normalizePath(entry.from) === path
139
- );
138
+ const redirect = redirectAt(context.redirects, path);
140
139
  if (redirect) {
141
140
  found.push(
142
141
  finding(
@@ -3,6 +3,7 @@ import type { Diagnostic } from "../../core/types.ts";
3
3
  import { deployPlatform } from "../../deploy/platforms/index.ts";
4
4
  import { finding } from "../catalog.ts";
5
5
  import { pageSite } from "../locate.ts";
6
+ import { redirectAt } from "../redirects.ts";
6
7
  import { ERROR_ROUTES } from "../types.ts";
7
8
  import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
8
9
  import { decodePath, normalizePath, siteOrigin } from "../url.ts";
@@ -118,9 +119,7 @@ const canonicalChecks = (
118
119
 
119
120
  // The canonical points at another page on this site. It must exist, and it
120
121
  // must not itself redirect — a canonical to a redirect is a dead end.
121
- const redirect = context.redirects.find(
122
- (entry) => normalizePath(entry.from) === target
123
- );
122
+ const redirect = redirectAt(context.redirects, target);
124
123
  if (redirect) {
125
124
  found.push(
126
125
  finding(
@@ -3,13 +3,9 @@ import type { Diagnostic } from "../../core/types.ts";
3
3
  import { finding } from "../catalog.ts";
4
4
  import { orphanPages } from "../graph.ts";
5
5
  import { pageSite } from "../locate.ts";
6
- import type {
7
- AuditContext,
8
- CheckModule,
9
- PageSnapshot,
10
- SnapshotLink,
11
- } from "../types.ts";
12
- import { isServed, normalizePath, resolveHref, siteOrigin } from "../url.ts";
6
+ import { redirectAt } from "../redirects.ts";
7
+ import type { CheckModule, PageSnapshot, SnapshotLink } from "../types.ts";
8
+ import { isServed, resolveHref, siteOrigin } from "../url.ts";
13
9
 
14
10
  /** Browser-magic fragments that scroll without needing a matching id. */
15
11
  const MAGIC_FRAGMENTS = new Set(["", "top"]);
@@ -33,9 +29,6 @@ const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
33
29
  }
34
30
  };
35
31
 
36
- const redirectFrom = (context: AuditContext, path: string) =>
37
- context.redirects.find((entry) => normalizePath(entry.from) === path);
38
-
39
32
  /**
40
33
  * Internal links, and what they land on.
41
34
  *
@@ -150,7 +143,7 @@ export const linkChecks: CheckModule = {
150
143
  }
151
144
 
152
145
  const { path } = resolved;
153
- const redirect = redirectFrom(context, path);
146
+ const redirect = redirectAt(context.redirects, path);
154
147
  if (redirect) {
155
148
  if (link.content) {
156
149
  found.push(
@@ -200,7 +193,7 @@ export const linkChecks: CheckModule = {
200
193
  );
201
194
  }
202
195
  for (const [path, page] of redirectedChrome) {
203
- const redirect = redirectFrom(context, path);
196
+ const redirect = redirectAt(context.redirects, path);
204
197
  found.push(
205
198
  finding(
206
199
  "BLUME_AUDIT_LINK_TO_REDIRECT",
@@ -2,6 +2,7 @@ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
2
2
  import type { Diagnostic } from "../../core/types.ts";
3
3
  import { finding } from "../catalog.ts";
4
4
  import { pageSite } from "../locate.ts";
5
+ import { redirectAt } from "../redirects.ts";
5
6
  import type { AuditContext, CheckModule } from "../types.ts";
6
7
  import { decodePath, normalizePath, siteOrigin } from "../url.ts";
7
8
 
@@ -100,9 +101,7 @@ const checkListedUrl = (
100
101
  );
101
102
  const page = context.byUrl.get(path);
102
103
  if (!page) {
103
- const redirect = context.redirects.find(
104
- (entry) => normalizePath(entry.from) === path
105
- );
104
+ const redirect = redirectAt(context.redirects, path);
106
105
  return [
107
106
  finding(
108
107
  "BLUME_AUDIT_SITEMAP_BAD_URL",
@@ -1,3 +1,7 @@
1
+ import {
2
+ isPatternPath,
3
+ patternDestination,
4
+ } from "../core/redirect-patterns.ts";
1
5
  import type { RedirectResolution } from "./types.ts";
2
6
  import { normalizePath } from "./url.ts";
3
7
 
@@ -7,18 +11,6 @@ interface ConfiguredRedirect {
7
11
  status: number;
8
12
  }
9
13
 
10
- /**
11
- * Follow every configured redirect through to its destination, classifying what
12
- * it lands on.
13
- *
14
- * - `loop` — the chain revisits a hop it has already been to. Never resolves.
15
- * - `broken` — the chain ends somewhere the build does not serve.
16
- * - `chain` — it resolves, but through at least one intermediate redirect.
17
- * - `ok` — one hop, straight to a real page.
18
- *
19
- * An external destination (`https://…`) is always `ok`: it's outside the site,
20
- * so there's no local page to check it against.
21
- */
22
14
  /**
23
15
  * A destination's page path: the part before any query string or fragment. A
24
16
  * redirect to `/guide#setup` or `/search?q=x` lands on the `/guide` / `/search`
@@ -30,6 +22,22 @@ const pathOnly = (value: string): string => {
30
22
  return cut === -1 ? value : value.slice(0, cut);
31
23
  };
32
24
 
25
+ /**
26
+ * Follow every configured redirect through to its destination, classifying what
27
+ * it lands on.
28
+ *
29
+ * - `loop` — the chain revisits a hop it has already been to. Never resolves.
30
+ * - `broken` — the chain ends somewhere the build does not serve.
31
+ * - `chain` — it resolves, but through at least one intermediate redirect.
32
+ * - `ok` — one hop, straight to a real page.
33
+ * - `pattern` — a pattern (`/beta/:slug*`), which covers paths rather than
34
+ * naming one, so there is no single chain to walk; {@link redirectAt}
35
+ * resolves a path it covers.
36
+ *
37
+ * An external destination (`https://…`) is always `ok`: it's outside the site,
38
+ * so there's no local page to check it against. A hop onto a path a pattern
39
+ * covers continues through that pattern.
40
+ */
33
41
  export const resolveRedirects = (
34
42
  redirects: readonly ConfiguredRedirect[],
35
43
  /** Whether the build serves a normalized path — see `isServed`. */
@@ -39,9 +47,14 @@ export const resolveRedirects = (
39
47
  for (const redirect of redirects) {
40
48
  byFrom.set(normalizePath(redirect.from), redirect);
41
49
  }
50
+ const hopFrom = (path: string): string | undefined =>
51
+ byFrom.get(path)?.to ?? patternDestination(redirects, path);
42
52
 
43
53
  return redirects.map((redirect) => {
44
54
  const from = normalizePath(redirect.from);
55
+ if (isPatternPath(redirect.from)) {
56
+ return { ...redirect, chain: [from], outcome: "pattern" as const };
57
+ }
45
58
  const chain: string[] = [from];
46
59
  const seen = new Set<string>([from]);
47
60
  let current = redirect.to;
@@ -63,11 +76,11 @@ export const resolveRedirects = (
63
76
  }
64
77
  chain.push(next);
65
78
  seen.add(next);
66
- const hop = byFrom.get(next);
67
- if (!hop) {
79
+ const hop = hopFrom(next);
80
+ if (hop === undefined) {
68
81
  break;
69
82
  }
70
- current = hop.to;
83
+ current = hop;
71
84
  }
72
85
 
73
86
  const destination = chain.at(-1) ?? from;
@@ -84,3 +97,21 @@ export const resolveRedirects = (
84
97
  };
85
98
  });
86
99
  };
100
+
101
+ /**
102
+ * The configured redirect a path takes, for the checks that report a link,
103
+ * canonical, sitemap entry, or `hreflang` pointing through one: the exact
104
+ * redirect from that path, else the pattern covering it, its `to` filled in
105
+ * for this path.
106
+ */
107
+ export const redirectAt = (
108
+ redirects: readonly RedirectResolution[],
109
+ path: string
110
+ ): { to: string } | undefined => {
111
+ const exact = redirects.find((entry) => normalizePath(entry.from) === path);
112
+ if (exact) {
113
+ return exact;
114
+ }
115
+ const to = patternDestination(redirects, path);
116
+ return to === undefined ? undefined : { to };
117
+ };
package/src/audit/run.ts CHANGED
@@ -12,6 +12,7 @@ import { CHECKS } from "./catalog.ts";
12
12
  import type { CheckId } from "./catalog.ts";
13
13
  import { assetChecks } from "./checks/assets.ts";
14
14
  import { contentChecks } from "./checks/content.ts";
15
+ import { contrastChecks } from "./checks/contrast.ts";
15
16
  import { dnsAidChecks } from "./checks/dns-aid.ts";
16
17
  import { duplicateChecks } from "./checks/duplicates.ts";
17
18
  import { i18nChecks } from "./checks/i18n.ts";
@@ -51,6 +52,7 @@ const MODULES: CheckModule[] = [
51
52
  ogImageChecks,
52
53
  i18nChecks,
53
54
  assetChecks,
55
+ contrastChecks,
54
56
  sitemapChecks,
55
57
  robotsChecks,
56
58
  llmsChecks,
@@ -13,6 +13,7 @@ import type {
13
13
  export type AuditTier = "static" | "network" | "external";
14
14
 
15
15
  export type AuditCategory =
16
+ | "accessibility"
16
17
  | "content"
17
18
  | "duplicates"
18
19
  | "indexability"
@@ -110,7 +111,7 @@ export interface RedirectResolution {
110
111
  status: number;
111
112
  /** Every hop from `from` to the final target, inclusive. */
112
113
  chain: string[];
113
- outcome: "ok" | "loop" | "broken" | "chain";
114
+ outcome: "ok" | "loop" | "broken" | "chain" | "pattern";
114
115
  }
115
116
 
116
117
  /** One urlset file behind a sitemap, for the per-file size limits. */
@@ -15,6 +15,13 @@ declare module "blume:search-client" {
15
15
  export const createSearch: () => Fn | Promise<Fn>;
16
16
  }
17
17
 
18
+ declare module "blume:consent-client" {
19
+ /** Start the configured consent adapter's browser module (see `consentClientTemplate`); a no-op for one without. */
20
+ // oxlint-disable-next-line typescript/consistent-type-imports
21
+ type Consent = import("./components/layout/consent/types.ts").BlumeConsent;
22
+ export const startConsentClient: (consent: Consent) => void;
23
+ }
24
+
18
25
  declare module "blume:ask" {
19
26
  /** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
20
27
  // Mirrors the generated `.blume/src/env.d.ts` declaration: the trigger's type
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The browser half of the bot check: a fresh token for each question the
3
+ * assistant sends (both providers' tokens are single-use). The provider's
4
+ * script loads with the first question rather than with the page, and one
5
+ * widget serves every question after it: an invisible hCaptcha, or a
6
+ * Turnstile that only shows itself when it needs the reader to interact,
7
+ * in a small container at the foot of the viewport.
8
+ *
9
+ * The client router swaps `<body>` on every navigation, taking the container
10
+ * and its widget with it, so a question after a swap mounts a new one.
11
+ */
12
+ import type { CaptchaSettings } from "./schema.ts";
13
+
14
+ /** Each provider's script, rendering only when asked to. */
15
+ export const CAPTCHA_SCRIPTS = {
16
+ hcaptcha: "https://js.hcaptcha.com/1/api.js?render=explicit",
17
+ turnstile:
18
+ "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit",
19
+ } as const;
20
+
21
+ /** What Blume asks a Turnstile widget for. */
22
+ interface TurnstileRenderOptions {
23
+ appearance: "interaction-only";
24
+ callback: (token: string) => void;
25
+ "error-callback": () => void;
26
+ execution: "execute";
27
+ sitekey: string;
28
+ }
29
+
30
+ /** The slice of `window.turnstile` the check uses. */
31
+ export interface TurnstileApi {
32
+ execute: (widget: string) => void;
33
+ render: (container: HTMLElement, options: TurnstileRenderOptions) => string;
34
+ reset: (widget: string) => void;
35
+ }
36
+
37
+ /** What Blume asks an hCaptcha widget for. */
38
+ interface HcaptchaRenderOptions {
39
+ sitekey: string;
40
+ size: "invisible";
41
+ }
42
+
43
+ /** The slice of `window.hcaptcha` the check uses. */
44
+ export interface HcaptchaApi {
45
+ execute: (
46
+ widget: string,
47
+ options: { async: true }
48
+ ) => Promise<{ response: string }>;
49
+ render: (container: HTMLElement, options: HcaptchaRenderOptions) => string;
50
+ reset: (widget: string) => void;
51
+ }
52
+
53
+ /** The window, with the providers' globals once their scripts ran. */
54
+ type CaptchaWindow = Window & {
55
+ hcaptcha?: HcaptchaApi;
56
+ turnstile?: TurnstileApi;
57
+ };
58
+
59
+ /** A Turnstile token on its way. */
60
+ interface PendingToken {
61
+ reject: (error: Error) => void;
62
+ resolve: (token: string) => void;
63
+ }
64
+
65
+ /** Get a token for the next question. */
66
+ export type CaptchaTokenGetter = (settings: CaptchaSettings) => Promise<string>;
67
+
68
+ /** A token getter with its own script, container, and widget. */
69
+ export const createCaptchaClient = (): CaptchaTokenGetter => {
70
+ let loaded: Promise<void> | undefined;
71
+ let host: HTMLElement | undefined;
72
+ let widget: string | undefined;
73
+ let pending: PendingToken | undefined;
74
+
75
+ const load = (kind: CaptchaSettings["kind"]): Promise<void> => {
76
+ // oxlint-disable-next-line promise/avoid-new -- adapt the script tag's load events
77
+ loaded ??= new Promise<void>((resolve, reject) => {
78
+ const tag = document.createElement("script");
79
+ tag.async = true;
80
+ tag.src = CAPTCHA_SCRIPTS[kind];
81
+ tag.addEventListener("load", () => resolve());
82
+ tag.addEventListener("error", () => {
83
+ // Let the next question try again.
84
+ loaded = undefined;
85
+ reject(new Error(`The ${kind} script didn't load.`));
86
+ });
87
+ document.head.append(tag);
88
+ });
89
+ return loaded;
90
+ };
91
+
92
+ // The widget's container, mounted again when a swap took the last one.
93
+ const mount = (): HTMLElement => {
94
+ if (host?.isConnected) {
95
+ return host;
96
+ }
97
+ host = document.createElement("div");
98
+ host.className = "fixed bottom-4 left-1/2 z-[60] -translate-x-1/2";
99
+ host.dataset.blumeCaptcha = "";
100
+ document.body.append(host);
101
+ widget = undefined;
102
+ return host;
103
+ };
104
+
105
+ const turnstileToken = (api: TurnstileApi, siteKey: string) =>
106
+ // oxlint-disable-next-line promise/avoid-new -- adapt Turnstile's token callbacks
107
+ new Promise<string>((resolve, reject) => {
108
+ pending = { reject, resolve };
109
+ const target = mount();
110
+ if (widget === undefined) {
111
+ widget = api.render(target, {
112
+ appearance: "interaction-only",
113
+ callback: (token) => pending?.resolve(token),
114
+ "error-callback": () =>
115
+ pending?.reject(new Error("The Turnstile check failed.")),
116
+ execution: "execute",
117
+ sitekey: siteKey,
118
+ });
119
+ } else {
120
+ api.reset(widget);
121
+ }
122
+ api.execute(widget);
123
+ });
124
+
125
+ const hcaptchaToken = async (api: HcaptchaApi, siteKey: string) => {
126
+ const target = mount();
127
+ if (widget === undefined) {
128
+ widget = api.render(target, { sitekey: siteKey, size: "invisible" });
129
+ } else {
130
+ api.reset(widget);
131
+ }
132
+ const { response } = await api.execute(widget, { async: true });
133
+ return response;
134
+ };
135
+
136
+ return async (settings) => {
137
+ await load(settings.kind);
138
+ // SAFETY: CaptchaWindow only adds the providers' optional globals, each
139
+ // checked before use.
140
+ const { hcaptcha, turnstile } = window as CaptchaWindow;
141
+ if (settings.kind === "hcaptcha" && hcaptcha) {
142
+ return hcaptchaToken(hcaptcha, settings.siteKey);
143
+ }
144
+ if (settings.kind === "turnstile" && turnstile) {
145
+ return turnstileToken(turnstile, settings.siteKey);
146
+ }
147
+ throw new Error(`The ${settings.kind} script loaded without its API.`);
148
+ };
149
+ };
150
+
151
+ /** The page's token getter: one script and one widget per page load. */
152
+ export const captchaToken = createCaptchaClient();
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+
3
+ import type { AdapterDescriptor } from "../core/adapter.ts";
4
+ import { adapterDescriptorSchema } from "../core/adapter.ts";
5
+
6
+ /** Options for {@link hcaptcha}. */
7
+ export interface HcaptchaOptions {
8
+ /** The site key, from the hCaptcha dashboard. */
9
+ siteKey: string;
10
+ }
11
+
12
+ export const hcaptchaOptionsSchema = z.strictObject({
13
+ siteKey: z.string().min(1),
14
+ });
15
+
16
+ export type HcaptchaAdapter = AdapterDescriptor<"hcaptcha", HcaptchaOptions>;
17
+
18
+ export const hcaptchaAdapterSchema = adapterDescriptorSchema(
19
+ "hcaptcha",
20
+ hcaptchaOptionsSchema
21
+ );
22
+
23
+ /** Where the assistant's route checks an hCaptcha token. */
24
+ export const HCAPTCHA_VERIFY_URL = "https://api.hcaptcha.com/siteverify";
25
+
26
+ /**
27
+ * hCaptcha, as an invisible check: most readers never see a challenge. The
28
+ * site key is public and goes to the browser; the secret the route checks
29
+ * tokens with comes from `HCAPTCHA_SECRET_KEY`.
30
+ */
31
+ export const hcaptcha = (options: HcaptchaOptions): HcaptchaAdapter => ({
32
+ kind: "hcaptcha",
33
+ options,
34
+ requiredSecrets: ["HCAPTCHA_SECRET_KEY"],
35
+ runtimeDeps: [],
36
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bot checks for the assistant (`ai.assistant.captcha` in `blume.config.ts`).
3
+ * The assistant gets a token from the check before each question and the
4
+ * generated route verifies it before calling the model:
5
+ *
6
+ * ```ts
7
+ * import { defineConfig } from "blume";
8
+ * import { turnstile } from "blume/captcha";
9
+ *
10
+ * export default defineConfig({
11
+ * ai: {
12
+ * assistant: {
13
+ * enabled: true,
14
+ * captcha: turnstile({ siteKey: "0x4AAAAAAA…" }),
15
+ * },
16
+ * },
17
+ * });
18
+ * ```
19
+ *
20
+ * Each factory returns a plain descriptor (see `core/adapter.ts`); the browser
21
+ * half is `client.ts`, the server half `verify.ts`.
22
+ */
23
+ export type { AdapterDescriptor, JsonValue } from "../core/adapter.ts";
24
+ export { hcaptcha } from "./hcaptcha.ts";
25
+ export type { HcaptchaAdapter, HcaptchaOptions } from "./hcaptcha.ts";
26
+ export type { CaptchaAdapter, CaptchaSettings } from "./schema.ts";
27
+ export { turnstile } from "./turnstile.ts";
28
+ export type { TurnstileAdapter, TurnstileOptions } from "./turnstile.ts";
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+
3
+ import { hcaptchaAdapterSchema } from "./hcaptcha.ts";
4
+ import { turnstileAdapterSchema } from "./turnstile.ts";
5
+
6
+ /**
7
+ * The configured bot check, as its factory returned it. Anything that isn't
8
+ * one fails with the adapter hint; an adapter's own option errors keep their
9
+ * messages.
10
+ */
11
+ export const captchaAdapterSchema = z.discriminatedUnion(
12
+ "kind",
13
+ [hcaptchaAdapterSchema, turnstileAdapterSchema],
14
+ {
15
+ error:
16
+ 'captcha takes one adapter — e.g. `captcha: turnstile({ siteKey })` or `captcha: hcaptcha({ siteKey })`, imported from "blume/captcha".',
17
+ }
18
+ );
19
+
20
+ export type CaptchaAdapter = z.output<typeof captchaAdapterSchema>;
21
+
22
+ /** What the browser needs to run the check: never the secret. */
23
+ export interface CaptchaSettings {
24
+ kind: CaptchaAdapter["kind"];
25
+ siteKey: string;
26
+ }
27
+
28
+ /** The public half of an adapter, for the data snapshot. */
29
+ export const captchaSettings = (adapter: CaptchaAdapter): CaptchaSettings => ({
30
+ kind: adapter.kind,
31
+ siteKey: adapter.options.siteKey,
32
+ });