@leed-ai/cli 0.0.0 → 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.
Files changed (263) hide show
  1. package/LICENSE.md +57 -0
  2. package/README.md +120 -3
  3. package/dist/LICENSE.md +57 -0
  4. package/dist/acorn-DAgDT1om.js +5087 -0
  5. package/dist/angular-l0_4PK8N.js +3598 -0
  6. package/dist/authShared-Dray38Ls.js +1161 -0
  7. package/dist/babel-CJeRQd5u.js +10038 -0
  8. package/dist/buildResults-tiq1nugP.js +21180 -0
  9. package/dist/chunk-1VJ9lAw7.js +42 -0
  10. package/dist/cliAuth-38JjD6hw.js +15 -0
  11. package/dist/env-B0bHiXJJ.js +29 -0
  12. package/dist/errors-CqKamGCu.js +481 -0
  13. package/dist/estree-E8WG8UHj.js +6930 -0
  14. package/dist/flow-6xaRIbPQ.js +52255 -0
  15. package/dist/glimmer-aJPjsfbT.js +7272 -0
  16. package/dist/graphql-mcgtPEiZ.js +1901 -0
  17. package/dist/html--eeea3Kl.js +6553 -0
  18. package/dist/leed.eleventy.mjs +6525 -0
  19. package/dist/leed.mjs +88952 -0
  20. package/dist/leed.sources.json +798 -0
  21. package/dist/leedLogger-Zkada5aO.js +78 -0
  22. package/dist/markdown-D73jNKwx.js +6836 -0
  23. package/dist/meriyah-CMp1nDDW.js +8932 -0
  24. package/dist/package.json +101 -0
  25. package/dist/postcss-h4fBr2Fn.js +6760 -0
  26. package/dist/prettier-CwFSKxfN.js +16821 -0
  27. package/dist/static/customer-site-worker/package.json +9 -0
  28. package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
  29. package/dist/static/customer-site-worker/src/constants.ts +20 -0
  30. package/dist/static/customer-site-worker/src/cors.ts +120 -0
  31. package/dist/static/customer-site-worker/src/index.ts +242 -0
  32. package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
  33. package/dist/static/customer-site-worker/src/proxy.ts +92 -0
  34. package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
  35. package/dist/static/customer-site-worker/src/tier.ts +40 -0
  36. package/dist/static/customer-site-worker/src/types.ts +82 -0
  37. package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
  38. package/dist/static/customer-site-worker/tsconfig.json +19 -0
  39. package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
  40. package/dist/static/install/husky/colors.mjs +5 -0
  41. package/dist/static/install/husky/hooks/commit-msg +1 -0
  42. package/dist/static/install/husky/hooks/pre-commit +1 -0
  43. package/dist/static/install/husky/hooks/pre-push +1 -0
  44. package/dist/static/install/husky/valid-command.mjs +29 -0
  45. package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
  46. package/dist/static/install/package.json +10 -0
  47. package/dist/static/install/packages.txt +4 -0
  48. package/dist/static/install/tasks.json +111 -0
  49. package/dist/static/javascript/detectIncognito.js +195 -0
  50. package/dist/static/javascript/documentation.js +2781 -0
  51. package/dist/static/javascript/fp/fp.esm.js +2896 -0
  52. package/dist/static/javascript/fp/fp.js +3067 -0
  53. package/dist/static/javascript/live-search.js +986 -0
  54. package/dist/static/javascript/lunr-search.js +843 -0
  55. package/dist/static/javascript/timeme.js +451 -0
  56. package/dist/static/javascript/tracker/webmcp.js +67 -0
  57. package/dist/static/javascript/tracker/whisper.js +1 -0
  58. package/dist/static/javascript/utilities.js +819 -0
  59. package/dist/static/javascript/versions.json +74 -0
  60. package/dist/static/javascript/zoomable.js +488 -0
  61. package/dist/static/site/_data/eleventyComputed.js +228 -0
  62. package/dist/static/site/_data/leed-form-data.json +253 -0
  63. package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
  64. package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
  65. package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
  66. package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
  67. package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
  68. package/dist/static/site/_includes/leed/api/object.hbs +110 -0
  69. package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
  70. package/dist/static/site/_includes/leed/api/property.hbs +153 -0
  71. package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
  72. package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
  73. package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
  74. package/dist/static/site/_includes/leed/api/security.hbs +75 -0
  75. package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
  76. package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
  77. package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
  78. package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
  79. package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
  80. package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
  81. package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
  82. package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
  83. package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
  84. package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
  85. package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
  86. package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
  87. package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
  88. package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
  89. package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
  90. package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
  91. package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
  92. package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
  93. package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
  94. package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
  95. package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
  96. package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
  97. package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
  98. package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
  99. package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
  100. package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
  101. package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
  102. package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
  103. package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
  104. package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
  105. package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
  106. package/dist/static/site/_includes/leed/form.hbs +266 -0
  107. package/dist/static/site/_includes/leed/head.hbs +67 -0
  108. package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
  109. package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
  110. package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
  111. package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
  112. package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
  113. package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
  114. package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
  115. package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
  116. package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
  117. package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
  118. package/dist/static/site/leed-feed-atom.hbs +42 -0
  119. package/dist/static/site/leed-feed-json.hbs +56 -0
  120. package/dist/static/site/leed-feed-rss.hbs +39 -0
  121. package/dist/static/site/leed-headers.hbs +38 -0
  122. package/dist/static/site/leed-redirects.hbs +10 -0
  123. package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
  124. package/dist/static/site/leed-sitemap.hbs +19 -0
  125. package/dist/static/site/leed-unsubscribe.hbs +44 -0
  126. package/dist/static/site/leed-unsubscribed.hbs +40 -0
  127. package/dist/static/tailwind/components/alerts.css +132 -0
  128. package/dist/static/tailwind/components/api-methods.css +39 -0
  129. package/dist/static/tailwind/components/breadcrumbs.css +31 -0
  130. package/dist/static/tailwind/components/code.css +88 -0
  131. package/dist/static/tailwind/components/collapsible.css +38 -0
  132. package/dist/static/tailwind/components/document-search.css +222 -0
  133. package/dist/static/tailwind/components/form.css +185 -0
  134. package/dist/static/tailwind/components/kbd.css +7 -0
  135. package/dist/static/tailwind/components/pagination.css +81 -0
  136. package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
  137. package/dist/static/tailwind/components/tab-groups.css +117 -0
  138. package/dist/static/tailwind/components/tables.css +0 -0
  139. package/dist/static/tailwind/components/toc.css +60 -0
  140. package/dist/static/tailwind/components.css +248 -0
  141. package/dist/static/tailwind/defaults.css +260 -0
  142. package/dist/static/tailwind/docs/api.css +701 -0
  143. package/dist/static/tailwind/docs/components/blockquote.css +5 -0
  144. package/dist/static/tailwind/docs/components/button.css +11 -0
  145. package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
  146. package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
  147. package/dist/static/tailwind/docs/components/navigation.css +279 -0
  148. package/dist/static/tailwind/docs/components/table.css +54 -0
  149. package/dist/static/tailwind/docs/components.css +6 -0
  150. package/dist/static/tailwind/docs/docs.css +427 -0
  151. package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
  152. package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
  153. package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
  154. package/dist/static/tailwind/docs/layouts.css +3 -0
  155. package/dist/static/tailwind/docs/themes/amber.css +113 -0
  156. package/dist/static/tailwind/docs/themes/blue.css +94 -0
  157. package/dist/static/tailwind/docs/themes/cyan.css +111 -0
  158. package/dist/static/tailwind/docs/themes/emerald.css +111 -0
  159. package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
  160. package/dist/static/tailwind/docs/themes/green.css +95 -0
  161. package/dist/static/tailwind/docs/themes/indigo.css +111 -0
  162. package/dist/static/tailwind/docs/themes/lime.css +111 -0
  163. package/dist/static/tailwind/docs/themes/orange.css +111 -0
  164. package/dist/static/tailwind/docs/themes/pink.css +111 -0
  165. package/dist/static/tailwind/docs/themes/purple.css +111 -0
  166. package/dist/static/tailwind/docs/themes/red.css +111 -0
  167. package/dist/static/tailwind/docs/themes/rose.css +111 -0
  168. package/dist/static/tailwind/docs/themes/sky.css +111 -0
  169. package/dist/static/tailwind/docs/themes/teal.css +111 -0
  170. package/dist/static/tailwind/docs/themes/violet.css +111 -0
  171. package/dist/static/tailwind/docs/themes/yellow.css +111 -0
  172. package/dist/static/tailwind/docs/themes.css +84 -0
  173. package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
  174. package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
  175. package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
  176. package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
  177. package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
  178. package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
  179. package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
  180. package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
  181. package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
  182. package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
  183. package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
  184. package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
  185. package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
  186. package/dist/static/tailwind/fonts/all.css +6 -0
  187. package/dist/static/tailwind/fonts/config.jsonc +31 -0
  188. package/dist/static/tailwind/fonts/geist-mono.css +142 -0
  189. package/dist/static/tailwind/fonts/geist.css +142 -0
  190. package/dist/static/tailwind/fonts/inter.css +215 -0
  191. package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
  192. package/dist/static/tailwind/fonts/noto-sans.css +134 -0
  193. package/dist/static/tailwind/fonts/open-sans.css +89 -0
  194. package/dist/static/tailwind/highlighters.css +14 -0
  195. package/dist/static/tailwind/hljs/README.md +53 -0
  196. package/dist/static/tailwind/hljs/a11y.css +96 -0
  197. package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
  198. package/dist/static/tailwind/hljs/atom-one.css +164 -0
  199. package/dist/static/tailwind/hljs/default.css +164 -0
  200. package/dist/static/tailwind/hljs/github.css +101 -0
  201. package/dist/static/tailwind/hljs/google-code.css +112 -0
  202. package/dist/static/tailwind/hljs/grayscale.css +115 -0
  203. package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
  204. package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
  205. package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
  206. package/dist/static/tailwind/hljs/papercolor.css +120 -0
  207. package/dist/static/tailwind/hljs/qt-creator.css +91 -0
  208. package/dist/static/tailwind/hljs/summerfruit.css +115 -0
  209. package/dist/static/tailwind/hljs/tomorrow.css +115 -0
  210. package/dist/static/tailwind/leed.css +170 -0
  211. package/dist/static/tailwind/site-tokens.reference.css +105 -0
  212. package/dist/static/tailwind/utils/icons.css +102 -0
  213. package/dist/static/tailwind/utils/utilities.css +105 -0
  214. package/dist/static/templates/pages/PAGE.hbs +27 -0
  215. package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
  216. package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
  217. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
  218. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
  219. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
  220. package/dist/static/templates/raw-content/.ci/build.sh +58 -0
  221. package/dist/static/templates/raw-content/README.md +179 -0
  222. package/dist/static/templates/raw-content/_gitignore +50 -0
  223. package/dist/static/templates/raw-content/src/404.hbs +21 -0
  224. package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
  225. package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
  226. package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
  227. package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
  228. package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
  229. package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
  230. package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
  231. package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
  232. package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
  233. package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
  234. package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
  235. package/dist/static/templates/raw-content/src/index.hbs +21 -0
  236. package/dist/static/templates/raw-content/src/robots.hbs +9 -0
  237. package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
  238. package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
  239. package/dist/static/templates/sandbox/_gitignore +50 -0
  240. package/dist/static/templates/sandbox/identity.json +1 -0
  241. package/dist/static/templates/sandbox/src/404.hbs +21 -0
  242. package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
  243. package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
  244. package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
  245. package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
  246. package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
  247. package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
  248. package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
  249. package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
  250. package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
  251. package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
  252. package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
  253. package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
  254. package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
  255. package/dist/static/templates/sandbox/src/index.hbs +20 -0
  256. package/dist/static/templates/sandbox/src/robots.hbs +9 -0
  257. package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
  258. package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
  259. package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
  260. package/dist/thrown-sukVkp_1.js +113 -0
  261. package/dist/typescript-D4T0V_Hn.js +23004 -0
  262. package/dist/yaml-9wR8GxF_.js +5236 -0
  263. package/package.json +152 -10
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "customer-site-worker",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "dependencies": {
7
+ "hono": "4.12.12"
8
+ }
9
+ }
@@ -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
+ }