@leed-ai/cli 0.0.0-stage → 6.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +57 -0
- package/README.md +125 -2
- package/dist/LICENSE.md +57 -0
- package/dist/acorn-DAgDT1om.js +5087 -0
- package/dist/angular-l0_4PK8N.js +3598 -0
- package/dist/authShared-Dray38Ls.js +1161 -0
- package/dist/babel-CJeRQd5u.js +10038 -0
- package/dist/buildResults-tiq1nugP.js +21180 -0
- package/dist/chunk-1VJ9lAw7.js +42 -0
- package/dist/cliAuth-38JjD6hw.js +15 -0
- package/dist/env-B0bHiXJJ.js +29 -0
- package/dist/errors-CqKamGCu.js +481 -0
- package/dist/estree-E8WG8UHj.js +6930 -0
- package/dist/flow-6xaRIbPQ.js +52255 -0
- package/dist/glimmer-aJPjsfbT.js +7272 -0
- package/dist/graphql-mcgtPEiZ.js +1901 -0
- package/dist/html--eeea3Kl.js +6553 -0
- package/dist/leed.eleventy.mjs +6525 -0
- package/dist/leed.mjs +88952 -0
- package/dist/leed.sources.json +798 -0
- package/dist/leedLogger-Zkada5aO.js +78 -0
- package/dist/markdown-D73jNKwx.js +6836 -0
- package/dist/meriyah-CMp1nDDW.js +8932 -0
- package/dist/package.json +101 -0
- package/dist/postcss-h4fBr2Fn.js +6760 -0
- package/dist/prettier-CwFSKxfN.js +16821 -0
- package/dist/static/customer-site-worker/package.json +9 -0
- package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
- package/dist/static/customer-site-worker/src/constants.ts +20 -0
- package/dist/static/customer-site-worker/src/cors.ts +120 -0
- package/dist/static/customer-site-worker/src/index.ts +242 -0
- package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
- package/dist/static/customer-site-worker/src/proxy.ts +92 -0
- package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
- package/dist/static/customer-site-worker/src/tier.ts +40 -0
- package/dist/static/customer-site-worker/src/types.ts +82 -0
- package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
- package/dist/static/customer-site-worker/tsconfig.json +19 -0
- package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
- package/dist/static/install/husky/colors.mjs +5 -0
- package/dist/static/install/husky/hooks/commit-msg +1 -0
- package/dist/static/install/husky/hooks/pre-commit +1 -0
- package/dist/static/install/husky/hooks/pre-push +1 -0
- package/dist/static/install/husky/valid-command.mjs +29 -0
- package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
- package/dist/static/install/package.json +10 -0
- package/dist/static/install/packages.txt +4 -0
- package/dist/static/install/tasks.json +111 -0
- package/dist/static/javascript/detectIncognito.js +195 -0
- package/dist/static/javascript/documentation.js +2781 -0
- package/dist/static/javascript/fp/fp.esm.js +2896 -0
- package/dist/static/javascript/fp/fp.js +3067 -0
- package/dist/static/javascript/live-search.js +986 -0
- package/dist/static/javascript/lunr-search.js +843 -0
- package/dist/static/javascript/timeme.js +451 -0
- package/dist/static/javascript/tracker/webmcp.js +67 -0
- package/dist/static/javascript/tracker/whisper.js +1 -0
- package/dist/static/javascript/utilities.js +819 -0
- package/dist/static/javascript/versions.json +74 -0
- package/dist/static/javascript/zoomable.js +488 -0
- package/dist/static/site/_data/eleventyComputed.js +228 -0
- package/dist/static/site/_data/leed-form-data.json +253 -0
- package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
- package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
- package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
- package/dist/static/site/_includes/leed/api/object.hbs +110 -0
- package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
- package/dist/static/site/_includes/leed/api/property.hbs +153 -0
- package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
- package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
- package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
- package/dist/static/site/_includes/leed/api/security.hbs +75 -0
- package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
- package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
- package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
- package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
- package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
- package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
- package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
- package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
- package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
- package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
- package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
- package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
- package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
- package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
- package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
- package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
- package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
- package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
- package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
- package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
- package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
- package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
- package/dist/static/site/_includes/leed/form.hbs +266 -0
- package/dist/static/site/_includes/leed/head.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
- package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
- package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
- package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
- package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
- package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
- package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
- package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
- package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
- package/dist/static/site/leed-feed-atom.hbs +42 -0
- package/dist/static/site/leed-feed-json.hbs +56 -0
- package/dist/static/site/leed-feed-rss.hbs +39 -0
- package/dist/static/site/leed-headers.hbs +38 -0
- package/dist/static/site/leed-redirects.hbs +10 -0
- package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
- package/dist/static/site/leed-sitemap.hbs +19 -0
- package/dist/static/site/leed-unsubscribe.hbs +44 -0
- package/dist/static/site/leed-unsubscribed.hbs +40 -0
- package/dist/static/tailwind/components/alerts.css +132 -0
- package/dist/static/tailwind/components/api-methods.css +39 -0
- package/dist/static/tailwind/components/breadcrumbs.css +31 -0
- package/dist/static/tailwind/components/code.css +88 -0
- package/dist/static/tailwind/components/collapsible.css +38 -0
- package/dist/static/tailwind/components/document-search.css +222 -0
- package/dist/static/tailwind/components/form.css +185 -0
- package/dist/static/tailwind/components/kbd.css +7 -0
- package/dist/static/tailwind/components/pagination.css +81 -0
- package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
- package/dist/static/tailwind/components/tab-groups.css +117 -0
- package/dist/static/tailwind/components/tables.css +0 -0
- package/dist/static/tailwind/components/toc.css +60 -0
- package/dist/static/tailwind/components.css +248 -0
- package/dist/static/tailwind/defaults.css +260 -0
- package/dist/static/tailwind/docs/api.css +701 -0
- package/dist/static/tailwind/docs/components/blockquote.css +5 -0
- package/dist/static/tailwind/docs/components/button.css +11 -0
- package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
- package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
- package/dist/static/tailwind/docs/components/navigation.css +279 -0
- package/dist/static/tailwind/docs/components/table.css +54 -0
- package/dist/static/tailwind/docs/components.css +6 -0
- package/dist/static/tailwind/docs/docs.css +427 -0
- package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
- package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
- package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
- package/dist/static/tailwind/docs/layouts.css +3 -0
- package/dist/static/tailwind/docs/themes/amber.css +113 -0
- package/dist/static/tailwind/docs/themes/blue.css +94 -0
- package/dist/static/tailwind/docs/themes/cyan.css +111 -0
- package/dist/static/tailwind/docs/themes/emerald.css +111 -0
- package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
- package/dist/static/tailwind/docs/themes/green.css +95 -0
- package/dist/static/tailwind/docs/themes/indigo.css +111 -0
- package/dist/static/tailwind/docs/themes/lime.css +111 -0
- package/dist/static/tailwind/docs/themes/orange.css +111 -0
- package/dist/static/tailwind/docs/themes/pink.css +111 -0
- package/dist/static/tailwind/docs/themes/purple.css +111 -0
- package/dist/static/tailwind/docs/themes/red.css +111 -0
- package/dist/static/tailwind/docs/themes/rose.css +111 -0
- package/dist/static/tailwind/docs/themes/sky.css +111 -0
- package/dist/static/tailwind/docs/themes/teal.css +111 -0
- package/dist/static/tailwind/docs/themes/violet.css +111 -0
- package/dist/static/tailwind/docs/themes/yellow.css +111 -0
- package/dist/static/tailwind/docs/themes.css +84 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
- package/dist/static/tailwind/fonts/all.css +6 -0
- package/dist/static/tailwind/fonts/config.jsonc +31 -0
- package/dist/static/tailwind/fonts/geist-mono.css +142 -0
- package/dist/static/tailwind/fonts/geist.css +142 -0
- package/dist/static/tailwind/fonts/inter.css +215 -0
- package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
- package/dist/static/tailwind/fonts/noto-sans.css +134 -0
- package/dist/static/tailwind/fonts/open-sans.css +89 -0
- package/dist/static/tailwind/highlighters.css +14 -0
- package/dist/static/tailwind/hljs/README.md +53 -0
- package/dist/static/tailwind/hljs/a11y.css +96 -0
- package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
- package/dist/static/tailwind/hljs/atom-one.css +164 -0
- package/dist/static/tailwind/hljs/default.css +164 -0
- package/dist/static/tailwind/hljs/github.css +101 -0
- package/dist/static/tailwind/hljs/google-code.css +112 -0
- package/dist/static/tailwind/hljs/grayscale.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
- package/dist/static/tailwind/hljs/papercolor.css +120 -0
- package/dist/static/tailwind/hljs/qt-creator.css +91 -0
- package/dist/static/tailwind/hljs/summerfruit.css +115 -0
- package/dist/static/tailwind/hljs/tomorrow.css +115 -0
- package/dist/static/tailwind/leed.css +170 -0
- package/dist/static/tailwind/site-tokens.reference.css +105 -0
- package/dist/static/tailwind/utils/icons.css +102 -0
- package/dist/static/tailwind/utils/utilities.css +105 -0
- package/dist/static/templates/pages/PAGE.hbs +27 -0
- package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
- package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
- package/dist/static/templates/raw-content/.ci/build.sh +58 -0
- package/dist/static/templates/raw-content/README.md +179 -0
- package/dist/static/templates/raw-content/_gitignore +50 -0
- package/dist/static/templates/raw-content/src/404.hbs +21 -0
- package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
- package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
- package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
- package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
- package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
- package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
- package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/raw-content/src/index.hbs +21 -0
- package/dist/static/templates/raw-content/src/robots.hbs +9 -0
- package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
- package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
- package/dist/static/templates/sandbox/_gitignore +50 -0
- package/dist/static/templates/sandbox/identity.json +1 -0
- package/dist/static/templates/sandbox/src/404.hbs +21 -0
- package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
- package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
- package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
- package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
- package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
- package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
- package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
- package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/sandbox/src/index.hbs +20 -0
- package/dist/static/templates/sandbox/src/robots.hbs +9 -0
- package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
- package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
- package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
- package/dist/thrown-sukVkp_1.js +113 -0
- package/dist/typescript-D4T0V_Hn.js +23004 -0
- package/dist/yaml-9wR8GxF_.js +5236 -0
- package/package.json +157 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { DocResource, Env } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared ASSETS content-hydration helper (#284, #294).
|
|
6
|
+
*
|
|
7
|
+
* "path present + content absent" in a `resources[]` envelope is the hydration
|
|
8
|
+
* contract: customer-functions emits the path + canonical url and leaves
|
|
9
|
+
* `content` absent, and the edge fills it from the per-company ASSETS bundle —
|
|
10
|
+
* no origin hop. This is the SINGLE implementation used by BOTH:
|
|
11
|
+
* - `mcp-proxy.ts` (Docs MCP #284), which unwraps the JSON-RPC tool result and
|
|
12
|
+
* hydrates the inner `{ resources }` envelope, and
|
|
13
|
+
* - `proxy.ts proxyToFunctions` (core-site agent surface #294), which hydrates
|
|
14
|
+
* the plain `{ resources }` JSON body the `/api/agent/pages-*` endpoints
|
|
15
|
+
* return.
|
|
16
|
+
* Both surfaces share the one `X-Fetch-Result: page-resources` marker and THIS
|
|
17
|
+
* one hydrator (documentation, api, and posts are all just pages), so they can
|
|
18
|
+
* never drift.
|
|
19
|
+
*
|
|
20
|
+
* Fetches run in parallel (`Promise.allSettled`), preserve input order, and
|
|
21
|
+
* tolerate partial failure (per-item `error`).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Fetch each resource's content from ASSETS in parallel, preserving order. */
|
|
25
|
+
export async function hydrateResources(
|
|
26
|
+
c: Context<{ Bindings: Env }>,
|
|
27
|
+
resources: DocResource[],
|
|
28
|
+
): Promise<DocResource[]> {
|
|
29
|
+
const settled = await Promise.allSettled(resources.map((r) => hydrateOne(c, r)));
|
|
30
|
+
return settled.map((outcome, i) =>
|
|
31
|
+
outcome.status === "fulfilled"
|
|
32
|
+
? outcome.value
|
|
33
|
+
: { ...resources[i], error: `hydration failed: ${String(outcome.reason)}` },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the ASSETS file for a resource. A path ending in `/` is a page
|
|
39
|
+
* directory → `index.md`; a file path (e.g. `.../openapi.yaml`) is fetched
|
|
40
|
+
* directly. A resource that already carries an `error` (a per-item marker — e.g.
|
|
41
|
+
* `not_found` / `protected_resource`) or no `path` is passed through untouched, so
|
|
42
|
+
* markers are never hydrated. Paths come from the trusted resource envelope
|
|
43
|
+
* (never raw client input), so no client-controlled traversal is possible.
|
|
44
|
+
*/
|
|
45
|
+
async function hydrateOne(c: Context<{ Bindings: Env }>, resource: DocResource): Promise<DocResource> {
|
|
46
|
+
if (resource.error || !resource.path) return resource;
|
|
47
|
+
|
|
48
|
+
const filePath = resource.path.endsWith("/") ? `${resource.path}index.md` : resource.path;
|
|
49
|
+
const assetUrl = new URL(filePath, c.req.url).toString();
|
|
50
|
+
const res = await c.env.ASSETS.fetch(new Request(assetUrl));
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
return { ...resource, error: `not found: ${filePath}` };
|
|
53
|
+
}
|
|
54
|
+
return { ...resource, content: await res.text() };
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the agent-surface asset layout (#294).
|
|
3
|
+
*
|
|
4
|
+
* Lives in the worker because the worker is the constrained side: it is a
|
|
5
|
+
* standalone hono-only bundle (the build copies its `src/` + an isolated
|
|
6
|
+
* package.json with only `hono` into the customer site repo) and so CANNOT import
|
|
7
|
+
* `@leed/*` or anything from site-management `src/`. The build side has no such
|
|
8
|
+
* limit, so `src/utils/agentManifests.ts` imports these same constants from here
|
|
9
|
+
* (site-management's tsconfig lists this one file) — the build writes
|
|
10
|
+
* `<site>/_services/{webmcp,agent}.json` and the edge hydrates the exact same
|
|
11
|
+
* paths from ASSETS, with no risk of drift.
|
|
12
|
+
*/
|
|
13
|
+
export const AGENT_SERVICES_DIR = "_services";
|
|
14
|
+
export const WEBMCP_FILE = "webmcp.json";
|
|
15
|
+
export const AGENT_CARD_FILE = "agent.json";
|
|
16
|
+
|
|
17
|
+
/** ASSETS path the edge fetches the WebMCP manifest from. */
|
|
18
|
+
export const WEBMCP_ASSET_PATH = `/${AGENT_SERVICES_DIR}/${WEBMCP_FILE}`;
|
|
19
|
+
/** ASSETS path the edge fetches the A2A AgentCard from. */
|
|
20
|
+
export const AGENT_CARD_ASSET_PATH = `/${AGENT_SERVICES_DIR}/${AGENT_CARD_FILE}`;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Context, Next } from "hono";
|
|
2
|
+
import { isMcpProxyPath } from "./mcp-proxy";
|
|
3
|
+
import type { Env } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The core-site agent surfaces (#294): `/.well-known/webmcp` (WebMCP manifest)
|
|
7
|
+
* and `/.well-known/agent.json` (A2A AgentCard). Served by the edge for
|
|
8
|
+
* cross-origin in-browser + remote agents, so — like the Docs MCP surface — they
|
|
9
|
+
* need the public `Access-Control-Allow-Origin: *` policy, NOT the credentialed
|
|
10
|
+
* origin-locked one. Read-only public metadata, so `*` is safe.
|
|
11
|
+
*/
|
|
12
|
+
function isAgentWellKnownPath(pathname: string): boolean {
|
|
13
|
+
return pathname === "/.well-known/webmcp" || pathname === "/.well-known/agent.json";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseOrigins(value: string): string[] {
|
|
17
|
+
try {
|
|
18
|
+
return Array.isArray(value) ? value : JSON.parse(value);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Public CORS for the Docs MCP surface (#284), owned ENTIRELY at the edge so
|
|
26
|
+
* customer-functions is never hit for a preflight. `/mcp/*` + the OAuth
|
|
27
|
+
* `.well-known` paths serve cross-origin MCP clients with bearer/PKCE (NO
|
|
28
|
+
* cookies), so the correct, safe policy is `Access-Control-Allow-Origin: *` —
|
|
29
|
+
* NOT the origin-locked credentialed policy below. OPTIONS preflights are
|
|
30
|
+
* answered here (204) and short-circuited before the proxy, and CORS headers are
|
|
31
|
+
* stamped onto every proxied response. WWW-Authenticate is exposed so clients can
|
|
32
|
+
* walk discovery from the 401 challenge.
|
|
33
|
+
*
|
|
34
|
+
* Methods cover the stateless Streamable-HTTP surface: POST (JSON-RPC), GET
|
|
35
|
+
* (discovery + 401 challenge), OPTIONS (preflight). DELETE is intentionally
|
|
36
|
+
* absent — this MCP transport is stateless (no session id, no
|
|
37
|
+
* session-termination DELETE), so advertising it would be inaccurate. Allowed
|
|
38
|
+
* headers cover the MCP transport headers plus the OAuth bearer / JSON body.
|
|
39
|
+
*
|
|
40
|
+
* The header list matches the `allowHeaders` of `mcpCors()` in the CMS repo's
|
|
41
|
+
* `libs/mcp/src/cors.ts` exactly: protocol revision `2026-07-28` makes
|
|
42
|
+
* `mcp-method`/`mcp-name` required, so a preflight that omitted them would fail
|
|
43
|
+
* every browser Docs MCP client. `mcp-session-id` is absent because this
|
|
44
|
+
* transport is stateless — no session id is ever read, minted, or echoed.
|
|
45
|
+
*
|
|
46
|
+
* This worker ships baked into each customer site at *site-build* time, not with
|
|
47
|
+
* the customer-functions deploy, so a site built before this release fronts the
|
|
48
|
+
* new server with an allow-list that lacks `mcp-method`/`mcp-name`, and every
|
|
49
|
+
* browser Docs MCP client on it fails preflight until that site is rebuilt.
|
|
50
|
+
*/
|
|
51
|
+
async function mcpCors(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | undefined> {
|
|
52
|
+
if (c.req.method === "OPTIONS") {
|
|
53
|
+
return c.body(null, 204, {
|
|
54
|
+
"Access-Control-Allow-Origin": "*",
|
|
55
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
56
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type, mcp-protocol-version, mcp-method, mcp-name",
|
|
57
|
+
"Access-Control-Max-Age": "86400",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
await next();
|
|
61
|
+
c.res = new Response(c.res.body, c.res);
|
|
62
|
+
c.res.headers.set("Access-Control-Allow-Origin", "*");
|
|
63
|
+
c.res.headers.append("Access-Control-Expose-Headers", "WWW-Authenticate");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Public `*` CORS for the read-only agent `.well-known` documents (#294). These
|
|
68
|
+
* are plain GETs (no auth, no MCP transport headers), so the preflight advertises
|
|
69
|
+
* only GET/OPTIONS and the response is stamped with `Access-Control-Allow-Origin: *`.
|
|
70
|
+
*/
|
|
71
|
+
async function agentCors(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | undefined> {
|
|
72
|
+
if (c.req.method === "OPTIONS") {
|
|
73
|
+
return c.body(null, 204, {
|
|
74
|
+
"Access-Control-Allow-Origin": "*",
|
|
75
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
76
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
77
|
+
"Access-Control-Max-Age": "86400",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
await next();
|
|
81
|
+
c.res = new Response(c.res.body, c.res);
|
|
82
|
+
c.res.headers.set("Access-Control-Allow-Origin", "*");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function corsMiddleware(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | undefined> {
|
|
86
|
+
const pathname = new URL(c.req.url).pathname;
|
|
87
|
+
if (isMcpProxyPath(pathname)) {
|
|
88
|
+
return mcpCors(c, next);
|
|
89
|
+
}
|
|
90
|
+
if (isAgentWellKnownPath(pathname)) {
|
|
91
|
+
return agentCors(c, next);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const origin = c.req.header("Origin") ?? "";
|
|
95
|
+
|
|
96
|
+
if (origin === "" || origin === "null") {
|
|
97
|
+
await next();
|
|
98
|
+
// `@font-face` requests from a CSP-sandboxed iframe (page-proxy preview)
|
|
99
|
+
// carry `Origin: null`. Without ACAO the browser rejects the response and
|
|
100
|
+
// icon fonts fail to render. /static/* assets are public, so `*` is safe.
|
|
101
|
+
if (origin === "null" && new URL(c.req.url).pathname.startsWith("/static/")) {
|
|
102
|
+
c.res = new Response(c.res.body, c.res);
|
|
103
|
+
c.res.headers.set("Access-Control-Allow-Origin", "*");
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const validOrigins = parseOrigins(c.env.VALID_ORIGINS);
|
|
109
|
+
const matched = validOrigins.find((o) => o.includes(origin));
|
|
110
|
+
|
|
111
|
+
if (!matched) {
|
|
112
|
+
return c.text(`Access Forbidden (CORS): '${origin}'`, 403, {
|
|
113
|
+
"Access-Control-Allow-Origin": validOrigins[0] ?? "",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await next();
|
|
118
|
+
c.res = new Response(c.res.body, c.res);
|
|
119
|
+
c.res.headers.append("Access-Control-Allow-Origin", origin);
|
|
120
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer Site Worker — replaces Cloudflare Pages Functions routing.
|
|
3
|
+
*
|
|
4
|
+
* Routes API, file, shortcode, and email requests to the customer-functions
|
|
5
|
+
* service binding. Serves static assets via the ASSETS binding for all
|
|
6
|
+
* other paths.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { AGENT_CARD_ASSET_PATH, WEBMCP_ASSET_PATH } from "./constants";
|
|
10
|
+
import { corsMiddleware } from "./cors";
|
|
11
|
+
import { proxyMcp } from "./mcp-proxy";
|
|
12
|
+
import { proxyToFunctions } from "./proxy";
|
|
13
|
+
import { DOCS_LIVE_SEARCH_MIN_TIER, meetsTier } from "./tier";
|
|
14
|
+
import type { Env } from "./types";
|
|
15
|
+
|
|
16
|
+
const app = new Hono<{ Bindings: Env }>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Host-scoped redirect on the workers.dev URL (#266).
|
|
20
|
+
*
|
|
21
|
+
* When `REDIRECT_TO_PRIMARY_DOMAIN` is "true" — emitted on public builds
|
|
22
|
+
* once the custom domain becomes active — any request that hits the Worker
|
|
23
|
+
* via its `*.workers.dev` hostname is 301'd to `PRIMARY_DOMAIN`, except
|
|
24
|
+
* CMS page-proxy traffic identified by the
|
|
25
|
+
* `X-Leed-Proxy-Request: <COMPANY_ID>` header.
|
|
26
|
+
*
|
|
27
|
+
* Why workers.dev stays on at all: the CMS page-proxy fetches the
|
|
28
|
+
* workers.dev hostname directly so a leed.ai-zone Worker can reach the
|
|
29
|
+
* customer site without triggering Cloudflare's same-zone loop detection
|
|
30
|
+
* (522). The header is the gate that distinguishes proxy traffic from
|
|
31
|
+
* casual workers.dev hits. The companyId match gives weak per-tenant
|
|
32
|
+
* scoping (defense-in-depth against CMS routing bugs); the protected
|
|
33
|
+
* content is already public on the canonical domain, so a stronger
|
|
34
|
+
* shared-secret rotation pipeline would be overkill.
|
|
35
|
+
*
|
|
36
|
+
* If `REDIRECT_TO_PRIMARY_DOMAIN !== "true"` the gate is inert — covers
|
|
37
|
+
* the pre-custom-domain window when workers.dev is the customer's only
|
|
38
|
+
* public face, and preview builds (which disable workers.dev outright).
|
|
39
|
+
*/
|
|
40
|
+
app.use(async (c, next) => {
|
|
41
|
+
if (c.env.REDIRECT_TO_PRIMARY_DOMAIN === "true") {
|
|
42
|
+
const url = new URL(c.req.url);
|
|
43
|
+
if (url.hostname.endsWith(".workers.dev")) {
|
|
44
|
+
if (c.req.header("X-Leed-Proxy-Request") !== c.env.COMPANY_ID) {
|
|
45
|
+
return c.redirect(`https://${c.env.PRIMARY_DOMAIN}${url.pathname}${url.search}`, 301);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await next();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.use(corsMiddleware);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Stamp X-Site-Version and X-Preview on every response — assets, API,
|
|
56
|
+
* and errors alike. Re-wraps c.res because Responses returned from fetch
|
|
57
|
+
* (ASSETS and FUNCTIONS service bindings) have immutable headers.
|
|
58
|
+
*/
|
|
59
|
+
app.use(async (c, next) => {
|
|
60
|
+
await next();
|
|
61
|
+
c.res = new Response(c.res.body, c.res);
|
|
62
|
+
c.res.headers.append("X-Site-Version", c.env.SITE_VERSION);
|
|
63
|
+
c.res.headers.append("X-Preview", String(c.env.PREVIEW_SITE));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Global error handler — logs and returns a JSON error. The middleware
|
|
68
|
+
* above stamps site headers on the error response too.
|
|
69
|
+
*/
|
|
70
|
+
app.onError((err, c) => {
|
|
71
|
+
console.error(`Worker error: ${err.message}`);
|
|
72
|
+
return c.json({ error: err.message }, 500);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Preview guard: block event tracking and form fills on preview sites.
|
|
77
|
+
* Log the request body for debugging, then return 204.
|
|
78
|
+
*/
|
|
79
|
+
app.post("/api/event", async (c) => {
|
|
80
|
+
if (c.env.PREVIEW_SITE) {
|
|
81
|
+
const body = await c.req.text();
|
|
82
|
+
console.log(`Preview event request: ${body}`);
|
|
83
|
+
return c.body(null, 204);
|
|
84
|
+
}
|
|
85
|
+
return proxyToFunctions(c);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
app.post("/api/clerk", async (c) => {
|
|
89
|
+
if (c.env.PREVIEW_SITE) {
|
|
90
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
91
|
+
let body: string;
|
|
92
|
+
if (contentType.includes("form-data")) {
|
|
93
|
+
const form = await c.req.formData();
|
|
94
|
+
body = JSON.stringify(Object.fromEntries(form.entries()));
|
|
95
|
+
} else {
|
|
96
|
+
body = await c.req.text();
|
|
97
|
+
}
|
|
98
|
+
console.log(`Preview clerk request (${contentType}): ${body}`);
|
|
99
|
+
return c.body(null, 204);
|
|
100
|
+
}
|
|
101
|
+
return proxyToFunctions(c);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Docs MCP (#284): proxy `/mcp/*` and the OAuth `.well-known` discovery paths to
|
|
106
|
+
* customer-functions (which owns the protocol, auth, and logging), then hydrate
|
|
107
|
+
* content-bearing tool responses from ASSETS. Disabled on preview sites —
|
|
108
|
+
* the visitor MCP is a published-site feature, consistent with the event/clerk
|
|
109
|
+
* preview guards above.
|
|
110
|
+
*
|
|
111
|
+
* Per-customer entitlement is enforced HERE at the edge: when `MCP_ENABLED` is
|
|
112
|
+
* false (baked from the company's `entitlements.mcp` by the wrangler
|
|
113
|
+
* generator) the surface short-circuits with a 403 and never proxies to
|
|
114
|
+
* customer-functions. customer-functions keeps its own `entitlements.mcp` gate
|
|
115
|
+
* as defense-in-depth.
|
|
116
|
+
*/
|
|
117
|
+
const mcpHandler = (c: Parameters<typeof proxyMcp>[0]) => {
|
|
118
|
+
if (c.env.PREVIEW_SITE) return c.body(null, 404);
|
|
119
|
+
if (!c.env.MCP_ENABLED) {
|
|
120
|
+
return c.json({ error: "mcp_not_enabled", message: "MCP not enabled, upgrade service" }, 403);
|
|
121
|
+
}
|
|
122
|
+
return proxyMcp(c);
|
|
123
|
+
};
|
|
124
|
+
app.all("/mcp", mcpHandler);
|
|
125
|
+
app.all("/mcp/*", mcpHandler);
|
|
126
|
+
app.get("/.well-known/oauth-protected-resource", mcpHandler);
|
|
127
|
+
// RFC 9728 §3.1 path-inserted location for the `/mcp` resource identifier — the
|
|
128
|
+
// URL an MCP client derives and requests FIRST. Routing only the bare path above
|
|
129
|
+
// is the defect that made Operator MCP unusable (#348): with no
|
|
130
|
+
// protected-resource metadata the client omits the RFC 8707 `resource`
|
|
131
|
+
// indicator, the AS mints an audience-less token, and every request is rejected
|
|
132
|
+
// in an authorize-retry loop. `isMcpProxyPath` (used by CORS) knows this path
|
|
133
|
+
// too, but the predicate is not the router — this registration is what forwards
|
|
134
|
+
// it (#349).
|
|
135
|
+
app.get("/.well-known/oauth-protected-resource/mcp", mcpHandler);
|
|
136
|
+
app.get("/.well-known/oauth-authorization-server", mcpHandler);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Core-site agent surfaces (#294): serve the pre-rendered WebMCP manifest and
|
|
140
|
+
* A2A AgentCard. site-management renders them at build time from the shared
|
|
141
|
+
* capability definition (single source of truth) and writes them into the
|
|
142
|
+
* static asset bundle under `_services/`. They are fetched from the ASSETS
|
|
143
|
+
* binding rather than baked into wrangler vars because the WebMCP manifest
|
|
144
|
+
* exceeds the 5.1 kB text-binding limit (a 10 kB manifest fails the version
|
|
145
|
+
* upload). Read-only / ungated content (it describes already-public pages),
|
|
146
|
+
* but still per-site opt-in: disabled on preview sites and 404 when
|
|
147
|
+
* `MCP_ENABLED` is false (the same gate as the Docs MCP surface). CORS (`*`)
|
|
148
|
+
* is applied by the edge `corsMiddleware` for these `.well-known` paths.
|
|
149
|
+
*
|
|
150
|
+
* The discovery path stays gated by `MCP_ENABLED`, and the `_services/*` files
|
|
151
|
+
* are only written when the entitlement is on — so a disabled build has nothing
|
|
152
|
+
* to serve at either path. The raw `_services/*` files are an implementation
|
|
153
|
+
* detail; they only ever mirror content that is already public.
|
|
154
|
+
*/
|
|
155
|
+
async function serveAgentDocument(c: Parameters<typeof proxyMcp>[0], assetPath: string): Promise<Response> {
|
|
156
|
+
if (c.env.PREVIEW_SITE) return c.body(null, 404);
|
|
157
|
+
if (!c.env.MCP_ENABLED) return c.body(null, 404);
|
|
158
|
+
const res = await c.env.ASSETS.fetch(new Request(new URL(assetPath, c.req.url)));
|
|
159
|
+
if (!res.ok) return c.body(null, 404);
|
|
160
|
+
return c.body(await res.text(), 200, { "content-type": "application/json" });
|
|
161
|
+
}
|
|
162
|
+
app.get("/.well-known/webmcp", (c) => serveAgentDocument(c, WEBMCP_ASSET_PATH));
|
|
163
|
+
app.get("/.well-known/agent.json", (c) => serveAgentDocument(c, AGENT_CARD_ASSET_PATH));
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Remaining API routes proxied to customer-functions via service binding.
|
|
167
|
+
* /api/lid — POST session initialization (fingerprint, screen size)
|
|
168
|
+
* /api/abby/* — GET page recommendations (vectorize similarity)
|
|
169
|
+
* /api/sway/* — GET call-to-action suggestions (vectorize similarity)
|
|
170
|
+
* /api/poirot — GET known user lookup (lead identification)
|
|
171
|
+
* /api/agent/* — GET core-site agent surfaces (#294): the read-only tools
|
|
172
|
+
* (search_site, get_site_overview, get_pages_by_id,
|
|
173
|
+
* get_pages_by_path, list_recent_posts) that back the WebMCP
|
|
174
|
+
* manifest + A2A AgentCard. No dedicated edge route — they fall
|
|
175
|
+
* through to this catch-all proxy, then customer-functions enforces
|
|
176
|
+
* the `entitlements.mcp` gate and the posts-only scope guard. The
|
|
177
|
+
* two batch content endpoints return `X-Fetch-Result: page-resources`,
|
|
178
|
+
* which `proxyToFunctions` honors to hydrate each post's `content`
|
|
179
|
+
* from ASSETS (shared `assetsHydration` helper). CORS (`*`) is
|
|
180
|
+
* applied by the edge `corsMiddleware` above.
|
|
181
|
+
*/
|
|
182
|
+
/**
|
|
183
|
+
* Live docs search (#329): reject an unentitled site's search traffic AT THE
|
|
184
|
+
* EDGE, the same shape as the `MCP_ENABLED` gate above, so it never crosses the
|
|
185
|
+
* service binding and never costs customer-functions a KV company read.
|
|
186
|
+
*
|
|
187
|
+
* 404, not the MCP surface's 403: this endpoint is reached from a reader's
|
|
188
|
+
* browser, so it stays indistinguishable from "does not exist" rather than
|
|
189
|
+
* advertising a paid feature to scanners — and it matches what
|
|
190
|
+
* customer-functions itself returns, so the two gates are not tellable apart.
|
|
191
|
+
*
|
|
192
|
+
* An OPTIMIZATION, never the authority. customer-functions re-reads the tier
|
|
193
|
+
* from KV on every request, which is what makes "404s for a downgraded site
|
|
194
|
+
* WITHOUT a rebuild" hold. Hence `=== false`, never falsiness: a worker
|
|
195
|
+
* generated before this var existed must DEFER to that authority instead of
|
|
196
|
+
* 404ing its own search.
|
|
197
|
+
*/
|
|
198
|
+
const searchHandler = (c: Parameters<typeof proxyToFunctions>[0]) => {
|
|
199
|
+
// Absent tier (a worker generated before ENTITLEMENT_TIER existed) defers to
|
|
200
|
+
// customer-functions rather than 404ing its own search. Present-but-below
|
|
201
|
+
// fails closed, matching `hasFeature`.
|
|
202
|
+
const tier = c.env.ENTITLEMENT_TIER;
|
|
203
|
+
if (tier !== undefined && !meetsTier(tier, DOCS_LIVE_SEARCH_MIN_TIER)) {
|
|
204
|
+
return c.json({ error: "not_found" }, 404);
|
|
205
|
+
}
|
|
206
|
+
return proxyToFunctions(c);
|
|
207
|
+
};
|
|
208
|
+
app.all("/api/search", searchHandler);
|
|
209
|
+
app.all("/api/search/*", searchHandler);
|
|
210
|
+
|
|
211
|
+
app.all("/api/*", proxyToFunctions);
|
|
212
|
+
|
|
213
|
+
/** /f/:assetId/:fileName — GET file/document downloads from R2 */
|
|
214
|
+
app.all("/f/*", proxyToFunctions);
|
|
215
|
+
|
|
216
|
+
/** /s/:code — GET shortcode redirect (URL shortener) */
|
|
217
|
+
app.all("/s/*", proxyToFunctions);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* /e/i/:batchId/:hash.dest — GET email open tracking (1x1 pixel)
|
|
221
|
+
* /e/u/:batchId/:hash.dest — GET email URL click tracking (redirect)
|
|
222
|
+
* /e/f/:batchId/:hash.dest — GET email file click tracking (redirect)
|
|
223
|
+
* /e/o/:batchId/:hash.dest — GET email opt-out (unsubscribe form or process)
|
|
224
|
+
*/
|
|
225
|
+
app.all("/e/*", proxyToFunctions);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Asset fallback for paths the asset binding routes through the Worker via
|
|
229
|
+
* `run_worker_first` (the homepage and any URL ending in `/`, see
|
|
230
|
+
* wranglerGenerator). Without this, those page navigations would hit Hono's
|
|
231
|
+
* default 404 instead of being served from the static asset bundle.
|
|
232
|
+
*
|
|
233
|
+
* Uses `app.notFound` rather than `app.all("*", …)` because the wildcard
|
|
234
|
+
* form competes with the `/api/*`, `/f/*`, `/s/*`, `/e/*` proxy routes:
|
|
235
|
+
* non-GET requests would land on the asset binding, which only supports
|
|
236
|
+
* GET/HEAD and returns 405 for everything else (POST /api/event, etc.).
|
|
237
|
+
* `notFound` only fires when no other route matches, preserving the proxy
|
|
238
|
+
* routes above. (#266)
|
|
239
|
+
*/
|
|
240
|
+
app.notFound((c) => c.env.ASSETS.fetch(c.req.raw));
|
|
241
|
+
|
|
242
|
+
export default app;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { hydrateResources } from "./assetsHydration";
|
|
3
|
+
import { buildProxyHeaders, fetchFunctions } from "./proxyHeaders";
|
|
4
|
+
import type { DocResourcesPayload, Env } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Docs MCP proxy + hydration layer (#284).
|
|
8
|
+
*
|
|
9
|
+
* Proxies `/mcp/*` and the OAuth `.well-known` paths to customer-functions,
|
|
10
|
+
* injecting the trusted `companyId` header. Auth/protocol responses pass through
|
|
11
|
+
* untouched. On content-bearing JSON-RPC responses (marked
|
|
12
|
+
* `X-Fetch-Result: page-resources`), the referenced `.md`/`.yml` files are
|
|
13
|
+
* fetched directly from the ASSETS binding — no origin hop — and filled into each
|
|
14
|
+
* `DocResource.content` via the shared `assetsHydration` helper (the SAME helper
|
|
15
|
+
* `proxy.ts proxyToFunctions` uses for the core-site agent surface #294, sharing
|
|
16
|
+
* the one `page-resources` marker so the two never drift).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** True for the Docs MCP paths this worker proxies. */
|
|
20
|
+
export function isMcpProxyPath(pathname: string): boolean {
|
|
21
|
+
return (
|
|
22
|
+
pathname === "/mcp" ||
|
|
23
|
+
pathname.startsWith("/mcp/") ||
|
|
24
|
+
pathname === "/.well-known/oauth-protected-resource" ||
|
|
25
|
+
// RFC 9728 §3.1 path-inserted location for the `/mcp` resource identifier.
|
|
26
|
+
// Clients request this one FIRST; if the proxy does not forward it, fixing
|
|
27
|
+
// the origin alone leaves Docs MCP broken from the client's view (#349).
|
|
28
|
+
pathname === "/.well-known/oauth-protected-resource/mcp" ||
|
|
29
|
+
pathname === "/.well-known/oauth-authorization-server"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function proxyMcp(c: Context<{ Bindings: Env }>): Promise<Response> {
|
|
34
|
+
const headers = buildProxyHeaders(c);
|
|
35
|
+
|
|
36
|
+
// Buffer the request body ONCE into an ArrayBuffer instead of forwarding the
|
|
37
|
+
// one-time-use stream (`c.req.raw.body`). MCP/OAuth request payloads are small
|
|
38
|
+
// JSON/form bodies, so buffering is cheap and makes the body retransmittable.
|
|
39
|
+
//
|
|
40
|
+
// `redirect: "manual"` is required: customer-functions answers the OAuth
|
|
41
|
+
// authorize grant with a 302 to the client `redirect_uri`. Even in manual mode
|
|
42
|
+
// workerd validates that a streamed (one-time-use) body could be replayed and
|
|
43
|
+
// throws ("one-time-use body … requiring the body to be retransmitted") on the
|
|
44
|
+
// 302; a buffered body sidesteps that entirely. Manual mode still returns the
|
|
45
|
+
// 302 untouched so the BROWSER completes the OAuth handoff.
|
|
46
|
+
const body = c.req.raw.body ? await c.req.arrayBuffer() : undefined;
|
|
47
|
+
const response = await fetchFunctions(c, {
|
|
48
|
+
method: c.req.method,
|
|
49
|
+
headers,
|
|
50
|
+
body,
|
|
51
|
+
redirect: "manual",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Only content-bearing tool responses are hydrated. Redirects (the OAuth 302)
|
|
55
|
+
// and auth/protocol/discovery responses (no page-resources marker) pass through
|
|
56
|
+
// untouched, with Location/Set-Cookie intact.
|
|
57
|
+
if (response.status >= 300 && response.status < 400) return response;
|
|
58
|
+
if (response.headers.get("X-Fetch-Result") !== "page-resources") return response;
|
|
59
|
+
|
|
60
|
+
return hydrateMcpResponse(c, response);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RpcItem {
|
|
64
|
+
result?: { content?: Array<{ type: string; text: string }> };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Hydrate the JSON-RPC response when its tool result's text payload is a
|
|
69
|
+
* `{ resources }` envelope, then re-serialize. Non-content results are left
|
|
70
|
+
* as-is. Always a single message — the 2026-07-28 revision forbids batching, so
|
|
71
|
+
* a batch request is rejected upstream and never reaches hydration.
|
|
72
|
+
*/
|
|
73
|
+
async function hydrateMcpResponse(c: Context<{ Bindings: Env }>, response: Response): Promise<Response> {
|
|
74
|
+
const payload = (await response.json()) as RpcItem;
|
|
75
|
+
|
|
76
|
+
await hydrateRpcItem(c, payload);
|
|
77
|
+
|
|
78
|
+
const headers = new Headers(response.headers);
|
|
79
|
+
headers.delete("X-Fetch-Result");
|
|
80
|
+
return new Response(JSON.stringify(payload), { status: response.status, headers });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Hydrate one JSON-RPC result in place, if it carries a page-resources envelope. */
|
|
84
|
+
async function hydrateRpcItem(c: Context<{ Bindings: Env }>, item: RpcItem): Promise<void> {
|
|
85
|
+
const block = item?.result?.content?.[0];
|
|
86
|
+
if (block?.type !== "text" || typeof block.text !== "string") return;
|
|
87
|
+
|
|
88
|
+
let parsed: DocResourcesPayload;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(block.text);
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(parsed.resources)) return;
|
|
95
|
+
|
|
96
|
+
parsed.resources = await hydrateResources(c, parsed.resources);
|
|
97
|
+
block.text = JSON.stringify(parsed);
|
|
98
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
import { hydrateResources } from "./assetsHydration";
|
|
3
|
+
import { buildProxyHeaders, fetchFunctions } from "./proxyHeaders";
|
|
4
|
+
import type { DocResourcesPayload, Env } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Forwards the request to customer-functions (the `FUNCTIONS` service binding,
|
|
8
|
+
* or the `FUNCTIONS_ORIGIN` dev override) with context headers injected
|
|
9
|
+
* (companyId, preview, etc.). Handles two `X-Fetch-Result` response patterns:
|
|
10
|
+
* `html` (the page-proxy combine) and `page-resources` (the core-site agent
|
|
11
|
+
* surface #294 — fill each post's `content` from ASSETS via the shared hydrator).
|
|
12
|
+
*/
|
|
13
|
+
export async function proxyToFunctions(c: Context<{ Bindings: Env }>): Promise<Response> {
|
|
14
|
+
const headers = buildProxyHeaders(c);
|
|
15
|
+
|
|
16
|
+
// Buffer the request body ONCE (small JSON/form payloads) so it is
|
|
17
|
+
// retransmittable. Forwarding the one-time-use stream (`c.req.raw.body`) makes
|
|
18
|
+
// workerd throw on any redirect because the consumed body cannot be replayed.
|
|
19
|
+
//
|
|
20
|
+
// `redirect: "manual"`: never follow a 3xx from the origin. Following would
|
|
21
|
+
// try to replay the request body and throw; instead the redirect is returned
|
|
22
|
+
// to the browser with its Location header intact.
|
|
23
|
+
const body = c.req.raw.body ? await c.req.arrayBuffer() : undefined;
|
|
24
|
+
const response = await fetchFunctions(c, {
|
|
25
|
+
method: c.req.method,
|
|
26
|
+
headers,
|
|
27
|
+
body,
|
|
28
|
+
redirect: "manual",
|
|
29
|
+
});
|
|
30
|
+
return handleFetchResult(c, response);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Dispatch the response's `X-Fetch-Result` content pattern:
|
|
35
|
+
* - `page-resources`: a `{ resources }` JSON envelope whose `content` fields are
|
|
36
|
+
* hydrated from ASSETS (the core-site agent surface #294 — the same
|
|
37
|
+
* `page-resources` contract the Docs MCP uses, via the shared `assetsHydration`
|
|
38
|
+
* helper).
|
|
39
|
+
* - `html`: a JSON array of URLs whose HTML fragments are fetched + combined.
|
|
40
|
+
* Anything else passes through untouched.
|
|
41
|
+
*/
|
|
42
|
+
async function handleFetchResult(c: Context<{ Bindings: Env }>, response: Response): Promise<Response> {
|
|
43
|
+
const fetchResult = response.headers.get("X-Fetch-Result");
|
|
44
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
45
|
+
|
|
46
|
+
if (fetchResult === "page-resources" && contentType.includes("application/json")) {
|
|
47
|
+
return hydratePageResources(c, response);
|
|
48
|
+
}
|
|
49
|
+
if (fetchResult !== "html" || !contentType.includes("application/json")) {
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const urls: string[] = await response.json();
|
|
54
|
+
console.log(`Fetching ${urls.length} HTML fragments: ${urls.join(", ")}`);
|
|
55
|
+
|
|
56
|
+
const fragments = await Promise.all(
|
|
57
|
+
urls.map(async (url) => {
|
|
58
|
+
try {
|
|
59
|
+
const res = await c.env.ASSETS.fetch(new Request(url));
|
|
60
|
+
return res.ok ? await res.text() : "";
|
|
61
|
+
} catch {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const body = fragments.filter((f) => f !== "").join("");
|
|
68
|
+
return new Response(body, {
|
|
69
|
+
status: 200,
|
|
70
|
+
headers: { "content-type": "text/html" },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Hydrate a core-site agent `{ resources }` JSON envelope (#294). Unlike the Docs
|
|
76
|
+
* MCP (a nested JSON-RPC tool result, unwrapped in `mcp-proxy.ts`), these
|
|
77
|
+
* `/api/agent/pages-*` responses are a plain top-level `{ resources }` body — each
|
|
78
|
+
* resource's `content` is filled from ASSETS by the shared hydrator, then the
|
|
79
|
+
* envelope is re-serialized. The `X-Fetch-Result` marker is stripped from the
|
|
80
|
+
* re-emitted response. A non-`{ resources }` body passes through untouched.
|
|
81
|
+
*/
|
|
82
|
+
async function hydratePageResources(c: Context<{ Bindings: Env }>, response: Response): Promise<Response> {
|
|
83
|
+
const payload = (await response.json()) as DocResourcesPayload;
|
|
84
|
+
const headers = new Headers(response.headers);
|
|
85
|
+
headers.delete("X-Fetch-Result");
|
|
86
|
+
|
|
87
|
+
if (!Array.isArray(payload?.resources)) {
|
|
88
|
+
return new Response(JSON.stringify(payload), { status: response.status, headers });
|
|
89
|
+
}
|
|
90
|
+
payload.resources = await hydrateResources(c, payload.resources);
|
|
91
|
+
return new Response(JSON.stringify(payload), { status: response.status, headers });
|
|
92
|
+
}
|