@treeseed/core 0.1.2
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/README.md +145 -0
- package/dist/agents/index.js +5 -0
- package/dist/agents/registry-helper.js +14 -0
- package/dist/agents/registry.js +88 -0
- package/dist/components/DevWatchReload.astro +45 -0
- package/dist/components/SiteTitle.astro +51 -0
- package/dist/components/content/ContentStatusLegend.astro +18 -0
- package/dist/components/content/StatusBadge.astro +11 -0
- package/dist/components/docs/BookFontControls.astro +180 -0
- package/dist/components/docs/DesktopSidebarToggle.astro +88 -0
- package/dist/components/docs/DownloadBook.astro +34 -0
- package/dist/components/docs/Footer.astro +196 -0
- package/dist/components/docs/Header.astro +150 -0
- package/dist/components/docs/PageFrame.astro +260 -0
- package/dist/components/docs/PageSidebar.astro +63 -0
- package/dist/components/docs/PageTitle.astro +39 -0
- package/dist/components/docs/Sidebar.astro +41 -0
- package/dist/components/docs/ThemeSelect.astro +3 -0
- package/dist/components/forms/ContactForm.astro +234 -0
- package/dist/components/forms/FooterSubscribeForm.astro +189 -0
- package/dist/components/site/BookList.astro +27 -0
- package/dist/components/site/CTASection.astro +24 -0
- package/dist/components/site/ChronicleList.astro +33 -0
- package/dist/components/site/Hero.astro +18 -0
- package/dist/components/site/NotesList.astro +18 -0
- package/dist/components/site/PathCard.astro +16 -0
- package/dist/components/site/ProfileList.astro +30 -0
- package/dist/components/site/SectionIntro.astro +9 -0
- package/dist/components/site/StageBanner.astro +8 -0
- package/dist/components/site/TrustCallout.astro +9 -0
- package/dist/components/starlight.js +6 -0
- package/dist/config.js +8 -0
- package/dist/content-config.js +9 -0
- package/dist/content.js +230 -0
- package/dist/contracts.d.ts +130 -0
- package/dist/contracts.js +0 -0
- package/dist/deploy/config.d.ts +4 -0
- package/dist/deploy/config.js +154 -0
- package/dist/deploy/runtime.js +77 -0
- package/dist/env.yaml +322 -0
- package/dist/environment.d.ts +130 -0
- package/dist/environment.js +324 -0
- package/dist/index.js +81 -0
- package/dist/layouts/AuthoredEntryLayout.astro +87 -0
- package/dist/layouts/BookLayout.astro +35 -0
- package/dist/layouts/BridgeLayout.astro +11 -0
- package/dist/layouts/ContentLayout.astro +24 -0
- package/dist/layouts/MainLayout.astro +203 -0
- package/dist/layouts/NoteLayout.astro +26 -0
- package/dist/layouts/ProfileLayout.astro +81 -0
- package/dist/middleware/starlightRouteData.js +45 -0
- package/dist/pages/404.astro +29 -0
- package/dist/pages/[slug].astro +30 -0
- package/dist/pages/agents/[slug].astro +29 -0
- package/dist/pages/agents/index.astro +27 -0
- package/dist/pages/api/form/submit.js +14 -0
- package/dist/pages/books/[slug].astro +19 -0
- package/dist/pages/books/index.astro +28 -0
- package/dist/pages/contact.astro +27 -0
- package/dist/pages/feed.xml.js +34 -0
- package/dist/pages/index.astro +290 -0
- package/dist/pages/notes/[slug].astro +19 -0
- package/dist/pages/notes/index.astro +21 -0
- package/dist/pages/objectives/[slug].astro +31 -0
- package/dist/pages/objectives/index.astro +30 -0
- package/dist/pages/people/[slug].astro +29 -0
- package/dist/pages/people/index.astro +28 -0
- package/dist/pages/questions/[slug].astro +31 -0
- package/dist/pages/questions/index.astro +30 -0
- package/dist/plugin-default.js +4 -0
- package/dist/plugins/builtin/default-plugin.d.ts +21 -0
- package/dist/plugins/builtin/default-plugin.js +32 -0
- package/dist/plugins/constants.d.ts +21 -0
- package/dist/plugins/constants.js +28 -0
- package/dist/plugins/plugin.d.ts +42 -0
- package/dist/plugins/plugin.js +6 -0
- package/dist/plugins/runtime.d.ts +31 -0
- package/dist/plugins/runtime.js +120 -0
- package/dist/scripts/aggregate-book.js +112 -0
- package/dist/scripts/assert-release-tag-version.js +21 -0
- package/dist/scripts/build-dist.js +384 -0
- package/dist/scripts/build-tenant-worker.js +36 -0
- package/dist/scripts/package-tools.js +88 -0
- package/dist/scripts/patch-starlight-content-path.js +172 -0
- package/dist/scripts/paths.js +11 -0
- package/dist/scripts/publish-package.js +20 -0
- package/dist/scripts/release-verify.js +52 -0
- package/dist/scripts/run-fixture-astro-command.js +21 -0
- package/dist/scripts/tenant-astro-command.js +3 -0
- package/dist/scripts/tenant-build.js +16 -0
- package/dist/scripts/tenant-check.js +7 -0
- package/dist/scripts/test-smoke.js +105 -0
- package/dist/server.js +53 -0
- package/dist/site-resources.d.ts +29 -0
- package/dist/site-resources.js +127 -0
- package/dist/site.js +313 -0
- package/dist/styles/global.css +683 -0
- package/dist/styles/prose.css +89 -0
- package/dist/styles/tokens.css +24 -0
- package/dist/tenant/bridge.js +5 -0
- package/dist/tenant/config.d.ts +9 -0
- package/dist/tenant/config.js +124 -0
- package/dist/tenant/runtime-config.js +20 -0
- package/dist/tsconfigs/strict.json +3 -0
- package/dist/types/agents.d.ts +1 -0
- package/dist/types/agents.js +1 -0
- package/dist/types/astro-build.d.js +0 -0
- package/dist/types/cloudflare-sockets.d.js +0 -0
- package/dist/types/cloudflare.d.ts +1 -0
- package/dist/types/cloudflare.js +1 -0
- package/dist/types/forms.js +4 -0
- package/dist/utils/agents/adapters/execution.js +90 -0
- package/dist/utils/agents/adapters/mutations.js +30 -0
- package/dist/utils/agents/adapters/notification.js +16 -0
- package/dist/utils/agents/adapters/repository.js +61 -0
- package/dist/utils/agents/adapters/research.js +25 -0
- package/dist/utils/agents/adapters/verification.js +62 -0
- package/dist/utils/agents/cli-tools.js +5 -0
- package/dist/utils/agents/contracts/messages.d.ts +88 -0
- package/dist/utils/agents/contracts/messages.js +138 -0
- package/dist/utils/agents/contracts/run.d.ts +20 -0
- package/dist/utils/agents/contracts/run.js +0 -0
- package/dist/utils/agents/runtime-types.d.ts +117 -0
- package/dist/utils/agents/runtime-types.js +4 -0
- package/dist/utils/books-data.js +82 -0
- package/dist/utils/content-status.js +38 -0
- package/dist/utils/forms/config.js +87 -0
- package/dist/utils/forms/constants.js +27 -0
- package/dist/utils/forms/contact-submissions-local.js +19 -0
- package/dist/utils/forms/contact-submissions.js +72 -0
- package/dist/utils/forms/crypto.js +64 -0
- package/dist/utils/forms/guard.js +76 -0
- package/dist/utils/forms/http.js +51 -0
- package/dist/utils/forms/provider-core.js +88 -0
- package/dist/utils/forms/routing-core.js +7 -0
- package/dist/utils/forms/routing.js +13 -0
- package/dist/utils/forms/runtime-core.js +17 -0
- package/dist/utils/forms/runtime.js +27 -0
- package/dist/utils/forms/service-core.js +256 -0
- package/dist/utils/forms/service.js +55 -0
- package/dist/utils/forms/session.js +57 -0
- package/dist/utils/forms/smtp-cloudflare.js +107 -0
- package/dist/utils/forms/smtp-node.js +27 -0
- package/dist/utils/forms/smtp.js +10 -0
- package/dist/utils/forms/subscribers-local.js +21 -0
- package/dist/utils/forms/subscribers.js +53 -0
- package/dist/utils/forms/turnstile.js +31 -0
- package/dist/utils/forms/validation.js +58 -0
- package/dist/utils/hub-content.js +28 -0
- package/dist/utils/plugin-runtime.js +158 -0
- package/dist/utils/routes.js +17 -0
- package/dist/utils/seo.js +4 -0
- package/dist/utils/site-config-schema.js +282 -0
- package/dist/utils/site-config.js +122 -0
- package/dist/utils/starlight-nav.js +62 -0
- package/dist/utils/theme.js +49 -0
- package/dist/vendor/starlight/components/AnchorHeading.astro +53 -0
- package/dist/vendor/starlight/components/Banner.astro +23 -0
- package/dist/vendor/starlight/components/ContentNotice.astro +33 -0
- package/dist/vendor/starlight/components/ContentPanel.astro +27 -0
- package/dist/vendor/starlight/components/DraftContentNotice.astro +5 -0
- package/dist/vendor/starlight/components/EditLink.astro +28 -0
- package/dist/vendor/starlight/components/FallbackContentNotice.astro +5 -0
- package/dist/vendor/starlight/components/Footer.astro +61 -0
- package/dist/vendor/starlight/components/Head.astro +5 -0
- package/dist/vendor/starlight/components/Header.astro +94 -0
- package/dist/vendor/starlight/components/Hero.astro +143 -0
- package/dist/vendor/starlight/components/Icons.js +121 -0
- package/dist/vendor/starlight/components/LanguageSelect.astro +57 -0
- package/dist/vendor/starlight/components/LastUpdated.astro +14 -0
- package/dist/vendor/starlight/components/MarkdownContent.astro +5 -0
- package/dist/vendor/starlight/components/MobileMenuFooter.astro +35 -0
- package/dist/vendor/starlight/components/MobileMenuToggle.astro +107 -0
- package/dist/vendor/starlight/components/MobileTableOfContents.astro +151 -0
- package/dist/vendor/starlight/components/Page.astro +126 -0
- package/dist/vendor/starlight/components/PageFrame.astro +97 -0
- package/dist/vendor/starlight/components/PageSidebar.astro +59 -0
- package/dist/vendor/starlight/components/PageTitle.astro +17 -0
- package/dist/vendor/starlight/components/Pagination.astro +79 -0
- package/dist/vendor/starlight/components/Search.astro +488 -0
- package/dist/vendor/starlight/components/Select.astro +99 -0
- package/dist/vendor/starlight/components/Sidebar.astro +15 -0
- package/dist/vendor/starlight/components/SidebarPersistState.js +43 -0
- package/dist/vendor/starlight/components/SidebarPersister.astro +78 -0
- package/dist/vendor/starlight/components/SidebarRestorePoint.astro +12 -0
- package/dist/vendor/starlight/components/SidebarSublist.astro +154 -0
- package/dist/vendor/starlight/components/SiteTitle.astro +59 -0
- package/dist/vendor/starlight/components/SkipLink.astro +26 -0
- package/dist/vendor/starlight/components/SocialIcons.astro +32 -0
- package/dist/vendor/starlight/components/StarlightPage.astro +17 -0
- package/dist/vendor/starlight/components/TableOfContents/TableOfContentsList.astro +79 -0
- package/dist/vendor/starlight/components/TableOfContents/starlight-toc.js +93 -0
- package/dist/vendor/starlight/components/TableOfContents.astro +18 -0
- package/dist/vendor/starlight/components/ThemeProvider.astro +38 -0
- package/dist/vendor/starlight/components/ThemeSelect.astro +73 -0
- package/dist/vendor/starlight/components/TwoColumnContent.astro +54 -0
- package/dist/vendor/starlight/components.js +26 -0
- package/dist/vendor/starlight/constants.js +4 -0
- package/dist/vendor/starlight/expressive-code.d.js +1 -0
- package/dist/vendor/starlight/global.d.js +0 -0
- package/dist/vendor/starlight/i18n.d.js +1 -0
- package/dist/vendor/starlight/index.js +119 -0
- package/dist/vendor/starlight/integrations/asides-error.js +12 -0
- package/dist/vendor/starlight/integrations/asides.js +179 -0
- package/dist/vendor/starlight/integrations/code-rtl-support.js +21 -0
- package/dist/vendor/starlight/integrations/expressive-code/hast.d.js +1 -0
- package/dist/vendor/starlight/integrations/expressive-code/index.js +63 -0
- package/dist/vendor/starlight/integrations/expressive-code/preprocessor.js +92 -0
- package/dist/vendor/starlight/integrations/expressive-code/themes/night-owl-dark.jsonc +1796 -0
- package/dist/vendor/starlight/integrations/expressive-code/themes/night-owl-dark.jsonc.js +1 -0
- package/dist/vendor/starlight/integrations/expressive-code/themes/night-owl-light.jsonc +1695 -0
- package/dist/vendor/starlight/integrations/expressive-code/themes/night-owl-light.jsonc.js +1 -0
- package/dist/vendor/starlight/integrations/expressive-code/theming.js +62 -0
- package/dist/vendor/starlight/integrations/expressive-code/translations.js +29 -0
- package/dist/vendor/starlight/integrations/heading-links.js +61 -0
- package/dist/vendor/starlight/integrations/pagefind.js +43 -0
- package/dist/vendor/starlight/integrations/remark-rehype.js +68 -0
- package/dist/vendor/starlight/integrations/shared/absolutePathToLang.js +15 -0
- package/dist/vendor/starlight/integrations/shared/localeToLang.js +9 -0
- package/dist/vendor/starlight/integrations/shared/slugToLocale.js +10 -0
- package/dist/vendor/starlight/integrations/sitemap.js +20 -0
- package/dist/vendor/starlight/integrations/virtual-user-config.js +110 -0
- package/dist/vendor/starlight/integrations/vite-layer-order.js +42 -0
- package/dist/vendor/starlight/internal.js +6 -0
- package/dist/vendor/starlight/loaders.js +36 -0
- package/dist/vendor/starlight/locals.d.js +0 -0
- package/dist/vendor/starlight/locals.js +30 -0
- package/dist/vendor/starlight/package.json +231 -0
- package/dist/vendor/starlight/package.json.js +248 -0
- package/dist/vendor/starlight/props.js +0 -0
- package/dist/vendor/starlight/route-data.js +6 -0
- package/dist/vendor/starlight/routes/common.astro +23 -0
- package/dist/vendor/starlight/routes/ssr/404.astro +7 -0
- package/dist/vendor/starlight/routes/ssr/index.astro +14 -0
- package/dist/vendor/starlight/routes/static/404.astro +7 -0
- package/dist/vendor/starlight/routes/static/index.astro +12 -0
- package/dist/vendor/starlight/schema.js +102 -0
- package/dist/vendor/starlight/schemas/badge.js +26 -0
- package/dist/vendor/starlight/schemas/components.js +235 -0
- package/dist/vendor/starlight/schemas/expressiveCode.js +12 -0
- package/dist/vendor/starlight/schemas/favicon.js +33 -0
- package/dist/vendor/starlight/schemas/head.js +32 -0
- package/dist/vendor/starlight/schemas/hero.js +57 -0
- package/dist/vendor/starlight/schemas/i18n.js +101 -0
- package/dist/vendor/starlight/schemas/icon.js +7 -0
- package/dist/vendor/starlight/schemas/logo.js +24 -0
- package/dist/vendor/starlight/schemas/pagefind.js +108 -0
- package/dist/vendor/starlight/schemas/prevNextLink.js +14 -0
- package/dist/vendor/starlight/schemas/sidebar.js +80 -0
- package/dist/vendor/starlight/schemas/site-title.js +19 -0
- package/dist/vendor/starlight/schemas/social.js +19 -0
- package/dist/vendor/starlight/schemas/tableOfContents.js +16 -0
- package/dist/vendor/starlight/style/anchor-links.css +131 -0
- package/dist/vendor/starlight/style/asides.css +51 -0
- package/dist/vendor/starlight/style/layers.css +1 -0
- package/dist/vendor/starlight/style/markdown.css +253 -0
- package/dist/vendor/starlight/style/print.css +175 -0
- package/dist/vendor/starlight/style/props.css +188 -0
- package/dist/vendor/starlight/style/reset.css +52 -0
- package/dist/vendor/starlight/style/util.css +63 -0
- package/dist/vendor/starlight/translations/ar.json +30 -0
- package/dist/vendor/starlight/translations/ar.json.js +32 -0
- package/dist/vendor/starlight/translations/ca.json +43 -0
- package/dist/vendor/starlight/translations/ca.json.js +45 -0
- package/dist/vendor/starlight/translations/cs.json +43 -0
- package/dist/vendor/starlight/translations/cs.json.js +45 -0
- package/dist/vendor/starlight/translations/da.json +30 -0
- package/dist/vendor/starlight/translations/da.json.js +32 -0
- package/dist/vendor/starlight/translations/de.json +30 -0
- package/dist/vendor/starlight/translations/de.json.js +32 -0
- package/dist/vendor/starlight/translations/el.json +30 -0
- package/dist/vendor/starlight/translations/el.json.js +32 -0
- package/dist/vendor/starlight/translations/en.json +30 -0
- package/dist/vendor/starlight/translations/en.json.js +32 -0
- package/dist/vendor/starlight/translations/es.json +43 -0
- package/dist/vendor/starlight/translations/es.json.js +45 -0
- package/dist/vendor/starlight/translations/fa.json +30 -0
- package/dist/vendor/starlight/translations/fa.json.js +32 -0
- package/dist/vendor/starlight/translations/fi.json +30 -0
- package/dist/vendor/starlight/translations/fi.json.js +32 -0
- package/dist/vendor/starlight/translations/fr.json +33 -0
- package/dist/vendor/starlight/translations/fr.json.js +35 -0
- package/dist/vendor/starlight/translations/gl.json +43 -0
- package/dist/vendor/starlight/translations/gl.json.js +45 -0
- package/dist/vendor/starlight/translations/he.json +30 -0
- package/dist/vendor/starlight/translations/he.json.js +32 -0
- package/dist/vendor/starlight/translations/hi.json +30 -0
- package/dist/vendor/starlight/translations/hi.json.js +32 -0
- package/dist/vendor/starlight/translations/hu.json +43 -0
- package/dist/vendor/starlight/translations/hu.json.js +45 -0
- package/dist/vendor/starlight/translations/id.json +30 -0
- package/dist/vendor/starlight/translations/id.json.js +32 -0
- package/dist/vendor/starlight/translations/index.js +77 -0
- package/dist/vendor/starlight/translations/it.json +30 -0
- package/dist/vendor/starlight/translations/it.json.js +32 -0
- package/dist/vendor/starlight/translations/ja.json +30 -0
- package/dist/vendor/starlight/translations/ja.json.js +32 -0
- package/dist/vendor/starlight/translations/ko.json +30 -0
- package/dist/vendor/starlight/translations/ko.json.js +32 -0
- package/dist/vendor/starlight/translations/lv.json +30 -0
- package/dist/vendor/starlight/translations/lv.json.js +32 -0
- package/dist/vendor/starlight/translations/nb.json +30 -0
- package/dist/vendor/starlight/translations/nb.json.js +32 -0
- package/dist/vendor/starlight/translations/nl.json +30 -0
- package/dist/vendor/starlight/translations/nl.json.js +32 -0
- package/dist/vendor/starlight/translations/pl.json +33 -0
- package/dist/vendor/starlight/translations/pl.json.js +35 -0
- package/dist/vendor/starlight/translations/pt.json +30 -0
- package/dist/vendor/starlight/translations/pt.json.js +32 -0
- package/dist/vendor/starlight/translations/ro.json +30 -0
- package/dist/vendor/starlight/translations/ro.json.js +32 -0
- package/dist/vendor/starlight/translations/ru.json +33 -0
- package/dist/vendor/starlight/translations/ru.json.js +35 -0
- package/dist/vendor/starlight/translations/sk.json +30 -0
- package/dist/vendor/starlight/translations/sk.json.js +32 -0
- package/dist/vendor/starlight/translations/sv.json +30 -0
- package/dist/vendor/starlight/translations/sv.json.js +32 -0
- package/dist/vendor/starlight/translations/th.json +30 -0
- package/dist/vendor/starlight/translations/th.json.js +32 -0
- package/dist/vendor/starlight/translations/tr.json +30 -0
- package/dist/vendor/starlight/translations/tr.json.js +32 -0
- package/dist/vendor/starlight/translations/uk.json +30 -0
- package/dist/vendor/starlight/translations/uk.json.js +32 -0
- package/dist/vendor/starlight/translations/vi.json +30 -0
- package/dist/vendor/starlight/translations/vi.json.js +32 -0
- package/dist/vendor/starlight/translations/zh-CN.json +30 -0
- package/dist/vendor/starlight/translations/zh-CN.json.js +32 -0
- package/dist/vendor/starlight/translations/zh-TW.json +30 -0
- package/dist/vendor/starlight/translations/zh-TW.json.js +32 -0
- package/dist/vendor/starlight/types.js +0 -0
- package/dist/vendor/starlight/user-components/Aside.astro +40 -0
- package/dist/vendor/starlight/user-components/Badge.astro +148 -0
- package/dist/vendor/starlight/user-components/Card.astro +68 -0
- package/dist/vendor/starlight/user-components/CardGrid.astro +38 -0
- package/dist/vendor/starlight/user-components/FileTree.astro +137 -0
- package/dist/vendor/starlight/user-components/Icon.astro +42 -0
- package/dist/vendor/starlight/user-components/LinkButton.astro +78 -0
- package/dist/vendor/starlight/user-components/LinkCard.astro +78 -0
- package/dist/vendor/starlight/user-components/Steps.astro +90 -0
- package/dist/vendor/starlight/user-components/TabItem.astro +19 -0
- package/dist/vendor/starlight/user-components/Tabs.astro +268 -0
- package/dist/vendor/starlight/user-components/file-tree-icons.js +608 -0
- package/dist/vendor/starlight/user-components/rehype-file-tree.js +160 -0
- package/dist/vendor/starlight/user-components/rehype-steps.js +53 -0
- package/dist/vendor/starlight/user-components/rehype-tabs.js +73 -0
- package/dist/vendor/starlight/utils/base.js +14 -0
- package/dist/vendor/starlight/utils/canonical.js +13 -0
- package/dist/vendor/starlight/utils/collection-fs.js +13 -0
- package/dist/vendor/starlight/utils/collection.js +16 -0
- package/dist/vendor/starlight/utils/createPathFormatter.js +39 -0
- package/dist/vendor/starlight/utils/createTranslationSystem.js +60 -0
- package/dist/vendor/starlight/utils/error-map.js +110 -0
- package/dist/vendor/starlight/utils/format-path.js +9 -0
- package/dist/vendor/starlight/utils/generateToC.js +18 -0
- package/dist/vendor/starlight/utils/git.js +92 -0
- package/dist/vendor/starlight/utils/gitInlined.js +13 -0
- package/dist/vendor/starlight/utils/head.js +156 -0
- package/dist/vendor/starlight/utils/i18n.js +121 -0
- package/dist/vendor/starlight/utils/localizedUrl.js +37 -0
- package/dist/vendor/starlight/utils/navigation.js +320 -0
- package/dist/vendor/starlight/utils/path.js +52 -0
- package/dist/vendor/starlight/utils/plugins.js +355 -0
- package/dist/vendor/starlight/utils/routing/data.js +116 -0
- package/dist/vendor/starlight/utils/routing/index.js +107 -0
- package/dist/vendor/starlight/utils/routing/middleware.js +40 -0
- package/dist/vendor/starlight/utils/routing/types.js +0 -0
- package/dist/vendor/starlight/utils/routing.js +1 -0
- package/dist/vendor/starlight/utils/slugs.js +70 -0
- package/dist/vendor/starlight/utils/starlight-page.js +123 -0
- package/dist/vendor/starlight/utils/translations-fs.js +31 -0
- package/dist/vendor/starlight/utils/translations.js +31 -0
- package/dist/vendor/starlight/utils/types.js +0 -0
- package/dist/vendor/starlight/utils/url.js +5 -0
- package/dist/vendor/starlight/utils/user-config.js +255 -0
- package/dist/vendor/starlight/utils/validateLogoImports.js +22 -0
- package/dist/vendor/starlight/virtual-internal.d.js +0 -0
- package/dist/vendor/starlight/virtual.d.js +0 -0
- package/dist/worker/forms-worker.js +141 -0
- package/package.json +156 -0
- package/style/anchor-links.css +131 -0
- package/templates/github/deploy.workflow.yml +47 -0
- package/tsconfigs/strict.json +3 -0
- package/utils/git.ts +121 -0
- package/utils/gitInlined.ts +20 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { hashValue } from "./crypto.js";
|
|
2
|
+
async function hasRuntimeRecordsTable(db) {
|
|
3
|
+
const row = await db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'runtime_records' LIMIT 1").first();
|
|
4
|
+
return Boolean(row?.name);
|
|
5
|
+
}
|
|
6
|
+
async function createContactSubmission(db, input) {
|
|
7
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8
|
+
const ipHash = await hashValue(input.ip || "unknown");
|
|
9
|
+
if (await hasRuntimeRecordsTable(db)) {
|
|
10
|
+
const payloadJson = JSON.stringify({
|
|
11
|
+
name: input.name,
|
|
12
|
+
email: input.email,
|
|
13
|
+
organization: input.organization || null,
|
|
14
|
+
contactType: input.contactType,
|
|
15
|
+
subject: input.subject,
|
|
16
|
+
message: input.message,
|
|
17
|
+
userAgent: input.userAgent || "unknown user agent",
|
|
18
|
+
ipHash
|
|
19
|
+
});
|
|
20
|
+
const metaJson = JSON.stringify({});
|
|
21
|
+
await db.prepare(
|
|
22
|
+
`INSERT INTO runtime_records (
|
|
23
|
+
record_type,
|
|
24
|
+
record_key,
|
|
25
|
+
lookup_key,
|
|
26
|
+
secondary_key,
|
|
27
|
+
status,
|
|
28
|
+
schema_version,
|
|
29
|
+
created_at,
|
|
30
|
+
updated_at,
|
|
31
|
+
payload_json,
|
|
32
|
+
meta_json
|
|
33
|
+
) VALUES (?, ?, ?, ?, 'received', 1, ?, ?, ?, ?)`
|
|
34
|
+
).bind(
|
|
35
|
+
"contact_submission",
|
|
36
|
+
`${input.email}:${now}`,
|
|
37
|
+
input.email,
|
|
38
|
+
input.contactType,
|
|
39
|
+
now,
|
|
40
|
+
now,
|
|
41
|
+
payloadJson,
|
|
42
|
+
metaJson
|
|
43
|
+
).run();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await db.prepare(
|
|
47
|
+
`INSERT INTO contact_submissions (
|
|
48
|
+
name,
|
|
49
|
+
email,
|
|
50
|
+
organization,
|
|
51
|
+
contact_type,
|
|
52
|
+
subject,
|
|
53
|
+
message,
|
|
54
|
+
user_agent,
|
|
55
|
+
created_at,
|
|
56
|
+
ip_hash
|
|
57
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
58
|
+
).bind(
|
|
59
|
+
input.name,
|
|
60
|
+
input.email,
|
|
61
|
+
input.organization || null,
|
|
62
|
+
input.contactType,
|
|
63
|
+
input.subject,
|
|
64
|
+
input.message,
|
|
65
|
+
input.userAgent || "unknown user agent",
|
|
66
|
+
now,
|
|
67
|
+
ipHash
|
|
68
|
+
).run();
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
createContactSubmission
|
|
72
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FORM_TOKEN_TTL_MS } from "./constants.js";
|
|
2
|
+
function base64UrlEncode(input) {
|
|
3
|
+
let binary = "";
|
|
4
|
+
for (const value of input) {
|
|
5
|
+
binary += String.fromCharCode(value);
|
|
6
|
+
}
|
|
7
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
8
|
+
}
|
|
9
|
+
function base64UrlDecode(input) {
|
|
10
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
11
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
12
|
+
const binary = atob(padded);
|
|
13
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
14
|
+
}
|
|
15
|
+
async function importHmacKey(secret) {
|
|
16
|
+
return crypto.subtle.importKey(
|
|
17
|
+
"raw",
|
|
18
|
+
new TextEncoder().encode(secret),
|
|
19
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
20
|
+
false,
|
|
21
|
+
["sign", "verify"]
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
async function signFormToken(payload, secret) {
|
|
25
|
+
const serialized = JSON.stringify(payload);
|
|
26
|
+
const encodedPayload = base64UrlEncode(new TextEncoder().encode(serialized));
|
|
27
|
+
const key = await importHmacKey(secret);
|
|
28
|
+
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(encodedPayload));
|
|
29
|
+
return `${encodedPayload}.${base64UrlEncode(new Uint8Array(signature))}`;
|
|
30
|
+
}
|
|
31
|
+
async function verifyFormToken(token, secret) {
|
|
32
|
+
const [encodedPayload, encodedSignature] = token.split(".");
|
|
33
|
+
if (!encodedPayload || !encodedSignature) {
|
|
34
|
+
return { ok: false, reason: "malformed" };
|
|
35
|
+
}
|
|
36
|
+
const key = await importHmacKey(secret);
|
|
37
|
+
const isValid = await crypto.subtle.verify(
|
|
38
|
+
"HMAC",
|
|
39
|
+
key,
|
|
40
|
+
base64UrlDecode(encodedSignature),
|
|
41
|
+
new TextEncoder().encode(encodedPayload)
|
|
42
|
+
);
|
|
43
|
+
if (!isValid) {
|
|
44
|
+
return { ok: false, reason: "signature" };
|
|
45
|
+
}
|
|
46
|
+
const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(encodedPayload)));
|
|
47
|
+
if (Date.now() - payload.issuedAt > FORM_TOKEN_TTL_MS) {
|
|
48
|
+
return { ok: false, reason: "expired", payload };
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, payload };
|
|
51
|
+
}
|
|
52
|
+
function createOpaqueId() {
|
|
53
|
+
return crypto.randomUUID();
|
|
54
|
+
}
|
|
55
|
+
async function hashValue(value) {
|
|
56
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
57
|
+
return Array.from(new Uint8Array(digest)).map((entry) => entry.toString(16).padStart(2, "0")).join("");
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
createOpaqueId,
|
|
61
|
+
hashValue,
|
|
62
|
+
signFormToken,
|
|
63
|
+
verifyFormToken
|
|
64
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NONCE_TTL_SECONDS, RATE_LIMIT_MAX_ATTEMPTS, RATE_LIMIT_TTL_SECONDS } from "./constants.js";
|
|
2
|
+
import { hashValue } from "./crypto.js";
|
|
3
|
+
const localNonceStore = globalThis.__treeseedFixtureNonceStore ?? /* @__PURE__ */ new Map();
|
|
4
|
+
const localRateStore = globalThis.__treeseedFixtureRateStore ?? /* @__PURE__ */ new Map();
|
|
5
|
+
globalThis.__treeseedFixtureNonceStore = localNonceStore;
|
|
6
|
+
globalThis.__treeseedFixtureRateStore = localRateStore;
|
|
7
|
+
async function incrementCounter(kv, key, ttl) {
|
|
8
|
+
const currentValue = await kv.get(key);
|
|
9
|
+
const nextValue = (Number.parseInt(currentValue ?? "0", 10) || 0) + 1;
|
|
10
|
+
await kv.put(key, String(nextValue), { expirationTtl: ttl });
|
|
11
|
+
return nextValue;
|
|
12
|
+
}
|
|
13
|
+
async function assertNonceUnused(kv, nonce) {
|
|
14
|
+
const nonceKey = `nonce:${nonce}`;
|
|
15
|
+
const current = await kv.get(nonceKey);
|
|
16
|
+
if (current) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
await kv.put(nonceKey, "1", { expirationTtl: NONCE_TTL_SECONDS });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
async function applySubmissionRateLimit(kv, remoteIp, email, formType) {
|
|
23
|
+
const ipHash = remoteIp ? await hashValue(remoteIp) : "unknown";
|
|
24
|
+
const emailHash = await hashValue(email);
|
|
25
|
+
const ipKey = `rate:${formType}:ip:${ipHash}`;
|
|
26
|
+
const emailKey = `rate:${formType}:email:${emailHash}`;
|
|
27
|
+
const [ipCount, emailCount] = await Promise.all([
|
|
28
|
+
incrementCounter(kv, ipKey, RATE_LIMIT_TTL_SECONDS),
|
|
29
|
+
incrementCounter(kv, emailKey, RATE_LIMIT_TTL_SECONDS)
|
|
30
|
+
]);
|
|
31
|
+
return ipCount <= RATE_LIMIT_MAX_ATTEMPTS && emailCount <= RATE_LIMIT_MAX_ATTEMPTS;
|
|
32
|
+
}
|
|
33
|
+
function pruneExpiredLocalState() {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [nonce, expiresAt] of localNonceStore.entries()) {
|
|
36
|
+
if (expiresAt <= now) {
|
|
37
|
+
localNonceStore.delete(nonce);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const [key, record] of localRateStore.entries()) {
|
|
41
|
+
if (record.expiresAt <= now) {
|
|
42
|
+
localRateStore.delete(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function assertNonceUnusedLocal(nonce) {
|
|
47
|
+
pruneExpiredLocalState();
|
|
48
|
+
if (localNonceStore.has(nonce)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
localNonceStore.set(nonce, Date.now() + NONCE_TTL_SECONDS * 1e3);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
async function incrementLocalCounter(key, ttlSeconds) {
|
|
55
|
+
pruneExpiredLocalState();
|
|
56
|
+
const currentRecord = localRateStore.get(key);
|
|
57
|
+
const nextCount = (currentRecord?.count ?? 0) + 1;
|
|
58
|
+
localRateStore.set(key, {
|
|
59
|
+
count: nextCount,
|
|
60
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
61
|
+
});
|
|
62
|
+
return nextCount;
|
|
63
|
+
}
|
|
64
|
+
async function applySubmissionRateLimitLocal(remoteIp, email, formType) {
|
|
65
|
+
const ipHash = remoteIp ? await hashValue(remoteIp) : "unknown";
|
|
66
|
+
const emailHash = await hashValue(email);
|
|
67
|
+
const ipCount = await incrementLocalCounter(`rate:${formType}:ip:${ipHash}`, RATE_LIMIT_TTL_SECONDS);
|
|
68
|
+
const emailCount = await incrementLocalCounter(`rate:${formType}:email:${emailHash}`, RATE_LIMIT_TTL_SECONDS);
|
|
69
|
+
return ipCount <= RATE_LIMIT_MAX_ATTEMPTS && emailCount <= RATE_LIMIT_MAX_ATTEMPTS;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
applySubmissionRateLimit,
|
|
73
|
+
applySubmissionRateLimitLocal,
|
|
74
|
+
assertNonceUnused,
|
|
75
|
+
assertNonceUnusedLocal
|
|
76
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { FORM_CODE_PARAM, FORM_SUCCESS_PARAM, SUBSCRIBE_ANCHOR_ID } from "./constants.js";
|
|
2
|
+
function getRemoteIp(request) {
|
|
3
|
+
return request.headers.get("CF-Connecting-IP") ?? request.headers.get("X-Forwarded-For") ?? "";
|
|
4
|
+
}
|
|
5
|
+
function fallbackPath(formType) {
|
|
6
|
+
return formType === "contact" ? "/contact/" : "/";
|
|
7
|
+
}
|
|
8
|
+
function resolveRedirectBase(requestUrl) {
|
|
9
|
+
if (requestUrl instanceof URL) {
|
|
10
|
+
return requestUrl.origin;
|
|
11
|
+
}
|
|
12
|
+
if (typeof requestUrl === "string" && requestUrl.length) {
|
|
13
|
+
try {
|
|
14
|
+
return new URL(requestUrl).origin;
|
|
15
|
+
} catch {
|
|
16
|
+
return requestUrl;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return "https://treeseed.dev";
|
|
20
|
+
}
|
|
21
|
+
function buildRedirectTarget(formType, rawRedirectTo, isSuccess, code, requestUrl) {
|
|
22
|
+
const fallback = fallbackPath(formType);
|
|
23
|
+
const url = new URL(rawRedirectTo || fallback, resolveRedirectBase(requestUrl));
|
|
24
|
+
url.searchParams.set(FORM_SUCCESS_PARAM, isSuccess ? "success" : "error");
|
|
25
|
+
url.searchParams.set(FORM_CODE_PARAM, code);
|
|
26
|
+
if (formType === "subscribe") {
|
|
27
|
+
url.hash = SUBSCRIBE_ANCHOR_ID;
|
|
28
|
+
}
|
|
29
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
30
|
+
}
|
|
31
|
+
function sanitizeRedirectTo(rawRedirectTo, formType, requestUrl) {
|
|
32
|
+
const fallback = fallbackPath(formType);
|
|
33
|
+
if (!rawRedirectTo) {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const base = resolveRedirectBase(requestUrl);
|
|
38
|
+
const url = new URL(rawRedirectTo, base);
|
|
39
|
+
if (url.origin !== new URL(base).origin) {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
43
|
+
} catch {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
buildRedirectTarget,
|
|
49
|
+
getRemoteIp,
|
|
50
|
+
sanitizeRedirectTo
|
|
51
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { assertNonceUnused, assertNonceUnusedLocal, applySubmissionRateLimit, applySubmissionRateLimitLocal } from "./guard.js";
|
|
2
|
+
import { createContactSubmission } from "./contact-submissions.js";
|
|
3
|
+
import { upsertSubscriber } from "./subscribers.js";
|
|
4
|
+
import { sendEmail as sendDefaultEmail } from "./smtp.js";
|
|
5
|
+
import { verifyTurnstileToken as verifyDefaultTurnstileToken } from "./turnstile.js";
|
|
6
|
+
function createDefaultGuardStore(runtime, kv) {
|
|
7
|
+
return {
|
|
8
|
+
assertNonceUnused(nonce) {
|
|
9
|
+
if (runtime.isCloudflareRuntime && kv && !runtime.bypassCloudflareGuards) {
|
|
10
|
+
return assertNonceUnused(kv, nonce);
|
|
11
|
+
}
|
|
12
|
+
return assertNonceUnusedLocal(nonce);
|
|
13
|
+
},
|
|
14
|
+
applyRateLimit(remoteIp, email, formType) {
|
|
15
|
+
if (runtime.isCloudflareRuntime && kv && !runtime.bypassCloudflareGuards) {
|
|
16
|
+
return applySubmissionRateLimit(kv, remoteIp, email, formType);
|
|
17
|
+
}
|
|
18
|
+
return applySubmissionRateLimitLocal(remoteIp, email, formType);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function createDefaultSubscriberStore(runtime, db) {
|
|
23
|
+
return {
|
|
24
|
+
async upsert(input) {
|
|
25
|
+
if (runtime.isCloudflareRuntime && db) {
|
|
26
|
+
return upsertSubscriber(db, input);
|
|
27
|
+
}
|
|
28
|
+
const { upsertLocalSubscriber } = await import("./subscribers-local.js");
|
|
29
|
+
return upsertLocalSubscriber(input);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createDefaultContactStore(runtime, db) {
|
|
34
|
+
return {
|
|
35
|
+
async create(input) {
|
|
36
|
+
if (runtime.isCloudflareRuntime && db) {
|
|
37
|
+
return createContactSubmission(db, input);
|
|
38
|
+
}
|
|
39
|
+
const { createLocalContactSubmission } = await import("./contact-submissions-local.js");
|
|
40
|
+
return createLocalContactSubmission(input);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const BUILTIN_FORMS_PROVIDERS = {
|
|
45
|
+
store_only: {
|
|
46
|
+
id: "store_only",
|
|
47
|
+
behavior: {
|
|
48
|
+
contact: { notifyAdmin: false, requireSmtp: false },
|
|
49
|
+
subscribe: { notifyAdmin: false, sendConfirmation: false, requireSmtp: false }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
notify_admin: {
|
|
53
|
+
id: "notify_admin",
|
|
54
|
+
behavior: {
|
|
55
|
+
contact: { notifyAdmin: true, requireSmtp: false },
|
|
56
|
+
subscribe: { notifyAdmin: true, sendConfirmation: false, requireSmtp: false }
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
full_email: {
|
|
60
|
+
id: "full_email",
|
|
61
|
+
behavior: {
|
|
62
|
+
contact: { notifyAdmin: true, requireSmtp: true },
|
|
63
|
+
subscribe: { notifyAdmin: true, sendConfirmation: true, requireSmtp: true }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function finalizeFormsProvider(provider) {
|
|
68
|
+
return {
|
|
69
|
+
...provider,
|
|
70
|
+
createGuardStore: provider.createGuardStore ?? ((input) => createDefaultGuardStore(input.runtime, input.kv)),
|
|
71
|
+
createSubscriberStore: provider.createSubscriberStore ?? ((input) => createDefaultSubscriberStore(input.runtime, input.db)),
|
|
72
|
+
createContactStore: provider.createContactStore ?? ((input) => createDefaultContactStore(input.runtime, input.db)),
|
|
73
|
+
sendEmail: provider.sendEmail ?? sendDefaultEmail,
|
|
74
|
+
verifyTurnstileToken: provider.verifyTurnstileToken ?? verifyDefaultTurnstileToken
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolveBuiltinFormsProvider(providerId) {
|
|
78
|
+
const provider = BUILTIN_FORMS_PROVIDERS[providerId];
|
|
79
|
+
if (!provider) {
|
|
80
|
+
throw new Error(`Treeseed built-in forms provider "${providerId}" is not registered.`);
|
|
81
|
+
}
|
|
82
|
+
return finalizeFormsProvider(provider);
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
BUILTIN_FORMS_PROVIDERS,
|
|
86
|
+
finalizeFormsProvider,
|
|
87
|
+
resolveBuiltinFormsProvider
|
|
88
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getContactRoutingMap, getSubscribeRecipients } from "./config.js";
|
|
2
|
+
import { resolveContactRecipientsFromMap } from "./routing-core.js";
|
|
3
|
+
function resolveContactRecipients(contactType) {
|
|
4
|
+
const routingMap = getContactRoutingMap();
|
|
5
|
+
return resolveContactRecipientsFromMap(routingMap, contactType);
|
|
6
|
+
}
|
|
7
|
+
function resolveSubscribeRecipients() {
|
|
8
|
+
return getSubscribeRecipients();
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
resolveContactRecipients,
|
|
12
|
+
resolveSubscribeRecipients
|
|
13
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function deriveFormRuntimeCapabilities(input) {
|
|
2
|
+
const isLocalMode = input.isDevServer || Boolean(input.localDevMode);
|
|
3
|
+
return {
|
|
4
|
+
isCloudflareRuntime: input.isCloudflareRuntime,
|
|
5
|
+
isLocalMode,
|
|
6
|
+
localDevMode: input.localDevMode ?? "production",
|
|
7
|
+
bypassTurnstile: isLocalMode ? input.bypassTurnstile ?? false : false,
|
|
8
|
+
bypassCloudflareGuards: isLocalMode ? input.bypassCloudflareGuards ?? false : false,
|
|
9
|
+
useMailpit: isLocalMode ? input.useMailpit : false,
|
|
10
|
+
formsMode: input.formsMode,
|
|
11
|
+
smtpEnabled: input.smtpEnabled || (isLocalMode ? input.useMailpit : false),
|
|
12
|
+
turnstileEnabled: input.turnstileEnabled
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
deriveFormRuntimeCapabilities
|
|
17
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFormsMode,
|
|
3
|
+
getLocalDevMode,
|
|
4
|
+
isSmtpEnabled,
|
|
5
|
+
isTurnstileEnabled,
|
|
6
|
+
shouldBypassCloudflareGuardsByEnv,
|
|
7
|
+
shouldBypassTurnstileByEnv,
|
|
8
|
+
shouldUseMailpit
|
|
9
|
+
} from "./config.js";
|
|
10
|
+
import { deriveFormRuntimeCapabilities } from "./runtime-core.js";
|
|
11
|
+
function resolveFormRuntimeCapabilities(locals) {
|
|
12
|
+
const runtime = locals.runtime;
|
|
13
|
+
return deriveFormRuntimeCapabilities({
|
|
14
|
+
isCloudflareRuntime: Boolean(runtime?.env?.FORM_GUARD_KV && runtime?.env?.SITE_DATA_DB),
|
|
15
|
+
localDevMode: getLocalDevMode(),
|
|
16
|
+
isDevServer: import.meta.env.DEV,
|
|
17
|
+
bypassTurnstile: shouldBypassTurnstileByEnv(),
|
|
18
|
+
bypassCloudflareGuards: shouldBypassCloudflareGuardsByEnv(),
|
|
19
|
+
useMailpit: shouldUseMailpit(),
|
|
20
|
+
formsMode: getFormsMode(),
|
|
21
|
+
smtpEnabled: isSmtpEnabled(),
|
|
22
|
+
turnstileEnabled: isTurnstileEnabled()
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
resolveFormRuntimeCapabilities
|
|
27
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { CONTACT_TYPE_LABELS, FORM_SESSION_COOKIE, HONEYPOT_FIELD } from "./constants.js";
|
|
2
|
+
import { buildRedirectTarget, getRemoteIp, sanitizeRedirectTo } from "./http.js";
|
|
3
|
+
import { issueFormToken, verifyIssuedToken, createSessionCookie } from "./session.js";
|
|
4
|
+
import { parsePayload, validatePayload } from "./validation.js";
|
|
5
|
+
import { hashValue } from "./crypto.js";
|
|
6
|
+
import { resolveContactRecipientsFromMap } from "./routing-core.js";
|
|
7
|
+
function resultFor(formType, redirectTo, ok, code, message) {
|
|
8
|
+
return {
|
|
9
|
+
ok,
|
|
10
|
+
code,
|
|
11
|
+
message,
|
|
12
|
+
redirectTo: buildRedirectTarget(formType, redirectTo, ok, code)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function summarizeRequest(request) {
|
|
16
|
+
return request.headers.get("user-agent") ?? "unknown user agent";
|
|
17
|
+
}
|
|
18
|
+
async function buildContactMessage(payload, remoteIp, request) {
|
|
19
|
+
const ipHash = await hashValue(remoteIp || "unknown");
|
|
20
|
+
return [
|
|
21
|
+
"TreeSeed contact form submission",
|
|
22
|
+
"",
|
|
23
|
+
`Name: ${payload.name}`,
|
|
24
|
+
`Email: ${payload.email}`,
|
|
25
|
+
`Organization: ${payload.organization || "n/a"}`,
|
|
26
|
+
`Contact type: ${CONTACT_TYPE_LABELS[payload.contactType]}`,
|
|
27
|
+
`Subject: ${payload.subject}`,
|
|
28
|
+
"",
|
|
29
|
+
payload.message,
|
|
30
|
+
"",
|
|
31
|
+
"Metadata",
|
|
32
|
+
`Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
33
|
+
`IP hash: ${ipHash}`,
|
|
34
|
+
`User agent: ${summarizeRequest(request)}`
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
async function sendContactEmail(payload, remoteIp, request, runtime, config, formsProvider = config.formsProvider) {
|
|
38
|
+
const recipients = resolveContactRecipientsFromMap(config.contactRouting, payload.contactType);
|
|
39
|
+
if (!recipients.length) {
|
|
40
|
+
throw new Error("No contact recipients configured for this route.");
|
|
41
|
+
}
|
|
42
|
+
await formsProvider.sendEmail(
|
|
43
|
+
{
|
|
44
|
+
to: recipients,
|
|
45
|
+
subject: `[TreeSeed Contact] ${payload.subject}`,
|
|
46
|
+
text: await buildContactMessage(payload, remoteIp, request),
|
|
47
|
+
replyTo: payload.email
|
|
48
|
+
},
|
|
49
|
+
runtime,
|
|
50
|
+
{ smtp: config.smtpConfig, siteUrl: config.siteUrl }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
async function persistContactSubmission(payload, remoteIp, request, contactStore) {
|
|
54
|
+
await contactStore.create({
|
|
55
|
+
name: payload.name,
|
|
56
|
+
email: payload.email,
|
|
57
|
+
organization: payload.organization,
|
|
58
|
+
contactType: payload.contactType,
|
|
59
|
+
subject: payload.subject,
|
|
60
|
+
message: payload.message,
|
|
61
|
+
ip: remoteIp,
|
|
62
|
+
userAgent: summarizeRequest(request)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function handleSubscribe(payload, remoteIp, request, subscriberStore, runtime, config, formsProvider = config.formsProvider) {
|
|
66
|
+
await subscriberStore.upsert({
|
|
67
|
+
email: payload.email,
|
|
68
|
+
name: payload.name,
|
|
69
|
+
source: "footer",
|
|
70
|
+
ip: remoteIp
|
|
71
|
+
});
|
|
72
|
+
const notifyRecipients = config.subscribeRecipients;
|
|
73
|
+
const ipHash = await hashValue(remoteIp || "unknown");
|
|
74
|
+
if (!formsProvider.behavior.subscribe.notifyAdmin && !formsProvider.behavior.subscribe.sendConfirmation) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const notifyAdmin = async () => {
|
|
78
|
+
if (!notifyRecipients.length) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await formsProvider.sendEmail({
|
|
82
|
+
to: notifyRecipients,
|
|
83
|
+
subject: "[TreeSeed Updates] New subscriber",
|
|
84
|
+
text: [
|
|
85
|
+
"TreeSeed updates signup",
|
|
86
|
+
"",
|
|
87
|
+
`Email: ${payload.email}`,
|
|
88
|
+
`Name: ${payload.name || "n/a"}`,
|
|
89
|
+
`Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
90
|
+
`IP hash: ${ipHash}`,
|
|
91
|
+
`User agent: ${summarizeRequest(request)}`
|
|
92
|
+
].join("\n")
|
|
93
|
+
}, runtime, { smtp: config.smtpConfig, siteUrl: config.siteUrl });
|
|
94
|
+
};
|
|
95
|
+
if (!formsProvider.behavior.subscribe.sendConfirmation) {
|
|
96
|
+
if (formsProvider.behavior.subscribe.requireSmtp && !runtime.smtpEnabled) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await notifyAdmin();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn("Subscriber notification email failed", error);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!runtime.smtpEnabled) {
|
|
107
|
+
throw new Error("SMTP is required for full_email subscribe delivery.");
|
|
108
|
+
}
|
|
109
|
+
await notifyAdmin();
|
|
110
|
+
await formsProvider.sendEmail({
|
|
111
|
+
to: [payload.email],
|
|
112
|
+
subject: "You are subscribed to TreeSeed updates",
|
|
113
|
+
text: [
|
|
114
|
+
"Thanks for subscribing to TreeSeed updates.",
|
|
115
|
+
"",
|
|
116
|
+
"We will use this address to send occasional product, documentation, and workflow updates as the project evolves.",
|
|
117
|
+
"",
|
|
118
|
+
"You can reply to this email if you need anything in the meantime."
|
|
119
|
+
].join("\n")
|
|
120
|
+
}, runtime, { smtp: config.smtpConfig, siteUrl: config.siteUrl });
|
|
121
|
+
}
|
|
122
|
+
async function handleTokenRequestWithConfig(context, config) {
|
|
123
|
+
const formType = context.url.searchParams.get("formType");
|
|
124
|
+
if (formType !== "contact" && formType !== "subscribe") {
|
|
125
|
+
return new Response(JSON.stringify({ ok: false, message: "Unknown form type." }), {
|
|
126
|
+
status: 400,
|
|
127
|
+
headers: { "content-type": "application/json" }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const issued = await issueFormToken(formType, config.formSecret);
|
|
131
|
+
const cookie = createSessionCookie(issued.sessionId, context.url);
|
|
132
|
+
context.setCookie?.(cookie);
|
|
133
|
+
return new Response(
|
|
134
|
+
JSON.stringify({
|
|
135
|
+
ok: true,
|
|
136
|
+
formToken: issued.formToken,
|
|
137
|
+
sessionId: issued.sessionId
|
|
138
|
+
}),
|
|
139
|
+
{
|
|
140
|
+
headers: {
|
|
141
|
+
"content-type": "application/json",
|
|
142
|
+
"cache-control": "no-store"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
async function handleFormSubmissionWithConfig(context, config) {
|
|
148
|
+
const bindings = config.bindings ?? {};
|
|
149
|
+
const runtime = config.runtime;
|
|
150
|
+
const formsProvider = config.formsProvider;
|
|
151
|
+
const guardStore = formsProvider.createGuardStore({
|
|
152
|
+
runtime,
|
|
153
|
+
kv: bindings.FORM_GUARD_KV ?? null
|
|
154
|
+
});
|
|
155
|
+
const subscriberStore = formsProvider.createSubscriberStore({
|
|
156
|
+
runtime,
|
|
157
|
+
db: bindings.SITE_DATA_DB ?? null
|
|
158
|
+
});
|
|
159
|
+
const contactStore = formsProvider.createContactStore({
|
|
160
|
+
runtime,
|
|
161
|
+
db: bindings.SITE_DATA_DB ?? null
|
|
162
|
+
});
|
|
163
|
+
const formData = await context.request.formData();
|
|
164
|
+
const payload = parsePayload(formData);
|
|
165
|
+
const formTypeValue = typeof formData.get("formType") === "string" ? String(formData.get("formType")) : "contact";
|
|
166
|
+
const normalizedFormType = formTypeValue === "subscribe" ? "subscribe" : "contact";
|
|
167
|
+
const redirectTo = sanitizeRedirectTo(
|
|
168
|
+
typeof formData.get("redirectTo") === "string" ? String(formData.get("redirectTo")) : null,
|
|
169
|
+
normalizedFormType,
|
|
170
|
+
context.url
|
|
171
|
+
);
|
|
172
|
+
if (typeof formData.get(HONEYPOT_FIELD) === "string" && String(formData.get(HONEYPOT_FIELD)).trim()) {
|
|
173
|
+
return resultFor(normalizedFormType, redirectTo, false, "invalid_request", "Spam protection triggered.");
|
|
174
|
+
}
|
|
175
|
+
if (!payload) {
|
|
176
|
+
return resultFor(normalizedFormType, redirectTo, false, "invalid_form", "The submitted form data was invalid.");
|
|
177
|
+
}
|
|
178
|
+
const validation = validatePayload(payload);
|
|
179
|
+
if (!validation.ok) {
|
|
180
|
+
return resultFor(payload.formType, redirectTo, false, "invalid_form", validation.message);
|
|
181
|
+
}
|
|
182
|
+
const cookieSessionId = context.getCookie(FORM_SESSION_COOKIE) ?? "";
|
|
183
|
+
const postedSessionId = typeof formData.get("formSession") === "string" ? String(formData.get("formSession")).trim() : "";
|
|
184
|
+
const formToken = typeof formData.get("formToken") === "string" ? String(formData.get("formToken")) : "";
|
|
185
|
+
const sessionCandidates = Array.from(new Set([cookieSessionId, postedSessionId].filter(Boolean)));
|
|
186
|
+
let tokenResult;
|
|
187
|
+
if (!sessionCandidates.length) {
|
|
188
|
+
tokenResult = { ok: false, reason: "missing-session" };
|
|
189
|
+
} else {
|
|
190
|
+
tokenResult = { ok: false, reason: "mismatch" };
|
|
191
|
+
for (const sessionId of sessionCandidates) {
|
|
192
|
+
const candidateResult = await verifyIssuedToken(formToken, sessionId, payload.formType, config.formSecret);
|
|
193
|
+
if (candidateResult.ok) {
|
|
194
|
+
tokenResult = candidateResult;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (tokenResult.reason !== "expired") {
|
|
198
|
+
tokenResult = candidateResult;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!tokenResult.ok) {
|
|
203
|
+
const code = tokenResult.reason === "expired" ? "token_expired" : "token_invalid";
|
|
204
|
+
return resultFor(payload.formType, redirectTo, false, code, "Please refresh the page and try again.");
|
|
205
|
+
}
|
|
206
|
+
if (!await guardStore.assertNonceUnused(tokenResult.payload.nonce)) {
|
|
207
|
+
return resultFor(payload.formType, redirectTo, false, "token_replayed", "This submission token has already been used.");
|
|
208
|
+
}
|
|
209
|
+
const remoteIp = getRemoteIp(context.request);
|
|
210
|
+
const rateLimitOk = await guardStore.applyRateLimit(remoteIp, payload.email, payload.formType);
|
|
211
|
+
if (!rateLimitOk) {
|
|
212
|
+
return resultFor(payload.formType, redirectTo, false, "rate_limited", "Please wait a moment before trying again.");
|
|
213
|
+
}
|
|
214
|
+
const turnstileToken = typeof formData.get("cf-turnstile-response") === "string" ? String(formData.get("cf-turnstile-response")) : "";
|
|
215
|
+
const turnstileResult = await formsProvider.verifyTurnstileToken(
|
|
216
|
+
turnstileToken,
|
|
217
|
+
remoteIp,
|
|
218
|
+
payload.formType === "contact" ? "contact_submit" : "subscribe_submit",
|
|
219
|
+
runtime,
|
|
220
|
+
config.turnstileSecret
|
|
221
|
+
);
|
|
222
|
+
if (!turnstileResult.ok) {
|
|
223
|
+
return resultFor(payload.formType, redirectTo, false, "captcha_failed", "Please complete the verification challenge and try again.");
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (payload.formType === "contact") {
|
|
227
|
+
await persistContactSubmission(payload, remoteIp, context.request, contactStore);
|
|
228
|
+
if (formsProvider.behavior.contact.requireSmtp && !runtime.smtpEnabled) {
|
|
229
|
+
return resultFor(payload.formType, redirectTo, false, "config_error", "SMTP must be configured for this form mode.");
|
|
230
|
+
}
|
|
231
|
+
if (formsProvider.behavior.contact.notifyAdmin) {
|
|
232
|
+
if (!formsProvider.behavior.contact.requireSmtp && !runtime.smtpEnabled) {
|
|
233
|
+
return resultFor(payload.formType, redirectTo, true, "success", "Thanks, your submission has been received.");
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await sendContactEmail(payload, remoteIp, context.request, runtime, config, formsProvider);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.warn("Contact notification email failed", error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
if (formsProvider.behavior.subscribe.requireSmtp && !runtime.smtpEnabled) {
|
|
243
|
+
return resultFor(payload.formType, redirectTo, false, "config_error", "SMTP must be configured for this form mode.");
|
|
244
|
+
}
|
|
245
|
+
await handleSubscribe(payload, remoteIp, context.request, subscriberStore, runtime, config, formsProvider);
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error("Form submission failed", error);
|
|
249
|
+
return resultFor(payload.formType, redirectTo, false, "delivery_failed", "The submission could not be delivered right now.");
|
|
250
|
+
}
|
|
251
|
+
return resultFor(payload.formType, redirectTo, true, "success", "Thanks, your submission has been received.");
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
handleFormSubmissionWithConfig,
|
|
255
|
+
handleTokenRequestWithConfig
|
|
256
|
+
};
|