blume 2.0.3 → 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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +142 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
- package/dist/cli/{chunk-5f86nr5m.js.map → chunk-073rdvgb.js.map} +4 -5
- package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
- package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
- package/dist/cli/chunk-150jk7km.js +52 -0
- package/dist/cli/chunk-150jk7km.js.map +10 -0
- package/dist/cli/{chunk-9t7a85s3.js → chunk-313vnn40.js} +30 -19
- package/dist/cli/chunk-313vnn40.js.map +10 -0
- package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
- package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
- package/dist/cli/chunk-3x6x3pph.js.map +10 -0
- package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
- package/dist/cli/chunk-42t4jxdx.js.map +12 -0
- package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
- package/dist/cli/chunk-54ztw1nj.js.map +10 -0
- package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
- package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
- package/dist/cli/chunk-66w9j7p6.js +322 -0
- package/dist/cli/chunk-66w9j7p6.js.map +14 -0
- package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
- package/dist/cli/chunk-7ppf1rva.js.map +10 -0
- package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
- package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
- package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
- package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
- package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
- package/dist/cli/chunk-cfyg47zg.js.map +10 -0
- package/dist/cli/chunk-cm89scdv.js +443 -0
- package/dist/cli/chunk-cm89scdv.js.map +11 -0
- package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
- package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
- package/dist/cli/chunk-e6atmn4h.js.map +36 -0
- package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
- package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
- package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
- package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
- package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
- package/dist/cli/chunk-ehweg4fr.js.map +218 -0
- package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
- package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
- package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
- package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
- package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
- package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
- package/dist/cli/chunk-gb420vvz.js.map +17 -0
- package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
- package/dist/cli/chunk-hebn0tpx.js.map +14 -0
- package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
- package/dist/cli/chunk-htrwgabr.js.map +15 -0
- package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
- package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
- package/dist/cli/chunk-j134vrp2.js +65 -0
- package/dist/cli/chunk-j134vrp2.js.map +10 -0
- package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
- package/dist/cli/chunk-kwrg63tm.js +31 -0
- package/dist/cli/chunk-kwrg63tm.js.map +10 -0
- package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
- package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
- package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
- package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
- package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
- package/dist/cli/chunk-ncj7w596.js.map +35 -0
- package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
- package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
- package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
- package/dist/cli/chunk-nrns10mj.js.map +10 -0
- package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
- package/dist/cli/chunk-t2m5j4pr.js +104 -0
- package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
- package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
- package/dist/cli/chunk-tphac74f.js +126 -0
- package/dist/cli/chunk-tphac74f.js.map +11 -0
- package/dist/cli/chunk-w1r4vase.js +18 -0
- package/dist/cli/chunk-w1r4vase.js.map +10 -0
- package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
- package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
- package/dist/cli/index.js +31 -20
- package/dist/cli/index.js.map +5 -5
- package/dist/types/ai/agent-surface.d.ts +8 -7
- package/dist/types/ai/ai-catalog.d.ts +7 -0
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/ai/ask.d.ts +271 -58
- package/dist/types/ai/component-markdown.d.ts +2 -0
- package/dist/types/ai/index.d.ts +2 -2
- package/dist/types/ai/openapi-components.d.ts +4 -1
- package/dist/types/ai/site-skill.d.ts +26 -0
- package/dist/types/ai/skills.d.ts +16 -0
- package/dist/types/astro/integration.d.ts +9 -0
- package/dist/types/astro/markdown-negotiation.d.ts +3 -0
- package/dist/types/astro/pages.d.ts +10 -0
- package/dist/types/captcha/hcaptcha.d.ts +27 -0
- package/dist/types/captcha/index.d.ts +28 -0
- package/dist/types/captcha/schema.d.ts +29 -0
- package/dist/types/captcha/turnstile.d.ts +28 -0
- package/dist/types/cli/init/scaffold.d.ts +1 -1
- package/dist/types/components/content/api-field.d.ts +62 -0
- package/dist/types/components/content/api-page.d.ts +100 -0
- package/dist/types/components/layout/nav-utils.d.ts +26 -0
- package/dist/types/components/openapi/code-samples.d.ts +29 -0
- package/dist/types/components/openapi/helpers.d.ts +158 -0
- package/dist/types/components/openapi/security.d.ts +95 -0
- package/dist/types/components/openapi/snippets.d.ts +60 -0
- package/dist/types/consent/ethyca.d.ts +48 -0
- package/dist/types/consent/index.d.ts +37 -0
- package/dist/types/consent/native.d.ts +28 -0
- package/dist/types/consent/osano.d.ts +42 -0
- package/dist/types/consent/schema.d.ts +110 -0
- package/dist/types/core/changelog-index.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +217 -23
- package/dist/types/core/data.d.ts +53 -4
- package/dist/types/core/footer.d.ts +8 -0
- package/dist/types/core/i18n-ui.d.ts +88 -0
- package/dist/types/core/includes.d.ts +8 -2
- package/dist/types/core/links.d.ts +8 -0
- package/dist/types/core/localizable.d.ts +7 -0
- package/dist/types/core/page-modes.d.ts +29 -0
- package/dist/types/core/probe.d.ts +2 -0
- package/dist/types/core/project-graph.d.ts +6 -0
- package/dist/types/core/redirect-patterns.d.ts +152 -0
- package/dist/types/core/related.d.ts +40 -0
- package/dist/types/core/schema.d.ts +670 -56
- package/dist/types/core/sources/cache.d.ts +3 -0
- package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
- package/dist/types/core/sources/contentful.d.ts +17 -1
- package/dist/types/core/sources/lexical.d.ts +5 -0
- package/dist/types/core/sources/normalize.d.ts +20 -1
- package/dist/types/core/sources/read.d.ts +5 -0
- package/dist/types/core/sources/remote.d.ts +37 -10
- package/dist/types/core/sources/sanity.d.ts +1 -1
- package/dist/types/core/types.d.ts +18 -1
- package/dist/types/core/variables.d.ts +42 -0
- package/dist/types/deploy/adapter-output.d.ts +2 -1
- package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
- package/dist/types/deploy/adapters/node.d.ts +58 -23
- package/dist/types/deploy/adapters/registry.d.ts +10 -0
- package/dist/types/deploy/adapters/types.d.ts +3 -2
- package/dist/types/deploy/node-headers.d.ts +13 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
- package/dist/types/deploy/platforms/netlify.d.ts +7 -0
- package/dist/types/deploy/platforms/node.d.ts +3 -1
- package/dist/types/deploy/platforms/types.d.ts +18 -7
- package/dist/types/deploy/platforms/vercel.d.ts +26 -1
- package/dist/types/deploy/redirects.d.ts +55 -11
- package/dist/types/deploy/sitemap.d.ts +2 -1
- package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
- package/dist/types/markdown/code-title.d.ts +17 -0
- package/dist/types/markdown/features.d.ts +3 -2
- package/dist/types/narration/build.d.ts +70 -0
- package/dist/types/narration/provider.d.ts +26 -0
- package/dist/types/narration/script.d.ts +154 -0
- package/dist/types/openapi/checks.d.ts +27 -0
- package/dist/types/openapi/graphql.d.ts +6 -0
- package/dist/types/openapi/model.d.ts +27 -10
- package/dist/types/openapi/overlay.d.ts +54 -0
- package/dist/types/openapi/parse.d.ts +16 -0
- package/dist/types/openapi/references.d.ts +30 -4
- package/dist/types/ratelimit/cloudflare.d.ts +39 -0
- package/dist/types/ratelimit/index.d.ts +27 -0
- package/dist/types/ratelimit/memory.d.ts +36 -0
- package/dist/types/ratelimit/schema.d.ts +119 -0
- package/dist/types/ratelimit/upstash.d.ts +46 -0
- package/dist/types/ratelimit/wrangler.d.ts +23 -0
- package/dist/types/reference/asyncapi.d.ts +10 -10
- package/dist/types/reference/graphql.d.ts +89 -11
- package/dist/types/reference/index.d.ts +1 -1
- package/dist/types/reference/openapi.d.ts +33 -12
- package/dist/types/reference/options.d.ts +82 -6
- package/dist/types/reference/scalar.d.ts +13 -0
- package/dist/types/reference/schema.d.ts +144 -36
- package/dist/types/search/adapters/registry.d.ts +3 -0
- package/dist/types/search/adapters/typesense.d.ts +14 -4
- package/dist/types/search/build.d.ts +3 -1
- package/dist/types/search/documents.d.ts +12 -1
- package/dist/types/search/orama-index.d.ts +13 -9
- package/dist/types/search/sync/algolia.d.ts +5 -2
- package/dist/types/search/sync/index.d.ts +3 -1
- package/dist/types/search/sync/typesense.d.ts +11 -7
- package/dist/types/seo/metatags.d.ts +20 -0
- package/dist/types/sources/contentful.d.ts +16 -0
- package/dist/types/sources/github-releases.d.ts +8 -1
- package/dist/types/sources/payload.d.ts +2 -0
- package/dist/types/sources/registry.d.ts +54 -0
- package/dist/types/sources/shared.d.ts +3 -0
- package/dist/types/sources/strapi.d.ts +2 -0
- package/docs/01-quickstart.mdx +4 -2
- package/docs/02-deployment.mdx +71 -17
- package/docs/03-upgrading.mdx +4 -4
- package/docs/04-migrating.mdx +1 -1
- package/docs/advanced/changelog.mdx +13 -2
- package/docs/advanced/skills.mdx +15 -1
- package/docs/cli/audit.mdx +51 -8
- package/docs/cli/doctor.mdx +5 -1
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/index.mdx +1 -0
- package/docs/cli/translate.mdx +1 -0
- package/docs/cli/validate.mdx +4 -3
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/assistant.mdx +194 -38
- package/docs/configuration/consent.mdx +100 -0
- package/docs/configuration/customization.mdx +8 -6
- package/docs/configuration/export.mdx +5 -3
- package/docs/configuration/index.mdx +64 -5
- package/docs/configuration/meta.ts +3 -0
- package/docs/configuration/narration.mdx +119 -0
- package/docs/configuration/rate-limiting.mdx +90 -0
- package/docs/configuration/search.mdx +63 -12
- package/docs/configuration/theming.mdx +3 -1
- package/docs/content/components.mdx +108 -2
- package/docs/content/frontmatter.mdx +80 -2
- package/docs/content/i18n.mdx +39 -4
- package/docs/content/includes.mdx +16 -0
- package/docs/content/meta.mdx +1 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +32 -10
- package/docs/content/sources/contentful.mdx +35 -0
- package/docs/content/sources/custom.mdx +43 -0
- package/docs/content/sources/github-releases.mdx +39 -0
- package/docs/content/sources/index.mdx +91 -0
- package/docs/content/sources/meta.ts +16 -0
- package/docs/content/sources/notion.mdx +32 -0
- package/docs/content/sources/obsidian.mdx +37 -0
- package/docs/content/sources/payload.mdx +31 -0
- package/docs/content/sources/remote-mdx.mdx +44 -0
- package/docs/content/sources/sanity.mdx +29 -0
- package/docs/content/sources/strapi.mdx +32 -0
- package/docs/content/syntax.mdx +69 -4
- package/docs/content/variables.mdx +41 -0
- package/docs/discoverability/agent-discovery.mdx +29 -2
- package/docs/discoverability/index.mdx +2 -2
- package/docs/discoverability/json-api.mdx +2 -0
- package/docs/discoverability/llms-txt.mdx +2 -2
- package/docs/discoverability/markdown.mdx +3 -1
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/metadata.mdx +17 -0
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/references/api-pages.mdx +74 -0
- package/docs/references/asyncapi.mdx +4 -2
- package/docs/references/graphql.mdx +14 -2
- package/docs/references/meta.ts +1 -1
- package/docs/references/openapi.mdx +148 -7
- package/docs/references/scalar.mdx +3 -1
- package/package.json +43 -8
- package/skills/blume/SKILL.md +3 -0
- package/skills/blume-migrate/SKILL.md +6 -6
- package/skills/blume-migrate/references/docusaurus.md +7 -7
- package/skills/blume-migrate/references/fumadocs.md +17 -13
- package/skills/blume-migrate/references/mintlify.md +37 -19
- package/skills/blume-migrate/references/nextra.md +10 -7
- package/skills/blume-migrate/references/starlight.md +11 -8
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
- package/skills/blume-write-skill/SKILL.md +82 -0
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/agent-surface.ts +9 -7
- package/src/ai/ai-catalog.ts +6 -3
- package/src/ai/ask-context.ts +103 -20
- package/src/ai/ask-data.ts +6 -0
- package/src/ai/ask-limits.ts +13 -0
- package/src/ai/ask-tools.ts +142 -0
- package/src/ai/ask.ts +354 -76
- package/src/ai/changelog-markdown.ts +11 -6
- package/src/ai/component-markdown.ts +76 -1
- package/src/ai/cors.ts +14 -3
- package/src/ai/index.ts +12 -0
- package/src/ai/llms.ts +6 -2
- package/src/ai/markdown.ts +2 -1
- package/src/ai/mcp/data.ts +6 -0
- package/src/ai/mcp/query.ts +14 -6
- package/src/ai/openapi-components.ts +36 -6
- package/src/ai/relative-links.ts +4 -38
- package/src/ai/site-skill.ts +274 -0
- package/src/ai/skills.ts +35 -2
- package/src/ai/write-skill.ts +97 -0
- package/src/astro/generate.ts +210 -66
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +42 -4
- package/src/astro/markdown-negotiation.ts +3 -0
- package/src/astro/module-types.ts +9 -1
- package/src/astro/pages.ts +23 -0
- package/src/astro/templates.ts +406 -108
- package/src/astro/variables.ts +31 -0
- package/src/audit/catalog.ts +38 -3
- package/src/audit/checks/contrast.ts +183 -0
- package/src/audit/checks/i18n.ts +2 -3
- package/src/audit/checks/indexability.ts +2 -3
- package/src/audit/checks/links.ts +19 -13
- package/src/audit/checks/network.ts +110 -6
- package/src/audit/checks/redirects.ts +6 -4
- package/src/audit/checks/robots.ts +58 -2
- package/src/audit/checks/sitemap.ts +2 -3
- package/src/audit/redirects.ts +46 -15
- package/src/audit/run.ts +5 -0
- package/src/audit/types.ts +4 -1
- package/src/audit/url.ts +14 -5
- package/src/blume-modules.d.ts +12 -1
- package/src/captcha/client.ts +152 -0
- package/src/captcha/hcaptcha.ts +36 -0
- package/src/captcha/index.ts +28 -0
- package/src/captcha/schema.ts +32 -0
- package/src/captcha/turnstile.ts +38 -0
- package/src/captcha/verify.ts +71 -0
- package/src/cli/args.ts +51 -0
- package/src/cli/build-failure.ts +23 -0
- package/src/cli/command-meta.ts +5 -0
- package/src/cli/commands/audit.ts +10 -1
- package/src/cli/commands/dev.ts +18 -2
- package/src/cli/commands/doctor.ts +67 -28
- package/src/cli/commands/eval.ts +36 -4
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/skill.ts +96 -0
- package/src/cli/commands/validate.ts +10 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/init/scaffold.ts +3 -3
- package/src/cli/prepare.ts +6 -3
- package/src/cli/required-secrets.ts +72 -30
- package/src/components/BlumePage.astro +2 -0
- package/src/components/content/ApiEndpoint.astro +18 -0
- package/src/components/content/ApiField.astro +79 -0
- package/src/components/content/ApiPlayground.astro +66 -0
- package/src/components/content/ApiRail.astro +19 -0
- package/src/components/content/Color.astro +1 -0
- package/src/components/content/FileTree.astro +1 -0
- package/src/components/content/GithubInfo.astro +1 -0
- package/src/components/content/Panel.astro +1 -0
- package/src/components/content/ParamField.astro +25 -0
- package/src/components/content/RequestExample.astro +17 -0
- package/src/components/content/ResponseExample.astro +17 -0
- package/src/components/content/ResponseField.astro +32 -0
- package/src/components/content/Step.astro +1 -0
- package/src/components/content/Tile.astro +1 -0
- package/src/components/content/Tooltip.astro +1 -0
- package/src/components/content/Tree.astro +1 -0
- package/src/components/content/TypeTable.astro +4 -1
- package/src/components/content/View.astro +21 -0
- package/src/components/content/ViewSwitcher.astro +168 -0
- package/src/components/content/api-field.ts +103 -0
- package/src/components/content/api-page.ts +248 -0
- package/src/components/content/mermaid-element.ts +15 -1
- package/src/components/content/view-switcher.ts +71 -0
- package/src/components/index.ts +2 -0
- package/src/components/islands/Assistant.astro +10 -1
- package/src/components/islands/assistant.tsx +104 -14
- package/src/components/islands/code-question.ts +62 -0
- package/src/components/islands/hooks.ts +136 -21
- package/src/components/islands/support-link.ts +68 -0
- package/src/components/layout/Analytics.astro +11 -2
- package/src/components/layout/Banner.astro +14 -10
- package/src/components/layout/Breadcrumbs.astro +6 -2
- package/src/components/layout/ConsentBanner.astro +62 -0
- package/src/components/layout/ConsentHead.astro +38 -0
- package/src/components/layout/Directory.astro +94 -0
- package/src/components/layout/DrawerSwitchers.astro +51 -0
- package/src/components/layout/Empty.astro +2 -2
- package/src/components/layout/Header.astro +122 -80
- package/src/components/layout/LanguageSwitcher.astro +48 -15
- package/src/components/layout/NarrationPlayer.astro +1085 -0
- package/src/components/layout/NavSelector.astro +33 -7
- package/src/components/layout/PageActions.astro +139 -24
- package/src/components/layout/PageFeedback.astro +54 -49
- package/src/components/layout/PageLayout.astro +71 -3
- package/src/components/layout/ReferenceLayout.astro +17 -6
- package/src/components/layout/RelatedPages.astro +42 -0
- package/src/components/layout/RootLayout.astro +318 -47
- package/src/components/layout/Search.astro +47 -19
- package/src/components/layout/SiteFooter.astro +124 -0
- package/src/components/layout/SiteMetatags.astro +16 -0
- package/src/components/layout/TableOfContents.astro +24 -17
- package/src/components/layout/consent/native.ts +79 -0
- package/src/components/layout/consent/types.ts +37 -0
- package/src/components/layout/footer-links.ts +53 -0
- package/src/components/layout/footer-socials.ts +87 -0
- package/src/components/layout/head-scripts.ts +6 -1
- package/src/components/layout/locale-redirect.ts +107 -0
- package/src/components/layout/nav-utils.ts +83 -0
- package/src/components/layout/page-feedback.ts +96 -0
- package/src/components/layout/search/algolia.ts +10 -1
- package/src/components/layout/search/analytics.ts +126 -0
- package/src/components/layout/search/endpoint.ts +48 -5
- package/src/components/layout/search/flexsearch.ts +17 -5
- package/src/components/layout/search/orama-cloud.ts +26 -14
- package/src/components/layout/search/pagefind.ts +114 -5
- package/src/components/layout/search/types.ts +32 -0
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/AsyncApiOperation.astro +14 -7
- package/src/components/openapi/Authorization.astro +10 -0
- package/src/components/openapi/GraphqlOperation.astro +5 -0
- package/src/components/openapi/Operation.astro +184 -13
- package/src/components/openapi/ParametersTable.astro +8 -4
- package/src/components/openapi/Playground.astro +7 -1
- package/src/components/openapi/RequestBody.astro +24 -5
- package/src/components/openapi/RequestPanel.astro +59 -7
- package/src/components/openapi/Responses.astro +7 -4
- package/src/components/openapi/SchemaTable.astro +15 -3
- package/src/components/openapi/async-snippets.ts +65 -9
- package/src/components/openapi/async.ts +123 -29
- package/src/components/openapi/code-samples.ts +97 -0
- package/src/components/openapi/graphql-helpers.ts +35 -12
- package/src/components/openapi/helpers.ts +36 -2
- package/src/components/openapi/message-model.ts +54 -11
- package/src/components/openapi/message.ts +14 -4
- package/src/components/openapi/operation-model.ts +16 -7
- package/src/components/openapi/playground-client.ts +24 -5
- package/src/components/openapi/playground-schema.ts +1 -1
- package/src/components/openapi/schema-tree.ts +44 -14
- package/src/components/openapi/security.ts +16 -3
- package/src/components/openapi/snippets.ts +873 -10
- package/src/components/openapi/validate-json.ts +4 -3
- package/src/components/props.ts +6 -0
- package/src/consent/client.ts +95 -0
- package/src/consent/clients.ts +19 -0
- package/src/consent/ethyca.ts +71 -0
- package/src/consent/head.ts +24 -0
- package/src/consent/index.ts +37 -0
- package/src/consent/init.ts +29 -0
- package/src/consent/native.ts +36 -0
- package/src/consent/osano.ts +59 -0
- package/src/consent/schema.ts +30 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/changelog-index.ts +31 -1
- package/src/core/client-address.ts +19 -0
- package/src/core/component-diagnostics.ts +53 -0
- package/src/core/component-overrides.ts +32 -0
- package/src/core/config-input.ts +225 -23
- package/src/core/data.ts +46 -4
- package/src/core/diagnostics.ts +8 -2
- package/src/core/footer.ts +26 -0
- package/src/core/graph.ts +39 -36
- package/src/core/i18n-ui.ts +84 -2
- package/src/core/includes.ts +21 -4
- package/src/core/links.ts +63 -19
- package/src/core/localizable.ts +26 -0
- package/src/core/navigation.ts +43 -12
- package/src/core/page-modes.ts +56 -0
- package/src/core/probe.ts +26 -6
- package/src/core/project-graph.ts +163 -16
- package/src/core/redirect-patterns.ts +520 -0
- package/src/core/related.ts +131 -0
- package/src/core/schema.ts +339 -66
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/contentful-rich-text.ts +29 -6
- package/src/core/sources/contentful.ts +135 -25
- package/src/core/sources/filesystem.ts +26 -4
- package/src/core/sources/github-releases.ts +12 -1
- package/src/core/sources/lexical.ts +15 -10
- package/src/core/sources/mdx-remote.ts +62 -10
- package/src/core/sources/normalize.ts +228 -41
- package/src/core/sources/notion.ts +7 -1
- package/src/core/sources/obsidian.ts +16 -3
- package/src/core/sources/payload.ts +38 -15
- package/src/core/sources/read.ts +8 -3
- package/src/core/sources/remote.ts +72 -15
- package/src/core/sources/resolve.ts +11 -1
- package/src/core/sources/sanity.ts +43 -25
- package/src/core/sources/strapi.ts +2 -2
- package/src/core/types.ts +19 -1
- package/src/core/ui-packs/ar.ts +44 -0
- package/src/core/ui-packs/bg.ts +46 -0
- package/src/core/ui-packs/bn.ts +44 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +45 -0
- package/src/core/ui-packs/da.ts +46 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +46 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +46 -0
- package/src/core/ui-packs/fi.ts +46 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +44 -0
- package/src/core/ui-packs/hi.ts +45 -0
- package/src/core/ui-packs/hr.ts +46 -0
- package/src/core/ui-packs/hu.ts +45 -0
- package/src/core/ui-packs/id.ts +46 -0
- package/src/core/ui-packs/it.ts +46 -0
- package/src/core/ui-packs/ja.ts +46 -0
- package/src/core/ui-packs/ko.ts +45 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +45 -0
- package/src/core/ui-packs/pl.ts +46 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +46 -0
- package/src/core/ui-packs/ro.ts +46 -0
- package/src/core/ui-packs/ru.ts +46 -0
- package/src/core/ui-packs/sk.ts +45 -0
- package/src/core/ui-packs/sr.ts +46 -0
- package/src/core/ui-packs/sv.ts +45 -0
- package/src/core/ui-packs/th.ts +44 -0
- package/src/core/ui-packs/tr.ts +46 -0
- package/src/core/ui-packs/uk.ts +46 -0
- package/src/core/ui-packs/vi.ts +46 -0
- package/src/core/ui-packs/zh-tw.ts +44 -0
- package/src/core/ui-packs/zh.ts +44 -0
- package/src/core/variables.ts +103 -0
- package/src/deploy/adapter-output.ts +7 -2
- package/src/deploy/adapters/cloudflare.ts +7 -5
- package/src/deploy/adapters/node.ts +47 -9
- package/src/deploy/adapters/types.ts +7 -3
- package/src/deploy/artifacts.ts +79 -9
- package/src/deploy/cloudflare-negotiation.ts +35 -5
- package/src/deploy/headers.ts +2 -1
- package/src/deploy/node-headers.ts +63 -9
- package/src/deploy/platforms/cloudflare.ts +45 -9
- package/src/deploy/platforms/netlify.ts +35 -2
- package/src/deploy/platforms/node.ts +12 -2
- package/src/deploy/platforms/types.ts +21 -7
- package/src/deploy/platforms/vercel.ts +149 -17
- package/src/deploy/redirects.ts +151 -24
- package/src/deploy/rss.ts +3 -1
- package/src/deploy/sitemap.ts +39 -5
- package/src/deploy/vercel-negotiation.ts +326 -102
- package/src/eval/agents.ts +18 -1
- package/src/eval/prompts.ts +6 -3
- package/src/eval/report.ts +34 -13
- package/src/eval/run.ts +5 -0
- package/src/markdown/api-rail.ts +259 -0
- package/src/markdown/code-title.ts +35 -9
- package/src/markdown/features.ts +3 -2
- package/src/markdown/fence-meta.ts +2 -0
- package/src/markdown/include.ts +11 -1
- package/src/markdown/index.ts +28 -5
- package/src/markdown/math.ts +4 -4
- package/src/markdown/relative-links.ts +3 -2
- package/src/markdown/variables.ts +56 -0
- package/src/markdown/views.ts +82 -0
- package/src/narration/build.ts +412 -0
- package/src/narration/player.ts +249 -0
- package/src/narration/provider.ts +29 -0
- package/src/narration/script.ts +592 -0
- package/src/og/card.ts +7 -6
- package/src/openapi/checks.ts +273 -0
- package/src/openapi/graphql-build.ts +21 -0
- package/src/openapi/graphql.ts +11 -0
- package/src/openapi/model.ts +72 -27
- package/src/openapi/overlay.ts +256 -0
- package/src/openapi/parse.ts +182 -4
- package/src/openapi/references.ts +64 -8
- package/src/openapi/render-mdx.ts +5 -1
- package/src/openapi/scalar.ts +40 -6
- package/src/openapi/source.ts +40 -15
- package/src/ratelimit/cloudflare.ts +54 -0
- package/src/ratelimit/index.ts +30 -0
- package/src/ratelimit/memory.ts +45 -0
- package/src/ratelimit/runtime.ts +247 -0
- package/src/ratelimit/schema.ts +28 -0
- package/src/ratelimit/upstash.ts +60 -0
- package/src/ratelimit/wrangler.ts +70 -0
- package/src/reference/asyncapi.ts +2 -2
- package/src/reference/graphql.ts +20 -3
- package/src/reference/index.ts +1 -0
- package/src/reference/openapi.ts +21 -7
- package/src/reference/options.ts +80 -8
- package/src/reference/scalar.ts +33 -8
- package/src/registry/eject.ts +23 -5
- package/src/registry/registry.ts +7 -0
- package/src/search/adapters/locale-scope.ts +30 -0
- package/src/search/adapters/typesense.ts +14 -4
- package/src/search/build.ts +21 -3
- package/src/search/documents.ts +26 -2
- package/src/search/orama-index.ts +191 -34
- package/src/search/pagefind-ranking.ts +39 -0
- package/src/search/source-pages.ts +29 -0
- package/src/search/sync/algolia.ts +111 -10
- package/src/search/sync/index.ts +52 -13
- package/src/search/sync/orama-cloud.ts +3 -0
- package/src/search/sync/typesense.ts +76 -26
- package/src/seo/metatags.ts +67 -0
- package/src/sources/contentful.ts +12 -0
- package/src/sources/github-releases.ts +7 -1
- package/src/sources/shared.ts +3 -0
- package/src/theme/chrome-icons.ts +1 -0
- package/src/theme/color.ts +500 -0
- package/src/theme/entry.ts +85 -6
- package/src/theme/palette.ts +121 -45
- package/src/translate/prompts.ts +1 -0
- package/src/translate/validate.ts +31 -3
- package/src/upgrade/upgrade.ts +4 -171
- package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
- package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
- package/dist/cli/chunk-35d4wj9f.js.map +0 -15
- package/dist/cli/chunk-364znk6q.js +0 -75
- package/dist/cli/chunk-364znk6q.js.map +0 -11
- package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
- package/dist/cli/chunk-8ktnccpt.js.map +0 -10
- package/dist/cli/chunk-9t7a85s3.js.map +0 -10
- package/dist/cli/chunk-b07cmahc.js +0 -145
- package/dist/cli/chunk-b07cmahc.js.map +0 -11
- package/dist/cli/chunk-c8chx29p.js.map +0 -36
- package/dist/cli/chunk-e04dxsz1.js +0 -39
- package/dist/cli/chunk-e04dxsz1.js.map +0 -10
- package/dist/cli/chunk-j85vccga.js.map +0 -36
- package/dist/cli/chunk-p3v96n38.js.map +0 -10
- package/dist/cli/chunk-pehfxfta.js.map +0 -10
- package/dist/cli/chunk-pv29h0wf.js.map +0 -190
- package/dist/cli/chunk-q58y5e6a.js.map +0 -10
- package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
- package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
- package/dist/cli/chunk-yfyb25rh.js.map +0 -14
- package/docs/content/sources.mdx +0 -334
- /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
- /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
- /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
- /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
- /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
- /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
- /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
- /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
package/src/openapi/scalar.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { scalarReferenceTemplate } from "../astro/templates.ts";
|
|
|
6
6
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
7
7
|
import { trimChar } from "../core/trim.ts";
|
|
8
8
|
import { resolveAccent, resolveRadius } from "../theme/palette.ts";
|
|
9
|
+
import { readOverlaidSpec } from "./parse.ts";
|
|
9
10
|
import { resolveReferences } from "./references.ts";
|
|
10
11
|
import type { ReferenceSource } from "./references.ts";
|
|
11
12
|
|
|
@@ -53,14 +54,44 @@ const themeConfiguration = (config: ResolvedConfig, override?: string) => {
|
|
|
53
54
|
};
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
/** A source's Scalar spec config, or none when its overlays failed. */
|
|
58
|
+
interface SpecConfiguration {
|
|
59
|
+
config?: { content: string } | { url: string };
|
|
60
|
+
warning?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The spec with its overlays applied, inlined: Scalar can't apply overlays
|
|
65
|
+
* itself. A failed overlay skips the page rather than embed the spec without
|
|
66
|
+
* it, since an overlay is often what hides the internal operations.
|
|
67
|
+
*/
|
|
68
|
+
const overlaidConfiguration = async (
|
|
69
|
+
ref: ReferenceSource,
|
|
70
|
+
root: string
|
|
71
|
+
): Promise<SpecConfiguration> => {
|
|
72
|
+
try {
|
|
73
|
+
const { text, warnings } = await readOverlaidSpec(ref.spec, root, {
|
|
74
|
+
overlays: ref.overlays,
|
|
75
|
+
});
|
|
76
|
+
return { config: { content: text }, warning: warnings[0] };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// SAFETY: reading, parsing, and overlaying throw Error instances.
|
|
79
|
+
const { message } = error as Error;
|
|
80
|
+
return {
|
|
81
|
+
warning: `API reference ${ref.route} was skipped: its overlays couldn't be applied to "${ref.spec}" (${message}).`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
56
86
|
/** Build the Scalar spec config for a source: inline `content` or remote `url`. */
|
|
57
87
|
const specConfiguration = async (
|
|
58
|
-
|
|
88
|
+
ref: ReferenceSource,
|
|
59
89
|
root: string
|
|
60
|
-
): Promise<{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
): Promise<SpecConfiguration> => {
|
|
91
|
+
const { spec } = ref;
|
|
92
|
+
if (ref.overlays?.length) {
|
|
93
|
+
return await overlaidConfiguration(ref, root);
|
|
94
|
+
}
|
|
64
95
|
if (URL_SPEC.test(spec)) {
|
|
65
96
|
return { config: { url: spec } };
|
|
66
97
|
}
|
|
@@ -133,7 +164,7 @@ export const buildReferenceFiles = async (options: {
|
|
|
133
164
|
const built = await Promise.all(
|
|
134
165
|
accepted.map(async (ref) => ({
|
|
135
166
|
ref,
|
|
136
|
-
spec: await specConfiguration(ref
|
|
167
|
+
spec: await specConfiguration(ref, root),
|
|
137
168
|
}))
|
|
138
169
|
);
|
|
139
170
|
|
|
@@ -142,6 +173,9 @@ export const buildReferenceFiles = async (options: {
|
|
|
142
173
|
if (spec.warning) {
|
|
143
174
|
warnings.push(spec.warning);
|
|
144
175
|
}
|
|
176
|
+
if (!spec.config) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
145
179
|
const pagePath = referencePagePath(ref.route);
|
|
146
180
|
// `theme` is the one option Blume maps (a named theme replaces the
|
|
147
181
|
// accent/radius layering); every other `scalar()` option is forwarded
|
package/src/openapi/source.ts
CHANGED
|
@@ -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
|
|
177
|
-
*
|
|
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.
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
-
|
|
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,15 +261,16 @@ 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
|
-
options
|
|
267
|
+
{ ...options, overlays: reference.overlays }
|
|
254
268
|
);
|
|
255
269
|
const extracted = extractOperations(document, reference.route);
|
|
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
|
|
322
|
-
// server); assigned separately so the
|
|
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
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { AdapterDescriptor } from "../core/adapter.ts";
|
|
4
|
+
import { adapterDescriptorSchema } from "../core/adapter.ts";
|
|
5
|
+
|
|
6
|
+
/** The Worker binding Blume declares for the rate limiter. */
|
|
7
|
+
export const RATE_LIMIT_BINDING = "BLUME_RATE_LIMIT";
|
|
8
|
+
|
|
9
|
+
/** Options for {@link cloudflare}. */
|
|
10
|
+
export interface CloudflareRateLimitOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The rate limiting namespace, a positive integer unique in your
|
|
13
|
+
* Cloudflare account. Defaults to one derived from the site.
|
|
14
|
+
*/
|
|
15
|
+
namespaceId?: string;
|
|
16
|
+
/** Requests each reader may make per window, per route. Defaults to 10. */
|
|
17
|
+
requests?: number;
|
|
18
|
+
/** The window, in seconds: Cloudflare allows 10 or 60. Defaults to 60. */
|
|
19
|
+
window?: 10 | 60;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const cloudflareRateLimitOptionsSchema = z.strictObject({
|
|
23
|
+
namespaceId: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^[1-9]\d*$/u, "namespaceId is a positive integer, as a string.")
|
|
26
|
+
.optional(),
|
|
27
|
+
requests: z.number().int().positive().optional(),
|
|
28
|
+
window: z.union([z.literal(10), z.literal(60)]).optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type CloudflareRateLimitAdapter = AdapterDescriptor<
|
|
32
|
+
"cloudflare",
|
|
33
|
+
CloudflareRateLimitOptions
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
export const cloudflareRateLimitAdapterSchema = adapterDescriptorSchema(
|
|
37
|
+
"cloudflare",
|
|
38
|
+
cloudflareRateLimitOptionsSchema
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Counts with Cloudflare's Workers rate limiting, for a site deployed with
|
|
43
|
+
* `deployment: cloudflare()`: Blume declares the binding in the Worker's
|
|
44
|
+
* config, so there is nothing to set up. Cloudflare counts per location and
|
|
45
|
+
* allows a window of 10 or 60 seconds.
|
|
46
|
+
*/
|
|
47
|
+
export const cloudflare = (
|
|
48
|
+
options: CloudflareRateLimitOptions = {}
|
|
49
|
+
): CloudflareRateLimitAdapter => ({
|
|
50
|
+
kind: "cloudflare",
|
|
51
|
+
options,
|
|
52
|
+
requiredSecrets: [],
|
|
53
|
+
runtimeDeps: [],
|
|
54
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiters for `blume.config.ts`. The server routes a reader can call —
|
|
3
|
+
* the assistant, the API playground proxy, and server-side search — limit
|
|
4
|
+
* each reader (by IP address) to a number of requests per window, answering
|
|
5
|
+
* `429 Too Many Requests` past it:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { defineConfig } from "blume";
|
|
9
|
+
* import { upstash } from "blume/ratelimit";
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* rateLimit: upstash({ requests: 30, window: 600 }),
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `memory()` is on by default; `rateLimit: false` turns limiting off. Each
|
|
17
|
+
* factory returns a plain descriptor (see `core/adapter.ts`), and the
|
|
18
|
+
* generated routes count with `runtime.ts`.
|
|
19
|
+
*/
|
|
20
|
+
export type { AdapterDescriptor, JsonValue } from "../core/adapter.ts";
|
|
21
|
+
export { cloudflare } from "./cloudflare.ts";
|
|
22
|
+
export type {
|
|
23
|
+
CloudflareRateLimitAdapter,
|
|
24
|
+
CloudflareRateLimitOptions,
|
|
25
|
+
} from "./cloudflare.ts";
|
|
26
|
+
export { memory } from "./memory.ts";
|
|
27
|
+
export type { MemoryAdapter, RateLimitOptions } from "./memory.ts";
|
|
28
|
+
export type { RateLimitAdapter } from "./schema.ts";
|
|
29
|
+
export { upstash } from "./upstash.ts";
|
|
30
|
+
export type { UpstashAdapter, UpstashOptions } from "./upstash.ts";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { AdapterDescriptor } from "../core/adapter.ts";
|
|
4
|
+
import { adapterDescriptorSchema } from "../core/adapter.ts";
|
|
5
|
+
|
|
6
|
+
/** How many requests a reader gets per window, unless set. */
|
|
7
|
+
export const DEFAULT_REQUESTS = 30;
|
|
8
|
+
|
|
9
|
+
/** The window those requests are counted over, in seconds, unless set. */
|
|
10
|
+
export const DEFAULT_WINDOW = 600;
|
|
11
|
+
|
|
12
|
+
/** The limit every rate limiter takes: a count of requests per window. */
|
|
13
|
+
export interface RateLimitOptions {
|
|
14
|
+
/** Requests each reader may make per window, per route. */
|
|
15
|
+
requests?: number;
|
|
16
|
+
/** The window, in seconds. */
|
|
17
|
+
window?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const rateLimitOptionsSchema = z.strictObject({
|
|
21
|
+
requests: z.number().int().positive().optional(),
|
|
22
|
+
window: z.number().int().positive().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type MemoryAdapter = AdapterDescriptor<"memory", RateLimitOptions>;
|
|
26
|
+
|
|
27
|
+
export const memoryAdapterSchema = adapterDescriptorSchema(
|
|
28
|
+
"memory",
|
|
29
|
+
rateLimitOptionsSchema
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Counts each reader's requests in the server's own memory, the default. On
|
|
34
|
+
* a server that runs as one process (`node()`) the count is exact; on a
|
|
35
|
+
* platform that runs many short-lived instances (Vercel, Netlify,
|
|
36
|
+
* Cloudflare) each instance keeps its own, so it stops a burst from one
|
|
37
|
+
* reader rather than enforcing the limit exactly. For that, share the count
|
|
38
|
+
* with `upstash()` or `cloudflare()`.
|
|
39
|
+
*/
|
|
40
|
+
export const memory = (options: RateLimitOptions = {}): MemoryAdapter => ({
|
|
41
|
+
kind: "memory",
|
|
42
|
+
options,
|
|
43
|
+
requiredSecrets: [],
|
|
44
|
+
runtimeDeps: [],
|
|
45
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rate limit the generated server routes check before any work: the
|
|
3
|
+
* assistant (`/api/ask`), the API playground proxy (`/_api-proxy`), and
|
|
4
|
+
* server-side search. Each route builds one limiter at module scope from the
|
|
5
|
+
* configured adapter (`rateLimit` in `blume.config.ts`) and asks it about
|
|
6
|
+
* every request, keyed by the reader's IP address and the route, so each
|
|
7
|
+
* route has its own budget.
|
|
8
|
+
*
|
|
9
|
+
* A limiter never stands between a reader and the docs by accident: a
|
|
10
|
+
* request whose address the host can't tell is let through, and so is one
|
|
11
|
+
* the shared store fails to count (logged, not blocked). A shared store
|
|
12
|
+
* without its secrets or binding falls back to counting in memory.
|
|
13
|
+
*/
|
|
14
|
+
import { clientAddressOf } from "../core/client-address.ts";
|
|
15
|
+
import type { ClientContext } from "../core/client-address.ts";
|
|
16
|
+
import { RATE_LIMIT_BINDING } from "./cloudflare.ts";
|
|
17
|
+
import { DEFAULT_REQUESTS, DEFAULT_WINDOW } from "./memory.ts";
|
|
18
|
+
import type { RateLimitAdapter } from "./schema.ts";
|
|
19
|
+
import { upstashSecrets } from "./upstash.ts";
|
|
20
|
+
|
|
21
|
+
/** What a limiter says about one request. */
|
|
22
|
+
export interface RateLimitResult {
|
|
23
|
+
allowed: boolean;
|
|
24
|
+
/** Seconds until the reader's window resets. */
|
|
25
|
+
retryAfter: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Count one request against `key`. */
|
|
29
|
+
export type Limiter = (key: string) => Promise<RateLimitResult>;
|
|
30
|
+
|
|
31
|
+
/** A Workers rate limiting binding (`ratelimits` in the Worker's config). */
|
|
32
|
+
export interface RateLimitBinding {
|
|
33
|
+
limit: (options: { key: string }) => Promise<{ success: boolean }>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What a generated route hands the limiter from its runtime. */
|
|
37
|
+
export interface LimiterRuntime {
|
|
38
|
+
/** The Worker's rate limiting binding, under `cloudflare()`. */
|
|
39
|
+
binding?: RateLimitBinding;
|
|
40
|
+
/** For tests; defaults to the global `fetch`. */
|
|
41
|
+
fetch?: typeof fetch;
|
|
42
|
+
/** For tests; defaults to `Date.now`. */
|
|
43
|
+
now?: () => number;
|
|
44
|
+
/** Reads a secret (`getSecret` from `astro:env/server`). */
|
|
45
|
+
secret?: (name: string) => string | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Past this many tracked readers, a memory limiter drops expired ones. */
|
|
49
|
+
const MEMORY_SWEEP_AT = 10_000;
|
|
50
|
+
|
|
51
|
+
/** One reader's count in the current window. */
|
|
52
|
+
interface MemoryEntry {
|
|
53
|
+
count: number;
|
|
54
|
+
resetAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Count in this server's memory, in fixed windows: each reader's first
|
|
59
|
+
* request opens a window, and the count resets when it ends.
|
|
60
|
+
*/
|
|
61
|
+
export const memoryLimiter = (
|
|
62
|
+
requests: number,
|
|
63
|
+
window: number,
|
|
64
|
+
now: () => number = Date.now
|
|
65
|
+
): Limiter => {
|
|
66
|
+
const entries = new Map<string, MemoryEntry>();
|
|
67
|
+
return (key) => {
|
|
68
|
+
const time = now();
|
|
69
|
+
if (entries.size >= MEMORY_SWEEP_AT) {
|
|
70
|
+
for (const [tracked, entry] of entries) {
|
|
71
|
+
if (entry.resetAt <= time) {
|
|
72
|
+
entries.delete(tracked);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let entry = entries.get(key);
|
|
77
|
+
if (!entry || entry.resetAt <= time) {
|
|
78
|
+
entry = { count: 0, resetAt: time + window * 1000 };
|
|
79
|
+
entries.set(key, entry);
|
|
80
|
+
}
|
|
81
|
+
entry.count += 1;
|
|
82
|
+
return Promise.resolve({
|
|
83
|
+
allowed: entry.count <= requests,
|
|
84
|
+
retryAfter: Math.ceil((entry.resetAt - time) / 1000),
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
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. */
|
|
106
|
+
interface UpstashReply {
|
|
107
|
+
error?: string;
|
|
108
|
+
result?: (number | null)[] | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
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.
|
|
115
|
+
*/
|
|
116
|
+
export const upstashLimiter = (
|
|
117
|
+
requests: number,
|
|
118
|
+
window: number,
|
|
119
|
+
endpoint: string,
|
|
120
|
+
token: string,
|
|
121
|
+
fetchImpl: typeof fetch = fetch
|
|
122
|
+
): Limiter => {
|
|
123
|
+
const url = endpoint.replace(/\/+$/u, "");
|
|
124
|
+
return async (key) => {
|
|
125
|
+
const response = await fetchImpl(url, {
|
|
126
|
+
body: JSON.stringify([
|
|
127
|
+
"EVAL",
|
|
128
|
+
UPSTASH_COUNT_SCRIPT,
|
|
129
|
+
"1",
|
|
130
|
+
key,
|
|
131
|
+
String(window),
|
|
132
|
+
]),
|
|
133
|
+
headers: {
|
|
134
|
+
authorization: `Bearer ${token}`,
|
|
135
|
+
"content-type": "application/json",
|
|
136
|
+
},
|
|
137
|
+
method: "POST",
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new Error(`Upstash answered ${response.status}.`);
|
|
141
|
+
}
|
|
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]);
|
|
148
|
+
if (Number.isNaN(count)) {
|
|
149
|
+
throw new TypeError("Upstash sent no count.");
|
|
150
|
+
}
|
|
151
|
+
return { allowed: count <= requests, retryAfter: ttl > 0 ? ttl : window };
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Count with a Workers rate limiting binding. */
|
|
156
|
+
export const bindingLimiter =
|
|
157
|
+
(binding: RateLimitBinding, window: number): Limiter =>
|
|
158
|
+
async (key) => {
|
|
159
|
+
const { success } = await binding.limit({ key });
|
|
160
|
+
return { allowed: success, retryAfter: window };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** The limit an adapter sets, with Cloudflare's own defaults for its binding. */
|
|
164
|
+
const limitOf = (adapter: RateLimitAdapter) =>
|
|
165
|
+
adapter.kind === "cloudflare"
|
|
166
|
+
? {
|
|
167
|
+
requests: adapter.options.requests ?? 10,
|
|
168
|
+
window: adapter.options.window ?? 60,
|
|
169
|
+
}
|
|
170
|
+
: {
|
|
171
|
+
requests: adapter.options.requests ?? DEFAULT_REQUESTS,
|
|
172
|
+
window: adapter.options.window ?? DEFAULT_WINDOW,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The limiter for the configured adapter, or `null` when rate limiting is
|
|
177
|
+
* off. A shared store missing its secrets or binding counts in memory and
|
|
178
|
+
* says so once.
|
|
179
|
+
*/
|
|
180
|
+
export const createLimiter = (
|
|
181
|
+
adapter: RateLimitAdapter | null,
|
|
182
|
+
runtime: LimiterRuntime = {}
|
|
183
|
+
): Limiter | null => {
|
|
184
|
+
if (!adapter) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const { requests, window } = limitOf(adapter);
|
|
188
|
+
if (adapter.kind === "cloudflare") {
|
|
189
|
+
if (runtime.binding) {
|
|
190
|
+
return bindingLimiter(runtime.binding, window);
|
|
191
|
+
}
|
|
192
|
+
console.warn(
|
|
193
|
+
`Rate limiting counts in memory: the Worker has no ${RATE_LIMIT_BINDING} binding.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (adapter.kind === "upstash") {
|
|
197
|
+
const secrets = upstashSecrets(adapter.options);
|
|
198
|
+
const [endpoint, token] = secrets.map((name) => runtime.secret?.(name));
|
|
199
|
+
if (endpoint && token) {
|
|
200
|
+
return upstashLimiter(requests, window, endpoint, token, runtime.fetch);
|
|
201
|
+
}
|
|
202
|
+
console.warn(
|
|
203
|
+
`Rate limiting counts in memory: set ${secrets.join(" and ")} to share the count through Upstash.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return memoryLimiter(requests, window, runtime.now);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** The slice of Astro's `APIContext` the check reads. */
|
|
210
|
+
export type RateLimitContext = ClientContext;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Count this request against the reader's budget for `scope` (the route).
|
|
214
|
+
* `null` to go ahead, or the `429 Too Many Requests` to return.
|
|
215
|
+
*/
|
|
216
|
+
export const rateLimited = async (
|
|
217
|
+
limiter: Limiter | null,
|
|
218
|
+
context: RateLimitContext,
|
|
219
|
+
scope: string
|
|
220
|
+
): Promise<Response | null> => {
|
|
221
|
+
const address = limiter ? clientAddressOf(context) : undefined;
|
|
222
|
+
if (!(limiter && address)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
// The host keeps two sites on one shared store apart.
|
|
226
|
+
const key = `blume:${new URL(context.request.url).host}:${scope}:${address}`;
|
|
227
|
+
let result: RateLimitResult;
|
|
228
|
+
try {
|
|
229
|
+
result = await limiter(key);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error("Rate limiting failed; letting the request through:", error);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (result.allowed) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return new Response(
|
|
238
|
+
`Too many requests: try again in ${result.retryAfter} seconds.`,
|
|
239
|
+
{
|
|
240
|
+
headers: {
|
|
241
|
+
"content-type": "text/plain; charset=utf-8",
|
|
242
|
+
"retry-after": String(result.retryAfter),
|
|
243
|
+
},
|
|
244
|
+
status: 429,
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { cloudflareRateLimitAdapterSchema } from "./cloudflare.ts";
|
|
4
|
+
import { memory, memoryAdapterSchema } from "./memory.ts";
|
|
5
|
+
import { upstashAdapterSchema } from "./upstash.ts";
|
|
6
|
+
|
|
7
|
+
const ADAPTER_HINT =
|
|
8
|
+
'rateLimit takes one adapter — e.g. `rateLimit: upstash()` or `rateLimit: memory({ requests: 60 })`, imported from "blume/ratelimit" — or `false` to turn it off.';
|
|
9
|
+
|
|
10
|
+
/** The configured rate limiter, as its factory returned it. */
|
|
11
|
+
export const rateLimitAdapterSchema = z.discriminatedUnion("kind", [
|
|
12
|
+
cloudflareRateLimitAdapterSchema,
|
|
13
|
+
memoryAdapterSchema,
|
|
14
|
+
upstashAdapterSchema,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export type RateLimitAdapter = z.output<typeof rateLimitAdapterSchema>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `blume.config.rateLimit`: how the server routes a reader can call (the
|
|
21
|
+
* assistant, the API playground proxy, server-side search) limit requests
|
|
22
|
+
* per reader. On by default with `memory()`; `false` turns it off (`null`
|
|
23
|
+
* once resolved).
|
|
24
|
+
*/
|
|
25
|
+
export const rateLimitConfigSchema = z
|
|
26
|
+
.union([z.literal(false), rateLimitAdapterSchema], { error: ADAPTER_HINT })
|
|
27
|
+
.optional()
|
|
28
|
+
.transform((adapter) => (adapter === false ? null : (adapter ?? memory())));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { AdapterDescriptor } from "../core/adapter.ts";
|
|
4
|
+
import { adapterDescriptorSchema } from "../core/adapter.ts";
|
|
5
|
+
import { rateLimitOptionsSchema } from "./memory.ts";
|
|
6
|
+
import type { RateLimitOptions } from "./memory.ts";
|
|
7
|
+
|
|
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
|
+
}
|
|
29
|
+
|
|
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>;
|
|
36
|
+
|
|
37
|
+
export const upstashAdapterSchema = adapterDescriptorSchema(
|
|
38
|
+
"upstash",
|
|
39
|
+
upstashOptionsSchema
|
|
40
|
+
);
|
|
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
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Keeps the count in Upstash Redis, which every instance on every host shares,
|
|
50
|
+
* so the limit holds exactly. It talks to Upstash's REST API, so there is
|
|
51
|
+
* nothing to install; the endpoint and token come from
|
|
52
|
+
* `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`, or the env vars
|
|
53
|
+
* `urlEnv` and `tokenEnv` name.
|
|
54
|
+
*/
|
|
55
|
+
export const upstash = (options: UpstashOptions = {}): UpstashAdapter => ({
|
|
56
|
+
kind: "upstash",
|
|
57
|
+
options,
|
|
58
|
+
requiredSecrets: upstashSecrets(options),
|
|
59
|
+
runtimeDeps: [],
|
|
60
|
+
});
|